diff --git a/app/(auth)/login/index.tsx b/app/(auth)/login/index.tsx index ab0f4d9..094cae5 100644 --- a/app/(auth)/login/index.tsx +++ b/app/(auth)/login/index.tsx @@ -2,11 +2,20 @@ import { useTheme } from "@/lib/theme/context"; import { BtnText, Button } from "@/components/button"; import { Link, router } from "expo-router"; import { HomeIcon } from "lucide-react-native"; -import { StyleSheet, Text, View, useColorScheme, Platform } from "react-native"; +import { + StyleSheet, + Text, + View, + useColorScheme, + Platform, + Pressable, +} from "react-native"; import { Image } from "expo-image"; import { Theme } from "@/lib/theme/theme"; import { createT } from "@/i18n"; import { useAppleAuth } from "@/hooks/useAppleAuth"; +import * as WebBrowser from "expo-web-browser"; +import { BROWSER_OPTS } from "@/lib/web/browserConfig"; export default function Login() { const { gs, theme } = useTheme(); @@ -16,12 +25,6 @@ export default function Login() { const { signInWithApple, loading: appleLoading, error: appleError } = useAppleAuth(); - // useEffect(() => { - // router.prefetch("/login/withEmail"); - // router.prefetch("/login/withPhoneNumber"); - // router.prefetch("/register"); - // }, []); - return ( @@ -153,25 +156,33 @@ export default function Login() { )} - {/* - By continuing, you agree to our{" "} - alert("Terms of Service")} + + {t("continuingWith")}{" "} + { + WebBrowser.openBrowserAsync( + "https://mindsofd23b.github.io/Landing-Elysio/termsandconditions/", + BROWSER_OPTS(theme), + ); + }} > - {t("termsOfService")} - {" "} + + {t("termsOfService")} + + {" "} {t("and")}{" "} - alert("Privacy Policy")} + { + WebBrowser.openBrowserAsync( + "https://mindsofd23b.github.io/Landing-Elysio/privacypolicy/", + BROWSER_OPTS(theme), + ); + }} > - {t("privacyPolicy")} - + {t("privacyPolicy")} + . - */} + ); diff --git a/app/(auth)/update.tsx b/app/(auth)/update.tsx index 728e741..879e6d4 100644 --- a/app/(auth)/update.tsx +++ b/app/(auth)/update.tsx @@ -1,8 +1,11 @@ +import { createT } from "@/i18n"; import { View, Text, StyleSheet, Animated, Easing } from "react-native"; import { Settings } from "lucide-react-native"; import { useEffect, useRef } from "react"; import { useTheme } from "@/lib/theme/context"; +const t = createT("update"); + interface UpdateScreenProps { duration?: string; } @@ -46,14 +49,11 @@ export default function UpdateScreen({ duration }: UpdateScreenProps) { - Update in progress - - We{"'"}re updating Elysio to bring you new features and improvements. The - app will be back shortly. - + {t("title")} + {t("body")} - Estimated time - {duration ?? "Unknown"} + {t("estimatedTime")} + {duration ?? t("unknown")} ); diff --git a/app/(onboarding)/index.tsx b/app/(onboarding)/index.tsx index eabda32..1d26919 100644 --- a/app/(onboarding)/index.tsx +++ b/app/(onboarding)/index.tsx @@ -1,3 +1,4 @@ +import { createT } from "@/i18n"; import { BtnText, Button } from "@/components/button"; import { useTheme } from "@/lib/theme/context"; import { get, store } from "@/utils/store"; @@ -8,6 +9,8 @@ import { Bell, Heart, Zap } from "lucide-react-native"; import { useEffect, useState } from "react"; import { Text, View } from "react-native"; +const t = createT("onboarding"); + const MAX_PAGE = 3; export default function OnboardingScreen() { @@ -104,7 +107,7 @@ export default function OnboardingScreen() { lineHeight: 42, }} > - Find your{"\n"}person. + {t("page0.title")} - Dating, reimagined. Connect with people who truly match you. + {t("page0.subtitle")} )} @@ -157,7 +160,7 @@ export default function OnboardingScreen() { fontSize: 14, }} > - You have a new match + {t("page1.notificationTitle")} - Someone liked your profile β€” say hi! + {t("page1.notificationSubtitle")} @@ -181,7 +184,7 @@ export default function OnboardingScreen() { lineHeight: 36, }} > - Never miss a{"\n"}connection. + {t("page1.title")} - We{"'"}ll notify you when you get a new match or message β€” so - you never leave someone waiting. + {t("page1.subtitle")} )} @@ -232,7 +236,7 @@ export default function OnboardingScreen() { fontWeight: "600", }} > - Real connections + {t("page2.realConnections")} - No ghosting + {t("page2.noGhosting")} @@ -268,7 +272,7 @@ export default function OnboardingScreen() { lineHeight: 36, }} > - Modern dating{"\n"}is broken. + {t("page2.title")} - Endless swiping, shallow profiles, ghosting β€” you deserve - better. Elysio is built around real conversations and genuine - matches. + {t("page2.subtitle")} )} diff --git a/app/(protected)/(tabs)/chat.tsx b/app/(protected)/(tabs)/chat.tsx index e1e856b..ccfd11d 100644 --- a/app/(protected)/(tabs)/chat.tsx +++ b/app/(protected)/(tabs)/chat.tsx @@ -1,3 +1,4 @@ +import { createT } from "@/i18n"; import { ActivityIndicator, Pressable, @@ -20,6 +21,8 @@ import { useAuthFetch } from "@/hooks/useAuthFetch"; import { useAuth } from "@/lib/auth/AuthProvider"; import { useSafeAreaControl } from "@/components/SafeArea"; +const t = createT("chat"); + // Design made with Pinterest and ChatGPT interface ChatResponse { room: { @@ -64,9 +67,9 @@ async function mapChatResponses(data: ChatResponse[], userId: string): Promise - {isSearching ? "Keine Chats gefunden" : "Noch keine Chats"} + {isSearching ? t("empty.noChatsFound") : t("empty.noChats")} {isSearching - ? "Zu deiner Suche konnten wir keine Unterhaltung finden. Versuche es mit einem anderen Namen." - : "Hier erscheinen deine Matches und Nachrichten. Starte einen neuen Kontakt und bringe das erste Gespraech ins Rollen."} + ? t("empty.noChatsFoundSubtitle") + : t("empty.noChatsSubtitle")} - {isSearching ? "Suche zuruecksetzen" : "Neue Leute entdecken"} + {isSearching ? t("empty.resetSearch") : t("empty.discover")} diff --git a/app/(protected)/(tabs)/favorites.tsx b/app/(protected)/(tabs)/favorites.tsx index c15d95e..4069af5 100644 --- a/app/(protected)/(tabs)/favorites.tsx +++ b/app/(protected)/(tabs)/favorites.tsx @@ -1,5 +1,6 @@ // Made with the help of Claude.ai and ChatGPT +import { createT } from "@/i18n"; import { router, useFocusEffect } from "expo-router"; import { CategoryGridCard } from "@/components/favorites/CategoryCard"; import { DateIdeaCard } from "@/components/favorites/DateIdeaCard"; @@ -57,6 +58,8 @@ const CATEGORIES = [ { label: "CHILL", icon: Armchair }, ]; +const t = createT("favorites"); + export default function Favorites() { const { theme } = useTheme(); const s = makeStyles(theme); @@ -129,9 +132,9 @@ export default function Favorites() { {/* ── Explore Categories ── */} - Explore Categories + {t("exploreCategories")} router.push("/favorites/allcategories")}> - View All + {t("viewAll")} @@ -148,7 +151,7 @@ export default function Favorites() { {/* ── Winning Tips ── */} - Winning Tips + {t("winningTips")} {TIPS.map((tip, i) => ( { if (await StoreReview.isAvailableAsync()) { await StoreReview.requestReview(); @@ -138,7 +141,7 @@ export default function Index() { const onStart = () => { if (!canCall(plan, callsData?.callsToday)) { - return alert("You have no more calls today. Upgrade your plan for more!"); + return alert(t("noMoreCalls")); } if (callsData?.callsToday === 3) { handleReview(); diff --git a/app/(protected)/(tabs)/settings.tsx b/app/(protected)/(tabs)/settings.tsx index 9c9e3f6..ea9daf8 100644 --- a/app/(protected)/(tabs)/settings.tsx +++ b/app/(protected)/(tabs)/settings.tsx @@ -19,17 +19,11 @@ import * as WebBrowser from "expo-web-browser"; import { BtnText, Button } from "@/components/button"; import { useSafeAreaControl } from "@/components/SafeArea"; import { useCallback } from "react"; +import { BROWSER_OPTS } from "@/lib/web/browserConfig"; const t = createT("auth.settings"); const version = Constants.expoConfig?.version; -const BROWSER_OPTS = (theme: any): Parameters[1] => ({ - presentationStyle: WebBrowser.WebBrowserPresentationStyle.FORM_SHEET, - controlsColor: theme.primary, - toolbarColor: theme.background, - enableBarCollapsing: true, -}); - function SectionLabel({ label }: { label: string }) { return {label}; } @@ -126,7 +120,7 @@ export default function SettingsScreen() { /> router.push("/settings/security")} /> {/* ── App ── */} - + {/* ── Legal ── */} - + {/* ── Version ── */} - + - Version + {t("version")} {version} diff --git a/app/(protected)/(tabs)/stats.tsx b/app/(protected)/(tabs)/stats.tsx index ed4f1f1..f6e888a 100644 --- a/app/(protected)/(tabs)/stats.tsx +++ b/app/(protected)/(tabs)/stats.tsx @@ -1,5 +1,6 @@ // Made with the help of Claude.ai and ChatGPT +import { createT } from "@/i18n"; import { router, useFocusEffect } from "expo-router"; import { View, Text, TouchableOpacity, StyleSheet, ScrollView } from "react-native"; import { useTheme } from "@/lib/theme/context"; @@ -7,6 +8,8 @@ import { Check, Clock, Flame, Heart, Snowflake } from "lucide-react-native"; import { useSafeAreaControl } from "@/components/SafeArea"; import { useCallback } from "react"; +const t = createT("stats"); + const DAYS = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"]; const TODAY_INDEX = 4; const FREEZE_INDEX = 1; @@ -47,14 +50,12 @@ export default function Index() { activeOpacity={0.8} > - PREMIUM - Find your perfect match - - Exclusive insights Β· Unlimited features - + {t("premium")} + {t("findYourMatch")} + {t("exclusiveInsights")} - Upgrade + {t("upgrade")} @@ -66,11 +67,11 @@ export default function Index() { > - Mutual Interests + {t("mutualInterests")} 64%{" "} - MATCH SCORE + {t("matchScore")} @@ -80,11 +81,11 @@ export default function Index() { > - Average waiting time + {t("averageWaitingTime")} 6.32{" "} - SECONDS + {t("seconds")} @@ -106,11 +107,11 @@ export default function Index() { {" "} - day streak + {t("dayStreak")} - Keep it up, you{"'"}re on a roll! + {t("keepItUp")} @@ -187,7 +188,7 @@ export default function Index() { {/* ── Best Time to be Online ── */} - Best time to be online + {t("bestTimeOnline")} {BAR_VALUES.map((val, i) => { const isPeak = i === PEAK_INDEX; @@ -223,14 +224,12 @@ export default function Index() { ); })} - - Activity peaks between 8 PM and 10 PM in your area. - + {t("activityPeaks")} {/* ── Top Match Interests ── */} - Top Match Interests + {t("topMatchInterests")} {[ { label: "MUSIC", color: theme.interestMusic }, diff --git a/app/(protected)/chats/[id].tsx b/app/(protected)/chats/[id].tsx index 46c7b76..4c20f69 100644 --- a/app/(protected)/chats/[id].tsx +++ b/app/(protected)/chats/[id].tsx @@ -1,3 +1,4 @@ +import { createT } from "@/i18n"; import { useAuthFetch } from "@/hooks/useAuthFetch"; import { useTheme } from "@/lib/theme/context"; import { Chat } from "@/types/chats"; @@ -41,6 +42,8 @@ import { import { MessageBubble } from "@/components/messageBubble"; import { useRoomSocket } from "@/hooks/useRoomSocket"; import { useSafeAreaControl } from "@/components/SafeArea"; + +const t = createT("chat"); // Design made with Pinterest and ChatGPT // Wie viele Pixel vom unteren Ende entfernt gilt noch als "unten" @@ -319,10 +322,10 @@ export default function ChatsScreen() { useRoomSocket(id, async (incoming) => { if (incoming.senderId === currentUserId) return; - let text = "[Encrypted message]"; + let text = t("encryptedMessage"); if (incoming.type !== "text") { - text = "Voice message"; + text = t("voiceMessage"); } else { const myKey = incoming.encryptedKeys.find((k) => k.userId === currentUserId); if (myKey) { @@ -334,7 +337,7 @@ export default function ChatsScreen() { encryptedKey: myKey.encryptedKey, }); } catch { - text = "[Unable to decrypt]"; + text = t("unableToDecrypt"); } } } @@ -539,7 +542,7 @@ export default function ChatsScreen() { > - 🚩 Report {user.name} + {t("report", { name: user.name })} - Cancel + {t("cancel")} @@ -783,7 +786,7 @@ function EmptyMessagesState({ name, onPress }: { name: string; onPress: () => vo marginBottom: 12, }} > - No messages yet + {t("noMessagesYet")} vo marginBottom: 28, }} > - Start the conversation with {firstName}. A simple hello can turn into - something special. + {t("startConversation", { name: firstName })} vo fontWeight: "700", }} > - Say hi + {t("sayHi")} diff --git a/app/(protected)/datePickerModal.tsx b/app/(protected)/datePickerModal.tsx index 08d4a1d..8e8e71a 100644 --- a/app/(protected)/datePickerModal.tsx +++ b/app/(protected)/datePickerModal.tsx @@ -1,3 +1,4 @@ +import { createT } from "@/i18n"; import { router, useLocalSearchParams } from "expo-router"; import { useState } from "react"; import { View, Text, StyleSheet, useColorScheme } from "react-native"; @@ -5,6 +6,8 @@ import DateTimePicker from "@react-native-community/datetimepicker"; import { BtnText, Button } from "@/components/button"; import { datePickerCallback } from "@/utils/datePickerCallback"; +const t = createT("datePicker"); + export default function DatePickerModal() { const { birthday } = useLocalSearchParams<{ birthday: string; @@ -24,7 +27,7 @@ export default function DatePickerModal() { return ( - Date of Birth + {t("title")} ); diff --git a/app/(protected)/favorites/allcategories.tsx b/app/(protected)/favorites/allcategories.tsx index b1340f9..9f0dcb1 100644 --- a/app/(protected)/favorites/allcategories.tsx +++ b/app/(protected)/favorites/allcategories.tsx @@ -1,3 +1,4 @@ +import { createT } from "@/i18n"; import { useTheme } from "@/lib/theme/context"; import { router, Stack } from "expo-router"; import { @@ -39,6 +40,8 @@ import { DateIdeaList, } from "@/components/favorites/CategoryCard"; +const t = createT("favorites"); + const { width } = Dimensions.get("window"); const H_PAD = 16; const COL_GAP = 10; @@ -751,7 +754,9 @@ export default function AllCategories() { router.back()} style={s.backBtn}> - Categories + + {t("categories")} + diff --git a/app/(protected)/favorites/ideadetail.tsx b/app/(protected)/favorites/ideadetail.tsx index a63fde8..433d242 100644 --- a/app/(protected)/favorites/ideadetail.tsx +++ b/app/(protected)/favorites/ideadetail.tsx @@ -1,8 +1,11 @@ +import { createT } from "@/i18n"; import { useTheme } from "@/lib/theme/context"; import { router, Stack, useLocalSearchParams } from "expo-router"; import { ChevronLeft, Clock, MapPin, Sparkles, Star, Zap } from "lucide-react-native"; import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native"; +const t = createT("favorites.ideaDetail"); + const EFFORT_COLOR: Record = { Low: "#4CAF50", Medium: "#FF9800", @@ -37,7 +40,7 @@ export default function IdeaDetail() { - TOP PICK + {t("topPick")} )} @@ -66,14 +69,14 @@ export default function IdeaDetail() { - EFFORT + {t("effort")} {params.effort} - DURATION + {t("duration")} {params.duration} @@ -89,7 +92,7 @@ export default function IdeaDetail() { color={theme.planPremium} strokeWidth={2} /> - VIBE + {t("vibe")} {params.vibe} @@ -98,37 +101,25 @@ export default function IdeaDetail() { {/* Tips section */} - Why this works + {t("whyThisWorks")} - A{" "} - - {params.vibe.toLowerCase()} - {" "} - date like this keeps things fresh and gives you both - something to talk about. The{" "} - - {params.effort} - {" "} - effort level means it{"'"}s easy to plan without pressure. + {t("whyBody", { + vibe: params.vibe.toLowerCase(), + effort: params.effort, + })} - What to keep in mind + {t("keepInMind")} - Budget around{" "} - - ${params.price} - {" "} - and block out{" "} - - {params.duration} - - . Book in advance if the venue is popular β€” especially on - weekends. + {t("keepInMindBody", { + price: params.price, + duration: params.duration, + })} diff --git a/app/(protected)/favorites/tipdetail.tsx b/app/(protected)/favorites/tipdetail.tsx index ad28ad1..8979e04 100644 --- a/app/(protected)/favorites/tipdetail.tsx +++ b/app/(protected)/favorites/tipdetail.tsx @@ -1,3 +1,4 @@ +import { createT } from "@/i18n"; import { useTheme } from "@/lib/theme/context"; import { Link, Stack, useLocalSearchParams } from "expo-router"; import { Sparkles } from "lucide-react-native"; @@ -6,6 +7,8 @@ import { TipSection } from "@/components/favorites/TipSection"; import { TIP_CONTENT } from "@/lib/favorites/tips"; import BackWrapper from "@/components/backwrapper"; +const t = createT("favorites.tipDetail"); + export default function TipDetail() { const { theme } = useTheme(); const { tip } = useLocalSearchParams<{ tip: string }>(); @@ -16,7 +19,7 @@ export default function TipDetail() { if (!data) { return ( - Tip not found. + {t("tipNotFound")} ); } @@ -89,15 +92,15 @@ export default function TipDetail() { ]} > - PREMIUM + {t("premium")} - Continue reading + {t("continueReading")} - Unlock the full guide with Premium + {t("unlockWithPremium")} diff --git a/app/(protected)/report.tsx b/app/(protected)/report.tsx index e1b550f..17af293 100644 --- a/app/(protected)/report.tsx +++ b/app/(protected)/report.tsx @@ -1,4 +1,5 @@ import { useState } from "react"; +import { createT } from "@/i18n"; import { View, Text, @@ -16,6 +17,8 @@ import { Ionicons } from "@expo/vector-icons"; import { useTheme } from "@/lib/theme/context"; import { Theme } from "@/lib/theme/theme"; +const t = createT("report"); + interface ReportedUser { id: string; name: string; @@ -29,14 +32,14 @@ interface Props { } const REPORT_REASONS = [ - "Inappropriate behaviour", - "Explicit content", - "Harassment or bullying", - "Hate speech", - "Illegal activities", - "Spam or scam", - "Underage user", - "Impersonation", + t("reasons.inappropriateBehaviour"), + t("reasons.explicitContent"), + t("reasons.harassmentOrBullying"), + t("reasons.hateSpeech"), + t("reasons.illegalActivities"), + t("reasons.spamOrScam"), + t("reasons.underageUser"), + t("reasons.impersonation"), ]; export default function ReportUser({ user, onBack, onSubmit }: Props) { @@ -58,21 +61,18 @@ export default function ReportUser({ user, onBack, onSubmit }: Props) { async function handleSubmit(andBlock: boolean) { if (!reportText) { - Alert.alert( - "Missing reason", - "Please select or describe the reason for your report.", - ); + Alert.alert(t("missingReason"), t("missingReasonBody")); return; } setSubmitting(true); try { onSubmit?.(reportText, andBlock); Alert.alert( - andBlock ? "Reported & Blocked" : "Report Submitted", + andBlock ? t("reportedAndBlocked") : t("reportSubmitted"), andBlock - ? `${user.name} has been reported and blocked.` - : `Your report has been submitted. We'll review it shortly.`, - [{ text: "OK", onPress: onBack }], + ? t("reportedAndBlockedBody", { name: user.name }) + : t("reportSubmittedBody"), + [{ text: t("ok"), onPress: onBack }], ); } finally { setSubmitting(false); @@ -88,7 +88,7 @@ export default function ReportUser({ user, onBack, onSubmit }: Props) { - Report + {t("title")} @@ -116,14 +116,14 @@ export default function ReportUser({ user, onBack, onSubmit }: Props) { - You are reporting + {t("youAreReporting")} {user.name} - What is the issue? + {t("whatIsTheIssue")} {REPORT_REASONS.map((reason) => ( - Add more detail (optional) + {t("addDetail")} - - Reports are anonymous. Our team reviews every report within 24 - hours. - + {t("anonymous")} @@ -179,7 +176,7 @@ export default function ReportUser({ user, onBack, onSubmit }: Props) { onPress={() => handleSubmit(false)} disabled={submitting} > - Submit report + {t("submitReport")} - Submit & Block + {t("submitAndBlock")} diff --git a/app/(protected)/settings/apperance.tsx b/app/(protected)/settings/apperance.tsx index fea259d..d42d458 100644 --- a/app/(protected)/settings/apperance.tsx +++ b/app/(protected)/settings/apperance.tsx @@ -1,3 +1,4 @@ +import { createT } from "@/i18n"; import { useTheme } from "@/lib/theme/context"; import { colors, strToOption, ThemeOptions } from "@/lib/theme/theme"; import BackWrapper from "@/components/backwrapper"; @@ -9,6 +10,8 @@ import { Pressable, StyleSheet, Switch, Text, useColorScheme, View } from "react import { useSafeAreaControl } from "@/components/SafeArea"; import { useFocusEffect } from "expo-router"; +const t = createT("settings.appearance"); + export default function Apperance() { const COLOR_SCHEME = useColorScheme(); const [mode, setModeRaw] = useState(); @@ -58,7 +61,7 @@ export default function Apperance() { return ( - Theme + {t("theme")} - Light + + {t("light")} + - System Theme + {t("systemTheme")} - Edit Profile + {t("title")} @@ -248,29 +250,29 @@ export default function PersonalDetails() { { @@ -281,20 +283,20 @@ export default function PersonalDetails() { }} /> - Bio + {t("bio")} {bioCharactersLeft} - Gallery + {t("gallery")} setIsEditingGallery((value) => !value)} > - {isEditingGallery ? "Done" : "Edit"} + {isEditingGallery ? t("done") : t("edit")} @@ -348,7 +350,9 @@ export default function PersonalDetails() { size={28} color={theme.primary} /> - Add + + {t("add")} + )} @@ -370,7 +374,7 @@ export default function PersonalDetails() { diff --git a/app/(protected)/settings/security.tsx b/app/(protected)/settings/security.tsx index e24b84b..cbc3f84 100644 --- a/app/(protected)/settings/security.tsx +++ b/app/(protected)/settings/security.tsx @@ -1,3 +1,4 @@ +import { createT } from "@/i18n"; import BackWrapper from "@/components/backwrapper"; import { BtnText, Button, Loader } from "@/components/button"; import Input from "@/components/input"; @@ -9,6 +10,8 @@ import { useSafeAreaControl } from "@/components/SafeArea"; import { useFocusEffect, router } from "expo-router"; import { useAuthFetch } from "@/hooks/useAuthFetch"; +const t = createT("settings.security"); + export default function Security() { const { gs, theme } = useTheme(); const styles = makeStyles(); @@ -34,15 +37,15 @@ export default function Security() { async function handleSave() { if (!currentPassword || !newPassword || !confirmPassword) { - Alert.alert("Error", "Please fill in all fields."); + Alert.alert(t("error"), t("errors.fillAllFields")); return; } if (newPassword !== confirmPassword) { - Alert.alert("Error", "New passwords do not match."); + Alert.alert(t("error"), t("errors.passwordsNoMatch")); return; } if (newPassword.length < 8) { - Alert.alert("Error", "Password must be at least 8 characters."); + Alert.alert(t("error"), t("errors.passwordTooShort")); return; } @@ -51,11 +54,11 @@ export default function Security() { method: "PATCH", body: JSON.stringify({ currentPassword, newPassword }), }); - Alert.alert("Success", "Password changed successfully.", [ - { text: "OK", onPress: () => router.back() }, + Alert.alert(t("success"), t("successMessage"), [ + { text: t("ok"), onPress: () => router.back() }, ]); } catch (err: any) { - Alert.alert("Error", err?.message ?? "Something went wrong."); + Alert.alert(t("error"), err?.message ?? t("errors.somethingWentWrong")); } } @@ -67,19 +70,19 @@ export default function Security() { enableOnAndroid extraScrollHeight={20} > - Security + {t("title")} - Change your password below. + {t("subtitle")} - Current Password + {t("currentPassword")} - New Password + {t("newPassword")} - Confirm New Password + {t("confirmNewPassword")} diff --git a/app/(protected)/subscriptions/checkout.tsx b/app/(protected)/subscriptions/checkout.tsx index 8f3f4ae..fa9db0b 100644 --- a/app/(protected)/subscriptions/checkout.tsx +++ b/app/(protected)/subscriptions/checkout.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { createT } from "@/i18n"; import * as WebBrowser from "expo-web-browser"; import { View, @@ -17,6 +18,8 @@ import { type BillingCycle, type Plan, PLANS_MAP as PLANS } from "@/lib/plans"; import Purchases, { PurchasesPackage } from "react-native-purchases"; import { usePurchasesReady } from "@/lib/PurchasesContext"; +const t = createT("subscriptions.checkout"); + const BASE_URL = "https://elysio.jamiepoeffel.ch"; // ─── Order Summary ───────────────────────────────────────────────────────────── @@ -42,18 +45,18 @@ function OrderSummary({ return ( - Order summary + {t("orderSummary")} - {plan.name} plan Β· {isYearly ? "Yearly" : "Monthly"} + {plan.name} plan Β· {isYearly ? t("yearly") : t("monthly")} {isYearly && ( - Save 17% vs monthly + {t("save17")} )} @@ -66,7 +69,7 @@ function OrderSummary({ - Total today + {t("totalToday")} {displayPrice} @@ -75,7 +78,7 @@ function OrderSummary({ {isYearly && ( - {displayPerMonth}/mo Β· billed annually + {t("billedAnnually", { perMonth: displayPerMonth })} )} @@ -87,7 +90,7 @@ function IncludedFeatures({ plan }: { plan: Plan }) { return ( - What{"'"}s included + {t("whatsIncluded")} {plan.features.map((f, i) => ( @@ -110,9 +113,9 @@ function IncludedFeatures({ plan }: { plan: Plan }) { function TrustBadges() { const { theme } = useTheme(); const items = [ - { Icon: Lock, label: "Secure payment" }, - { Icon: ShieldCheck, label: "Cancel anytime" }, - { Icon: CreditCard, label: "No hidden fees" }, + { Icon: Lock, label: t("trust.securePayment") }, + { Icon: ShieldCheck, label: t("trust.cancelAnytime") }, + { Icon: CreditCard, label: t("trust.noHiddenFees") }, ]; return ( @@ -192,9 +195,7 @@ export default function Checkout() { try { await Purchases.showManageSubscriptions(); } catch { - setError( - "Could not open cancellation. Go to Settings β†’ Apple ID β†’ Subscriptions.", - ); + setError(t("cancelError")); } finally { setLoading(false); } @@ -212,16 +213,14 @@ export default function Checkout() { const { customerInfo } = await Purchases.purchasePackage(pkg); if (typeof customerInfo.entitlements.active[planId] === "undefined") { - setError( - "Purchase completed but entitlement was not activated. Please contact support.", - ); + setError(t("entitlementError")); return; } router.replace("/(protected)/(tabs)"); } catch (err: any) { if (err?.userCancelled) return; - setError("Something went wrong during the purchase. Please try again."); + setError(t("purchaseError")); console.error("Subscription error:", err); } finally { setLoading(false); @@ -241,9 +240,11 @@ export default function Checkout() { showsVerticalScrollIndicator={false} > - Checkout + + {t("title")} + - Review your order before confirming + {t("subtitle")} @@ -302,7 +303,7 @@ export default function Checkout() { {loading ? ( ) : ( - Cancel Plan + {t("cancelPlan")} )} ) : hasOtherActivePlan ? ( @@ -313,8 +314,7 @@ export default function Checkout() { ]} > - You currently have an active {activePlanId} plan. Cancel it - first to switch. + {t("activePlanInfo", { planId: activePlanId })} ) : ( @@ -331,12 +331,14 @@ export default function Checkout() { {loading ? ( ) : ( - Confirm & Subscribe + + {t("confirmAndSubscribe")} + )} )} - By subscribing you agree to our{" "} + {t("legalNote")}{" "} WebBrowser.openBrowserAsync( @@ -352,9 +354,9 @@ export default function Checkout() { } style={{ color: theme.primary }} > - Terms and Conditions + {t("termsAndConditions")} {" "} - and{" "} + {t("and")}{" "} WebBrowser.openBrowserAsync( @@ -370,7 +372,7 @@ export default function Checkout() { } style={{ color: theme.primary }} > - Privacy Policy + {t("privacyPolicy")} . diff --git a/app/(protected)/subscriptions/index.tsx b/app/(protected)/subscriptions/index.tsx index 9f965dd..51af854 100644 --- a/app/(protected)/subscriptions/index.tsx +++ b/app/(protected)/subscriptions/index.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useRef, useState } from "react"; +import { createT } from "@/i18n"; import { View, Text, @@ -24,6 +25,8 @@ import { usePurchasesReady } from "@/lib/PurchasesContext"; import { useActivePlan } from "@/hooks/useActivePlan"; import { useSafeAreaControl } from "@/components/SafeArea"; +const t = createT("subscriptions"); + type RcPrice = { price: number; currencyCode: string }; type RcPrices = Record; @@ -60,7 +63,7 @@ function BillingToggle({ { color: value === "monthly" ? theme.text : theme.accent }, ]} > - Monthly + {t("monthly")} - Yearly + {t("yearly")} {value === "yearly" && ( @@ -119,7 +122,7 @@ function PlanCard({ const dividerColor = isHighlight ? "rgba(255,255,255,0.15)" : theme.accent + "22"; const btnBg = isHighlight ? "#fff" : theme.primary; const btnTextColor = isHighlight ? theme.primary : "#fff"; - const priceText = isMonthly ? "/mo" : "/annually"; + const priceText = isMonthly ? t("perMonth") : t("perAnnually"); const whole = Math.floor(displayPrice); const cents = (displayPrice % 1).toFixed(2).slice(1); @@ -188,7 +191,7 @@ function PlanCard({ { color: isHighlight ? "#fff" : theme.primary }, ]} > - βœ“ ACTIVE + {t("active")} @@ -225,7 +228,7 @@ function PlanCard({ activeOpacity={0.85} > - {isActive ? "Active" : "Subscribe"} + {isActive ? t("activeStatus") : t("subscribe")} @@ -373,11 +376,9 @@ export default function SubscriptionPlans() { {/* Header */} - - Choose your plan - + {t("title")} - Upgrade or downgrade at any time. + {t("subtitle")} @@ -429,7 +430,7 @@ export default function SubscriptionPlans() { - Cancel anytime Β· No hidden fees + {t("cancelAnytime")} ); diff --git a/app/(protected)/videocall/(pages)/CallScreen.tsx b/app/(protected)/videocall/(pages)/CallScreen.tsx index 0275e35..e850b6a 100644 --- a/app/(protected)/videocall/(pages)/CallScreen.tsx +++ b/app/(protected)/videocall/(pages)/CallScreen.tsx @@ -1,3 +1,4 @@ +import i18n, { createT } from "@/i18n"; import { View, Text, StyleSheet, Pressable, Animated } from "react-native"; import { RTCView } from "react-native-webrtc"; import { @@ -9,20 +10,11 @@ import { import { useSafeAreaControl } from "@/components/SafeArea"; import { useEffect } from "react"; -export const ICEBREAKERS = [ - "What's the weirdest thing you've ever eaten?", - "If you could live in any movie universe, which would you pick?", - "What's a skill you're secretly proud of?", - "What's the most spontaneous thing you've ever done?", - "If you had to eat one meal forever, what would it be?", - "What's your most controversial food opinion?", - "Would you rather explore space or the deep ocean?", - "What's the best trip you've ever been on?", - "What did you want to be as a kid?", - "What's your go-to karaoke song?", - "What's a hobby you've always wanted to try?", - "What's the last thing that made you laugh out loud?", -]; +const tCall = createT("videocall.call"); + +export const ICEBREAKERS = Array.from({ length: 12 }, (_, i) => + i18n.t(`videocall.icebreakers.${i}`), +); interface Props { remoteUrl: string | null; @@ -94,7 +86,7 @@ export function CallScreen({ /> ) : ( - Warte auf GegenΓΌber... + {tCall("waitingForPartner")} )} diff --git a/app/(protected)/videocall/(pages)/ConnectingScreen.tsx b/app/(protected)/videocall/(pages)/ConnectingScreen.tsx index 8fb76bf..0d84427 100644 --- a/app/(protected)/videocall/(pages)/ConnectingScreen.tsx +++ b/app/(protected)/videocall/(pages)/ConnectingScreen.tsx @@ -1,10 +1,13 @@ import { useNavigation } from "expo-router"; +import { createT } from "@/i18n"; import { useTheme } from "@/lib/theme/context"; import { Theme } from "@/lib/theme/theme"; import { useRef, useEffect } from "react"; import { View, Text, StyleSheet, Pressable, Animated, Easing } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; +const t = createT("videocall.connecting"); + function PulsingDots() { const { theme } = useTheme(); const dot0 = useRef(new Animated.Value(0)).current; @@ -76,10 +79,10 @@ export function ConnectingScreen({ matchState, onCancel }: Props) { const { theme } = useTheme(); const statusLabel = matchState === "matched" - ? "Match found! Connecting…" + ? t("matchFound") : matchState === "waiting" - ? "Looking for someone…" - : "Connecting to matchmaking…"; + ? t("looking") + : t("connecting"); const s = makeStyle(theme); @@ -100,15 +103,10 @@ export function ConnectingScreen({ matchState, onCancel }: Props) { {statusLabel} - {matchState === "idle" && ( - [DEV] Your state is idle - )} - {matchState === "waiting" && ( - This usually takes a few seconds - )} + {matchState === "waiting" && {t("hint")}} - Cancel + {t("cancel")} ); diff --git a/app/(protected)/videocall/(pages)/SetupScreen.tsx b/app/(protected)/videocall/(pages)/SetupScreen.tsx index 4de2388..d68d074 100644 --- a/app/(protected)/videocall/(pages)/SetupScreen.tsx +++ b/app/(protected)/videocall/(pages)/SetupScreen.tsx @@ -1,4 +1,5 @@ import { useState, useMemo } from "react"; +import { createT } from "@/i18n"; import { View, Text, StyleSheet, Pressable, ScrollView } from "react-native"; import { Ionicons, Feather } from "@expo/vector-icons"; import { useTheme } from "@/lib/theme/context"; @@ -7,14 +8,20 @@ import BackWrapper from "@/components/backwrapper"; import { BtnText, Button } from "@/components/button"; import { ArrowRightIcon, Check } from "lucide-react-native"; -const CAMERAS = [ - { id: "front", label: "Front camera" }, - { id: "back", label: "Back camera" }, -]; -const MICROPHONES = [ - { id: "default", label: "Built-in microphone" }, - { id: "headset", label: "Headset microphone" }, -]; +const t = createT("videocall.setup"); + +function getCameras() { + return [ + { id: "front", label: t("frontCamera") }, + { id: "back", label: t("backCamera") }, + ]; +} +function getMicrophones() { + return [ + { id: "default", label: t("builtInMic") }, + { id: "headset", label: t("headsetMic") }, + ]; +} const INTERESTS = [ "Music", "Travel", @@ -39,6 +46,8 @@ interface Props { } export function SetupScreen({ onConnect, onTestStreak }: Props) { + const CAMERAS = getCameras(); + const MICROPHONES = getMicrophones(); const [selectedCamera, setSelectedCamera] = useState(CAMERAS[0].id); const [selectedMic, setSelectedMic] = useState(MICROPHONES[0].id); const [selectedInterests, setSelectedInterests] = useState([]); @@ -60,17 +69,15 @@ export function SetupScreen({ onConnect, onTestStreak }: Props) { showsVerticalScrollIndicator={false} > - Ready to connect? - - Set up your devices and pick your interests - + {t("title")} + {t("subtitle")} {/* Camera */} - Camera + {t("camera")} {CAMERAS.map((cam) => ( @@ -99,7 +106,7 @@ export function SetupScreen({ onConnect, onTestStreak }: Props) { - Microphone + {t("microphone")} {MICROPHONES.map((mic) => ( @@ -126,9 +133,9 @@ export function SetupScreen({ onConnect, onTestStreak }: Props) { - Interests{" "} + {t("interests")}{" "} - Β· match with people like you + {t("interestsMatchHint")} @@ -167,7 +174,7 @@ export function SetupScreen({ onConnect, onTestStreak }: Props) { onConnect(selectedCamera, selectedMic, selectedInterests) } > - Find a match + {t("findMatch")} @@ -181,7 +188,7 @@ export function SetupScreen({ onConnect, onTestStreak }: Props) { }} > - [DEV] Test streak screen + {t("devTestStreak")} )} diff --git a/components/ChatComponent.tsx b/components/ChatComponent.tsx index 9d29c7f..24e0e62 100644 --- a/components/ChatComponent.tsx +++ b/components/ChatComponent.tsx @@ -1,3 +1,4 @@ +import { createT } from "@/i18n"; import { useTheme } from "@/lib/theme/context"; import { Theme } from "@/lib/theme/theme"; import { Chat } from "@/types/chats"; @@ -5,6 +6,8 @@ import { formatTime } from "@/utils/formatTime"; import { Image } from "expo-image"; import { Pressable, StyleSheet, Text, View } from "react-native"; +const t = createT("chat"); + interface ChatComponentProps { chat: Chat; onPress: () => void; @@ -53,7 +56,7 @@ export default function ChatComponent({ chat, onPress }: ChatComponentProps) { - {chat.lastMessage ? chat.lastMessage : "No messages yet"} + {chat.lastMessage ? chat.lastMessage : t("noMessagesYet")} diff --git a/components/ErrorScreen.tsx b/components/ErrorScreen.tsx index 81ab849..3bd53d7 100644 --- a/components/ErrorScreen.tsx +++ b/components/ErrorScreen.tsx @@ -1,3 +1,4 @@ +import { createT } from "@/i18n"; import { useEffect, useRef } from "react"; import { View, @@ -13,6 +14,8 @@ import * as Haptics from "expo-haptics"; import { useTheme } from "@/lib/theme/context"; import { router } from "expo-router"; +const t = createT("error"); + const CRACK_LENGTH = 50; const LEFT_PATH = "M 32 57 C 9 44 3 30 3 20 C 3 10 11 4 19 4 C 25 4 29 8 32 14"; @@ -277,10 +280,8 @@ export function ErrorScreen({ error, retry }: Props) { return ( - Something went wrong - - An unexpected error occurred. Try going back or tap retry to try again. - + {t("title")} + {t("body")} {__DEV__ && ( @@ -300,7 +301,7 @@ export function ErrorScreen({ error, retry }: Props) { ]} onPress={retry} > - Try again + {t("tryAgain")} )} - Go back + {t("goBack")} diff --git a/components/ServerStatusSheet.tsx b/components/ServerStatusSheet.tsx index 39b982e..b3adf46 100644 --- a/components/ServerStatusSheet.tsx +++ b/components/ServerStatusSheet.tsx @@ -1,6 +1,9 @@ +import { createT } from "@/i18n"; import { AlertTriangle, CheckCircle2 } from "lucide-react-native"; import { ActivityIndicator, StyleSheet, Text, View } from "react-native"; +const t = createT("serverStatus"); + export const ACCENT = "#d97706"; // ─── Types ─────────────────────────────────────────────────────────────────── @@ -35,7 +38,7 @@ export function RecheckPill({ seconds }: { seconds: number }) { - Rechecking in {seconds}s + {t("recheckingIn", { seconds })} diff --git a/components/StreakCelebrationModal.tsx b/components/StreakCelebrationModal.tsx index 6940489..0a5cec4 100644 --- a/components/StreakCelebrationModal.tsx +++ b/components/StreakCelebrationModal.tsx @@ -1,3 +1,4 @@ +import { createT } from "@/i18n"; import { useEffect, useRef } from "react"; import { View, @@ -12,6 +13,8 @@ import Svg, { Circle, Defs, Filter, FeGaussianBlur } from "react-native-svg"; import { Flame } from "lucide-react-native"; import { useTheme } from "@/lib/theme/context"; +const t = createT("streak"); + const { width: W, height: H } = Dimensions.get("window"); const PARTICLE_COUNT = 24; @@ -206,19 +209,16 @@ export default function StreakCelebrationScreen({ streak, onDismiss }: Props) { const message = streak >= 30 - ? "Legendary!" + ? t("messages.legendary") : streak >= 14 - ? "Unstoppable!" + ? t("messages.unstoppable") : streak >= 7 - ? "One week strong!" + ? t("messages.oneWeekStrong") : streak >= 3 - ? "Keep it going!" - : "Great start!"; + ? t("messages.keepItGoing") + : t("messages.greatStart"); - const sub = - streak >= 7 - ? "You're building something real. See you tomorrow." - : "Come back tomorrow to keep your streak alive."; + const sub = streak >= 7 ? t("sub.buildingReal") : t("sub.comeTomorrow"); return ( {streak} - day streak + + {t("dayStreak")} + {/* text */} @@ -329,7 +331,7 @@ export default function StreakCelebrationScreen({ streak, onDismiss }: Props) { onPress={onDismiss} activeOpacity={0.85} > - Continue + {t("continue")} diff --git a/components/favorites/DateIdeaCard.tsx b/components/favorites/DateIdeaCard.tsx index ae6c029..a4f8163 100644 --- a/components/favorites/DateIdeaCard.tsx +++ b/components/favorites/DateIdeaCard.tsx @@ -1,9 +1,12 @@ // Made with the help of Claude.ai and ChatGPT +import { createT } from "@/i18n"; import { useTheme } from "@/lib/theme/context"; import { Ionicons } from "@expo/vector-icons"; import { Pressable, StyleSheet, Text, View } from "react-native"; +const t = createT("favorites.ideaDetail"); + type Props = { title: string; location: string; @@ -33,7 +36,7 @@ export function DateIdeaCard({ {isTopPick && ( - TOP PICK + {t("topPick")} )} @@ -46,22 +49,22 @@ export function DateIdeaCard({ - EFFORT + {t("effort")} {effort} - TIME + {t("time")} {duration} - VIBE + {t("vibe")} {vibe} - Explore Details β†’ + {t("exploreDetails")} ); diff --git a/lib/web/browserConfig.tsx b/lib/web/browserConfig.tsx new file mode 100644 index 0000000..16a83d2 --- /dev/null +++ b/lib/web/browserConfig.tsx @@ -0,0 +1,11 @@ +import * as WebBrowser from "expo-web-browser"; +import { Theme } from "@/lib/theme/theme"; + +export const BROWSER_OPTS = ( + theme: Theme, +): Parameters[1] => ({ + presentationStyle: WebBrowser.WebBrowserPresentationStyle.FORM_SHEET, + controlsColor: theme.primary, + toolbarColor: theme.background, + enableBarCollapsing: true, +}); diff --git a/locales/en.json b/locales/en.json index 6168e15..1b5f3aa 100644 --- a/locales/en.json +++ b/locales/en.json @@ -16,6 +16,7 @@ "termsOfService": "Terms of Service", "and": "and", "privacyPolicy": "Privacy Policy", + "continuingWith": "By continuing, you agree to our", "withEmail": { "title": "Login with Email", "body": "Please enter your", @@ -160,7 +161,8 @@ } }, "home": { - "start": "START" + "start": "START", + "noMoreCalls": "You have no more calls today. Upgrade your plan for more!" }, "settings": { "title": "Settings", @@ -175,7 +177,12 @@ "appearance": "Appearance", "logOut": "Log Out", "confirmLogOut": "Are you sure you want to log out?", - "cancel": "Cancel" + "cancel": "Cancel", + "security": "Security", + "app": "App", + "legal": "Legal", + "info": "Info", + "version": "Version" }, "forgotPassword": { "title": "Forgot Password", @@ -211,6 +218,285 @@ } } }, + "settings": { + "interests": { + "title": "Select your Interests", + "body": "Pick between {{min}} and {{max}} interests to match with users who have similar things in common", + "continue": "Save", + "errorTitle": "Selection limit", + "ok": "OK", + "errorBody": "Please choose minimum {{min}} and maximum {{max}} interests.", + "fallbacks": { + "failToLoad": "Failed to load interests" + } + }, + "appearance": { + "theme": "Theme", + "light": "Light", + "dark": "Dark", + "systemTheme": "System Theme" + }, + "personalDetails": { + "title": "Edit Profile", + "fullName": "Full Name", + "fullNamePlaceholder": "Your Full Name", + "emailAddress": "Email Address", + "emailPlaceholder": "Your Email", + "phoneNumber": "Phone Number", + "phonePlaceholder": "Your Phone Number", + "dateOfBirth": "Date of Birth", + "country": "Country", + "bio": "Bio", + "bioPlaceholder": "Show people what you like - maybe your perfect video date, your vibe, or what makes you laugh.", + "gallery": "Gallery", + "edit": "Edit", + "done": "Done", + "add": "Add", + "saveAndExit": "Save and Exit" + }, + "security": { + "title": "Security", + "subtitle": "Change your password below.", + "currentPassword": "Current Password", + "currentPasswordPlaceholder": "Current password", + "newPassword": "New Password", + "newPasswordPlaceholder": "New password", + "confirmNewPassword": "Confirm New Password", + "confirmPasswordPlaceholder": "Repeat new password", + "savePassword": "Save Password", + "error": "Error", + "errors": { + "fillAllFields": "Please fill in all fields.", + "passwordsNoMatch": "New passwords do not match.", + "passwordTooShort": "Password must be at least 8 characters.", + "somethingWentWrong": "Something went wrong." + }, + "success": "Success", + "successMessage": "Password changed successfully.", + "ok": "OK" + } + }, + "chat": { + "search": "Search", + "voiceMessage": "🎀 Voice message", + "encryptedMessage": "[Encrypted message]", + "unableToDecrypt": "[Unable to decrypt]", + "typeMessage": "Type a message...", + "noMessagesYet": "No messages yet", + "startConversation": "Start the conversation with {{name}}. A simple hello can turn into something special.", + "sayHi": "Say hi", + "report": "🚩 Report {{name}}", + "cancel": "Cancel", + "empty": { + "noChats": "No Chats Yet", + "noChatsFound": "No Chats Found", + "noChatsSubtitle": "Your matches and messages will appear here. Start a new connection and get the first conversation going.", + "noChatsFoundSubtitle": "We couldn't find any conversation matching your search. Try a different name.", + "resetSearch": "Reset Search", + "discover": "Discover New People" + } + }, + "favorites": { + "exploreCategories": "Explore Categories", + "viewAll": "View All", + "winningTips": "Winning Tips", + "categories": "Categories", + "ideaDetail": { + "topPick": "TOP PICK", + "effort": "EFFORT", + "duration": "DURATION", + "time": "TIME", + "vibe": "VIBE", + "exploreDetails": "Explore Details β†’", + "whyThisWorks": "Why this works", + "keepInMind": "What to keep in mind", + "whyBody": "A {{vibe}} date like this keeps things fresh and gives you both something to talk about. The {{effort}} effort level means it's easy to plan without pressure.", + "keepInMindBody": "Budget around ${{price}} and block out {{duration}}. Book in advance if the venue is popular β€” especially on weekends." + }, + "tipDetail": { + "tipNotFound": "Tip not found.", + "continueReading": "Continue reading", + "unlockWithPremium": "Unlock the full guide with Premium", + "premium": "PREMIUM" + } + }, + "videocall": { + "setup": { + "title": "Ready to connect?", + "subtitle": "Set up your devices and pick your interests", + "camera": "Camera", + "microphone": "Microphone", + "interests": "Interests", + "interestsMatchHint": "Β· match with people like you", + "frontCamera": "Front camera", + "backCamera": "Back camera", + "builtInMic": "Built-in microphone", + "headsetMic": "Headset microphone", + "findMatch": "Find a match", + "devTestStreak": "[DEV] Test streak screen" + }, + "connecting": { + "matchFound": "Match found! Connecting…", + "looking": "Looking for someone…", + "connecting": "Connecting to matchmaking…", + "hint": "This usually takes a few seconds", + "cancel": "Cancel" + }, + "call": { + "waitingForPartner": "Waiting for partner..." + }, + "icebreakers": { + "0": "What's the weirdest thing you've ever eaten?", + "1": "If you could live in any movie universe, which would you pick?", + "2": "What's a skill you're secretly proud of?", + "3": "What's the most spontaneous thing you've ever done?", + "4": "If you had to eat one meal forever, what would it be?", + "5": "What's your most controversial food opinion?", + "6": "Would you rather explore space or the deep ocean?", + "7": "What's the best trip you've ever been on?", + "8": "What did you want to be as a kid?", + "9": "What's your go-to karaoke song?", + "10": "What's a hobby you've always wanted to try?", + "11": "What's the last thing that made you laugh out loud?" + } + }, + "report": { + "title": "Report", + "youAreReporting": "You are reporting", + "whatIsTheIssue": "What is the issue?", + "addDetail": "Add more detail (optional)", + "detailPlaceholder": "Describe what happened…", + "anonymous": "Reports are anonymous. Our team reviews every report within 24 hours.", + "submitReport": "Submit report", + "submitAndBlock": "Submit & Block", + "missingReason": "Missing reason", + "missingReasonBody": "Please select or describe the reason for your report.", + "reportedAndBlocked": "Reported & Blocked", + "reportSubmitted": "Report Submitted", + "reportedAndBlockedBody": "{{name}} has been reported and blocked.", + "reportSubmittedBody": "Your report has been submitted. We'll review it shortly.", + "ok": "OK", + "reasons": { + "inappropriateBehaviour": "Inappropriate behaviour", + "explicitContent": "Explicit content", + "harassmentOrBullying": "Harassment or bullying", + "hateSpeech": "Hate speech", + "illegalActivities": "Illegal activities", + "spamOrScam": "Spam or scam", + "underageUser": "Underage user", + "impersonation": "Impersonation" + } + }, + "subscriptions": { + "title": "Choose your plan", + "subtitle": "Upgrade or downgrade at any time.", + "monthly": "Monthly", + "yearly": "Yearly", + "cancelAnytime": "Cancel anytime Β· No hidden fees", + "active": "βœ“ ACTIVE", + "subscribe": "Subscribe", + "activeStatus": "Active", + "perMonth": "/mo", + "perAnnually": "/annually", + "checkout": { + "title": "Checkout", + "subtitle": "Review your order before confirming", + "orderSummary": "Order summary", + "whatsIncluded": "What's included", + "totalToday": "Total today", + "billedAnnually": "{{perMonth}}/mo Β· billed annually", + "cancelPlan": "Cancel Plan", + "confirmAndSubscribe": "Confirm & Subscribe", + "save17": "Save 17% vs monthly", + "yearly": "Yearly", + "monthly": "Monthly", + "legalNote": "By subscribing you agree to our", + "termsAndConditions": "Terms and Conditions", + "and": "and", + "privacyPolicy": "Privacy Policy", + "activePlanInfo": "You currently have an active {{planId}} plan. Cancel it first to switch.", + "purchaseError": "Something went wrong during the purchase. Please try again.", + "entitlementError": "Purchase completed but entitlement was not activated. Please contact support.", + "cancelError": "Could not open cancellation. Go to Settings β†’ Apple ID β†’ Subscriptions.", + "trust": { + "securePayment": "Secure payment", + "cancelAnytime": "Cancel anytime", + "noHiddenFees": "No hidden fees" + } + } + }, + "stats": { + "premium": "PREMIUM", + "findYourMatch": "Find your perfect match", + "exclusiveInsights": "Exclusive insights Β· Unlimited features", + "upgrade": "Upgrade", + "mutualInterests": "Mutual Interests", + "matchScore": "MATCH SCORE", + "averageWaitingTime": "Average waiting time", + "seconds": "SECONDS", + "dayStreak": "day streak", + "keepItUp": "Keep it up, you're on a roll!", + "bestTimeOnline": "Best time to be online", + "activityPeaks": "Activity peaks between 8 PM and 10 PM in your area.", + "topMatchInterests": "Top Match Interests" + }, + "onboarding": { + "page0": { + "title": "Find your\nperson.", + "subtitle": "Dating, reimagined. Connect with people who truly match you.", + "cta": "Get Started" + }, + "page1": { + "notificationTitle": "You have a new match", + "notificationSubtitle": "Someone liked your profile β€” say hi!", + "title": "Never miss a\nconnection.", + "subtitle": "We'll notify you when you get a new match or message β€” so you never leave someone waiting.", + "enable": "Enable Notifications", + "notNow": "Not now" + }, + "page2": { + "realConnections": "Real connections", + "noGhosting": "No ghosting", + "title": "Modern dating\nis broken.", + "subtitle": "Endless swiping, shallow profiles, ghosting β€” you deserve better. Elysio is built around real conversations and genuine matches.", + "cta": "Let's fix that", + "back": "Back" + } + }, + "datePicker": { + "title": "Date of Birth", + "save": "Save" + }, + "update": { + "title": "Update in progress", + "body": "We're updating Elysio to bring you new features and improvements. The app will be back shortly.", + "estimatedTime": "Estimated time", + "unknown": "Unknown" + }, + "streak": { + "dayStreak": "day streak", + "continue": "Continue", + "messages": { + "legendary": "Legendary!", + "unstoppable": "Unstoppable!", + "oneWeekStrong": "One week strong!", + "keepItGoing": "Keep it going!", + "greatStart": "Great start!" + }, + "sub": { + "buildingReal": "You're building something real. See you tomorrow.", + "comeTomorrow": "Come back tomorrow to keep your streak alive." + } + }, + "error": { + "title": "Something went wrong", + "body": "An unexpected error occurred. Try going back or tap retry to try again.", + "tryAgain": "Try again", + "goBack": "Go back" + }, + "serverStatus": { + "recheckingIn": "Rechecking in {{seconds}}s" + }, "outage": { "title": "We're experiencing issues", "body": "Our team is on it and working as fast as possible to get everything back up. We apologize for the inconvenience.",