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 { useState } from "react";
import { View, Text, TextInput, Pressable, ScrollView, Alert } from "react-native";
import { useTranslation } from "react-i18next";
import { useNavigation } from "@react-navigation/native";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
import type { RootStackParamList } from "@navigation/types";
@@ -10,6 +11,8 @@ import type { StrokeType } from "@types/workout";
const STROKES: StrokeType[] = ["Freestyle", "Backstroke", "Breaststroke", "Butterfly", "Mixed"];
export function BuilderScreen() {
const { t } = useTranslation("workout");
const { t: tc } = useTranslation("common");
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const { poolLength: defaultPoolLength } = useSettings();
const [poolLength, setPoolLength] = useState(defaultPoolLength);
@@ -31,31 +34,31 @@ export function BuilderScreen() {
navigation.navigate("PreSwim", { workoutId });
} catch (e) {
console.error("Failed to save workout:", e);
Alert.alert("Error", `Failed to save workout: ${e}`);
Alert.alert(tc("validation.error"), tc("validation.failedToSave", { error: String(e) }));
}
};
return (
<ScrollView className="flex-1 bg-white">
<View className="p-4">
<Text className="text-2xl font-bold text-slate-900 mb-1">Build Workout</Text>
<Text className="text-2xl font-bold text-slate-900 mb-1">{t("buildWorkout")}</Text>
<Text className="text-slate-500 mb-4">
{totalDistance}m total · {poolLength}m pool
{totalDistance}m total · {tc("pool", { length: poolLength })}
</Text>
<View className="mb-4">
<Text className="text-sm font-medium text-slate-500 mb-1">Workout Name</Text>
<Text className="text-sm font-medium text-slate-500 mb-1">{t("workoutName")}</Text>
<TextInput
value={workoutName}
onChangeText={setWorkoutName}
placeholder="Morning Swim"
placeholder={t("placeholder.workoutName")}
placeholderTextColor="#94a3b8"
className="bg-slate-50 border border-slate-200 rounded-xl px-4 py-3 text-slate-900"
/>
</View>
<View className="mb-4">
<Text className="text-sm font-medium text-slate-500 mb-1">Pool Length</Text>
<Text className="text-sm font-medium text-slate-500 mb-1">{t("poolLength")}</Text>
<View className="flex-row gap-2">
{[25, 50].map((length) => (
<Pressable
@@ -80,16 +83,16 @@ export function BuilderScreen() {
{sets.map((set, index) => (
<View key={index} className="bg-slate-50 rounded-2xl p-4 mb-3 border border-slate-200">
<View className="flex-row justify-between items-center mb-3">
<Text className="font-semibold text-slate-700">Set {index + 1}</Text>
<Text className="font-semibold text-slate-700">{t("setNumber", { number: index + 1 })}</Text>
{sets.length > 1 && (
<Pressable onPress={() => removeSet(index)}>
<Text className="text-red-500 text-sm font-medium">Remove</Text>
<Text className="text-red-500 text-sm font-medium">{t("remove")}</Text>
</Pressable>
)}
</View>
<Text className="text-xs font-medium text-slate-500 mb-1.5 uppercase tracking-wide">
Stroke
{t("stroke")}
</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false} className="mb-3">
{STROKES.map((stroke) => (
@@ -105,7 +108,7 @@ export function BuilderScreen() {
set.stroke === stroke ? "text-white" : "text-slate-600"
}`}
>
{stroke}
{tc(`stroke.${stroke.toLowerCase()}`)}
</Text>
</Pressable>
))}
@@ -113,29 +116,29 @@ export function BuilderScreen() {
<View className="flex-row gap-2">
<View className="flex-1">
<Text className="text-xs font-medium text-slate-500 mb-1">Distance (m)</Text>
<Text className="text-xs font-medium text-slate-500 mb-1">{t("distanceM")}</Text>
<TextInput
keyboardType="numeric"
value={String(set.distancePerRep)}
onChangeText={(t) => updateSet(index, { distancePerRep: Number(t) || 0 })}
onChangeText={(val) => updateSet(index, { distancePerRep: Number(val) || 0 })}
className="bg-white border border-slate-300 rounded-lg px-3 py-2 text-slate-900"
/>
</View>
<View className="flex-1">
<Text className="text-xs font-medium text-slate-500 mb-1">Reps</Text>
<Text className="text-xs font-medium text-slate-500 mb-1">{t("reps")}</Text>
<TextInput
keyboardType="numeric"
value={String(set.repCount)}
onChangeText={(t) => updateSet(index, { repCount: Number(t) || 1 })}
onChangeText={(val) => updateSet(index, { repCount: Number(val) || 1 })}
className="bg-white border border-slate-300 rounded-lg px-3 py-2 text-slate-900"
/>
</View>
<View className="flex-1">
<Text className="text-xs font-medium text-slate-500 mb-1">Rest (s)</Text>
<Text className="text-xs font-medium text-slate-500 mb-1">{t("restS")}</Text>
<TextInput
keyboardType="numeric"
value={String(set.restDuration)}
onChangeText={(t) => updateSet(index, { restDuration: Number(t) || 0 })}
onChangeText={(val) => updateSet(index, { restDuration: Number(val) || 0 })}
className="bg-white border border-slate-300 rounded-lg px-3 py-2 text-slate-900"
/>
</View>
@@ -147,7 +150,7 @@ export function BuilderScreen() {
onPress={addSet}
className="bg-slate-100 rounded-2xl py-3 items-center mb-6 border border-slate-200"
>
<Text className="text-sky-600 font-semibold">+ Add Set</Text>
<Text className="text-sky-600 font-semibold">{t("addSet")}</Text>
</Pressable>
<Pressable
@@ -156,7 +159,7 @@ export function BuilderScreen() {
className={`rounded-2xl py-4 items-center mb-8 ${isSaving ? "bg-sky-300" : "bg-sky-500"}`}
>
<Text className="text-white font-bold text-lg">
{isSaving ? "Saving..." : "Start Swim"}
{isSaving ? tc("saving") : t("startSwim")}
</Text>
</Pressable>
</View>