feat(i18n): add internationalization with i18next and expo-localization

This commit is contained in:
2026-08-08 10:09:30 +02:00
parent 408a2fa10c
commit 9946e78858
81 changed files with 1820 additions and 125 deletions
@@ -1,5 +1,6 @@
import { useEffect, useState } from "react";
import { View, Text, Pressable, ActivityIndicator } from "react-native";
import { useTranslation } from "react-i18next";
import { useNavigation, useRoute } from "@react-navigation/native";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
import type { RootStackParamList } from "@navigation/types";
@@ -20,6 +21,8 @@ type WorkoutWithSets = {
};
export function PreSwimScreen() {
const { t } = useTranslation("active-workout");
const { t: tc } = useTranslation("common");
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const route = useRoute<RouteProp<RootStackParamList, "PreSwim">>();
const [workout, setWorkout] = useState<WorkoutWithSets | null>(null);
@@ -72,16 +75,16 @@ export function PreSwimScreen() {
<View className="flex-1 bg-white p-4 items-center justify-center">
<View className="bg-green-50 rounded-2xl p-6 items-center mb-6 w-full">
<Text className="text-4xl mb-3">⌚</Text>
<Text className="text-xl font-bold text-green-800 mb-2">Workout sent to Watch!</Text>
<Text className="text-xl font-bold text-green-800 mb-2">{t("workoutSentToWatch")}</Text>
<Text className="text-green-600 text-center">
Open the SwimBuddy app on your Apple Watch to start tracking.
{t("openWatchApp")}
</Text>
</View>
<Pressable
onPress={() => navigation.popToTop()}
className="bg-slate-900 rounded-2xl py-4 items-center w-full"
>
<Text className="text-white font-bold text-lg">Done</Text>
<Text className="text-white font-bold text-lg">{tc("done")}</Text>
</Pressable>
</View>
);
@@ -92,16 +95,16 @@ export function PreSwimScreen() {
<View className="flex-1 bg-white p-4 items-center justify-center">
<View className="bg-red-50 rounded-2xl p-6 items-center mb-6 w-full">
<Text className="text-4xl mb-3">⚠️</Text>
<Text className="text-xl font-bold text-red-800 mb-2">Failed to send</Text>
<Text className="text-xl font-bold text-red-800 mb-2">{t("failedToSend")}</Text>
<Text className="text-red-600 text-center">
Could not send workout to Watch. Starting without Watch instead.
{t("couldNotSend")}
</Text>
</View>
<Pressable
onPress={() => navigation.navigate("PostSwim", { workoutId: route.params.workoutId })}
className="bg-sky-500 rounded-2xl py-4 items-center w-full"
>
<Text className="text-white font-bold text-lg">Start Without Watch</Text>
<Text className="text-white font-bold text-lg">{t("startWithoutWatch")}</Text>
</Pressable>
</View>
);
@@ -111,16 +114,16 @@ export function PreSwimScreen() {
<View className="flex-1 bg-white p-4">
<View className="items-center mb-6">
<Text className="text-2xl font-bold text-slate-900 mb-1">
{workout.name || "Workout"}
{workout.name || tc("workout")}
</Text>
<Text className="text-slate-500">
{formatDistance(workout.totalDistance ?? 0)} · {workout.poolLength}m pool
{formatDistance(workout.totalDistance ?? 0)} · {tc("pool", { length: workout.poolLength })}
</Text>
</View>
<View className="bg-sky-50 rounded-2xl p-4 mb-4 border border-sky-100">
<Text className="text-xs font-semibold text-sky-700 mb-3 uppercase tracking-wide">
Sets
{tc("sets")}
</Text>
{workout.sets.map((set, i) => (
<View
@@ -132,7 +135,7 @@ export function PreSwimScreen() {
className="w-3 h-3 rounded-full"
style={{ backgroundColor: STROKE_COLORS[set.stroke] ?? "#94a3b8" }}
/>
<Text className="text-slate-700 font-medium">{set.stroke}</Text>
<Text className="text-slate-700 font-medium">{tc(`stroke.${set.stroke.toLowerCase()}`)}</Text>
</View>
<Text className="text-slate-600">
{set.repCount}×{set.distancePerRep}m
@@ -143,9 +146,9 @@ export function PreSwimScreen() {
<View className="bg-slate-50 rounded-2xl p-4 mb-6">
<View className="flex-row justify-between mb-2">
<Text className="text-slate-500">Rest</Text>
<Text className="text-slate-500">{t("rest")}</Text>
<Text className="text-slate-900 font-medium">
{workout.sets[0]?.restDuration ?? 0}s between sets
{t("betweenSets", { duration: workout.sets[0]?.restDuration ?? 0 })}
</Text>
</View>
</View>
@@ -158,12 +161,12 @@ export function PreSwimScreen() {
/>
<Text className="text-sm text-slate-600">
{isReady
? "Apple Watch ready"
? t("watchStatus.ready")
: isPaired
? isInstalled
? "Watch paired but not in range"
: "Watch paired, app not installed"
: "No Watch paired"}
? t("watchStatus.pairedNotInRange")
: t("watchStatus.pairedNotInstalled")
: t("watchStatus.noWatch")}
</Text>
</View>
@@ -178,15 +181,15 @@ export function PreSwimScreen() {
<ActivityIndicator color="#fff" />
) : (
<Text className="text-white font-bold text-lg">
{isReady ? "Send to Watch & Start" : "Start Without Watch"}
{isReady ? t("sendToWatchAndStart") : t("startWithoutWatch")}
</Text>
)}
</Pressable>
<Text className="text-slate-400 text-xs text-center mt-3">
{isReady
? "Workout will be synced to your Apple Watch"
: "Workout will be tracked on this device"}
? t("syncToWatch")
: t("trackedOnDevice")}
</Text>
</View>
);