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")}
+
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.",