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
+2 -2
View File
@@ -49,7 +49,7 @@ struct WorkoutListView: View {
} label: {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(workout.name ?? "Workout")
Text(workout.name ?? String(localized: "Workout"))
.font(.headline)
.foregroundColor(.primary)
Text("\(workout.sets.reduce(0) { $0 + $1.distancePerRep * $1.repCount })m · \(workout.sets.count) sets")
@@ -73,7 +73,7 @@ struct WorkoutListView: View {
ForEach(workoutManager.completedWorkouts.prefix(3)) { workout in
HStack {
VStack(alignment: .leading, spacing: 2) {
Text(workout.name ?? "Workout")
Text(workout.name ?? String(localized: "Workout"))
.font(.subheadline)
.foregroundColor(.primary)
Text(workout.completedAt ?? "")
+215
View File
@@ -0,0 +1,215 @@
{
"sourceLanguage": "en",
"strings": {
"No workouts yet": {
"extractionState": "manual",
"localizations": {
"en": { "stringUnit": { "state": "translated", "value": "No workouts yet" } },
"nl": { "stringUnit": { "state": "translated", "value": "Nog geen workouts" } },
"de": { "stringUnit": { "state": "translated", "value": "Noch keine Workouts" } },
"fr": { "stringUnit": { "state": "translated", "value": "Aucun entraînement pour le moment" } },
"it": { "stringUnit": { "state": "translated", "value": "Nessun allenamento ancora" } },
"es": { "stringUnit": { "state": "translated", "value": "Aún no hay entrenamientos" } },
"pl": { "stringUnit": { "state": "translated", "value": "Brak treningów" } },
"pt": { "stringUnit": { "state": "translated", "value": "Ainda não há treinos" } },
"dk": { "stringUnit": { "state": "translated", "value": "Ingen træninger endnu" } }
}
},
"Create a workout on your iPhone to get started": {
"extractionState": "manual",
"localizations": {
"en": { "stringUnit": { "state": "translated", "value": "Create a workout on your iPhone to get started" } },
"nl": { "stringUnit": { "state": "translated", "value": "Maak een workout op je iPhone om te beginnen" } },
"de": { "stringUnit": { "state": "translated", "value": "Erstelle ein Workout auf deinem iPhone um zu beginnen" } },
"fr": { "stringUnit": { "state": "translated", "value": "Créez un entraînement sur votre iPhone pour commencer" } },
"it": { "stringUnit": { "state": "translated", "value": "Crea un allenamento sul tuo iPhone per iniziare" } },
"es": { "stringUnit": { "state": "translated", "value": "Crea un entrenamiento en tu iPhone para empezar" } },
"pl": { "stringUnit": { "state": "translated", "value": "Stwórz trening na swoim iPhone, aby rozpocząć" } },
"pt": { "stringUnit": { "state": "translated", "value": "Crie um treino no seu iPhone para começar" } },
"dk": { "stringUnit": { "state": "translated", "value": "Opret en træning på din iPhone for at komme i gang" } }
}
},
"Saved Workouts": {
"extractionState": "manual",
"localizations": {
"en": { "stringUnit": { "state": "translated", "value": "Saved Workouts" } },
"nl": { "stringUnit": { "state": "translated", "value": "Opgeslagen workouts" } },
"de": { "stringUnit": { "state": "translated", "value": "Gespeicherte Workouts" } },
"fr": { "stringUnit": { "state": "translated", "value": "Entraînements enregistrés" } },
"it": { "stringUnit": { "state": "translated", "value": "Allenamenti salvati" } },
"es": { "stringUnit": { "state": "translated", "value": "Entrenamientos guardados" } },
"pl": { "stringUnit": { "state": "translated", "value": "Zapisane treningi" } },
"pt": { "stringUnit": { "state": "translated", "value": "Treinos guardados" } },
"dk": { "stringUnit": { "state": "translated", "value": "Gemte træninger" } }
}
},
"Workout": {
"extractionState": "manual",
"localizations": {
"en": { "stringUnit": { "state": "translated", "value": "Workout" } },
"nl": { "stringUnit": { "state": "translated", "value": "Workout" } },
"de": { "stringUnit": { "state": "translated", "value": "Workout" } },
"fr": { "stringUnit": { "state": "translated", "value": "Entraînement" } },
"it": { "stringUnit": { "state": "translated", "value": "Allenamento" } },
"es": { "stringUnit": { "state": "translated", "value": "Entrenamiento" } },
"pl": { "stringUnit": { "state": "translated", "value": "Trening" } },
"pt": { "stringUnit": { "state": "translated", "value": "Treino" } },
"dk": { "stringUnit": { "state": "translated", "value": "Træning" } }
}
},
"Recent": {
"extractionState": "manual",
"localizations": {
"en": { "stringUnit": { "state": "translated", "value": "Recent" } },
"nl": { "stringUnit": { "state": "translated", "value": "Recent" } },
"de": { "stringUnit": { "state": "translated", "value": "Zuletzt" } },
"fr": { "stringUnit": { "state": "translated", "value": "Récent" } },
"it": { "stringUnit": { "state": "translated", "value": "Recenti" } },
"es": { "stringUnit": { "state": "translated", "value": "Reciente" } },
"pl": { "stringUnit": { "state": "translated", "value": "Ostatnie" } },
"pt": { "stringUnit": { "state": "translated", "value": "Recente" } },
"dk": { "stringUnit": { "state": "translated", "value": "Seneste" } }
}
},
"Sets": {
"extractionState": "manual",
"localizations": {
"en": { "stringUnit": { "state": "translated", "value": "Sets" } },
"nl": { "stringUnit": { "state": "translated", "value": "Sets" } },
"de": { "stringUnit": { "state": "translated", "value": "Sets" } },
"fr": { "stringUnit": { "state": "translated", "value": "Séries" } },
"it": { "stringUnit": { "state": "translated", "value": "Serie" } },
"es": { "stringUnit": { "state": "translated", "value": "Series" } },
"pl": { "stringUnit": { "state": "translated", "value": "Serie" } },
"pt": { "stringUnit": { "state": "translated", "value": "Séries" } },
"dk": { "stringUnit": { "state": "translated", "value": "Sæt" } }
}
},
"Start": {
"extractionState": "manual",
"localizations": {
"en": { "stringUnit": { "state": "translated", "value": "Start" } },
"nl": { "stringUnit": { "state": "translated", "value": "Start" } },
"de": { "stringUnit": { "state": "translated", "value": "Starten" } },
"fr": { "stringUnit": { "state": "translated", "value": "Commencer" } },
"it": { "stringUnit": { "state": "translated", "value": "Inizia" } },
"es": { "stringUnit": { "state": "translated", "value": "Iniciar" } },
"pl": { "stringUnit": { "state": "translated", "value": "Rozpocznij" } },
"pt": { "stringUnit": { "state": "translated", "value": "Iniciar" } },
"dk": { "stringUnit": { "state": "translated", "value": "Start" } }
}
},
"Back": {
"extractionState": "manual",
"localizations": {
"en": { "stringUnit": { "state": "translated", "value": "Back" } },
"nl": { "stringUnit": { "state": "translated", "value": "Terug" } },
"de": { "stringUnit": { "state": "translated", "value": "Zurück" } },
"fr": { "stringUnit": { "state": "translated", "value": "Retour" } },
"it": { "stringUnit": { "state": "translated", "value": "Indietro" } },
"es": { "stringUnit": { "state": "translated", "value": "Volver" } },
"pl": { "stringUnit": { "state": "translated", "value": "Wstecz" } },
"pt": { "stringUnit": { "state": "translated", "value": "Voltar" } },
"dk": { "stringUnit": { "state": "translated", "value": "Tilbage" } }
}
},
"Complete Lap": {
"extractionState": "manual",
"localizations": {
"en": { "stringUnit": { "state": "translated", "value": "Complete Lap" } },
"nl": { "stringUnit": { "state": "translated", "value": "Baan voltooien" } },
"de": { "stringUnit": { "state": "translated", "value": "Bahn abschließen" } },
"fr": { "stringUnit": { "state": "translated", "value": "Terminer la longueur" } },
"it": { "stringUnit": { "state": "translated", "value": "Completa lunghezza" } },
"es": { "stringUnit": { "state": "translated", "value": "Completar largo" } },
"pl": { "stringUnit": { "state": "translated", "value": "Ukończ długość" } },
"pt": { "stringUnit": { "state": "translated", "value": "Completar comprimento" } },
"dk": { "stringUnit": { "state": "translated", "value": "Fuldfør bane" } }
}
},
"End": {
"extractionState": "manual",
"localizations": {
"en": { "stringUnit": { "state": "translated", "value": "End" } },
"nl": { "stringUnit": { "state": "translated", "value": "Einde" } },
"de": { "stringUnit": { "state": "translated", "value": "Beenden" } },
"fr": { "stringUnit": { "state": "translated", "value": "Terminer" } },
"it": { "stringUnit": { "state": "translated", "value": "Fine" } },
"es": { "stringUnit": { "state": "translated", "value": "Finalizar" } },
"pl": { "stringUnit": { "state": "translated", "value": "Koniec" } },
"pt": { "stringUnit": { "state": "translated", "value": "Fim" } },
"dk": { "stringUnit": { "state": "translated", "value": "Slut" } }
}
},
"Rest": {
"extractionState": "manual",
"localizations": {
"en": { "stringUnit": { "state": "translated", "value": "Rest" } },
"nl": { "stringUnit": { "state": "translated", "value": "Rust" } },
"de": { "stringUnit": { "state": "translated", "value": "Pause" } },
"fr": { "stringUnit": { "state": "translated", "value": "Repos" } },
"it": { "stringUnit": { "state": "translated", "value": "Recupero" } },
"es": { "stringUnit": { "state": "translated", "value": "Descanso" } },
"pl": { "stringUnit": { "state": "translated", "value": "Odpoczynek" } },
"pt": { "stringUnit": { "state": "translated", "value": "Descanso" } },
"dk": { "stringUnit": { "state": "translated", "value": "Hvile" } }
}
},
"Lap detected!": {
"extractionState": "manual",
"localizations": {
"en": { "stringUnit": { "state": "translated", "value": "Lap detected!" } },
"nl": { "stringUnit": { "state": "translated", "value": "Baan gedetecteerd!" } },
"de": { "stringUnit": { "state": "translated", "value": "Bahn erkannt!" } },
"fr": { "stringUnit": { "state": "translated", "value": "Longueur détectée !" } },
"it": { "stringUnit": { "state": "translated", "value": "Lunghezza rilevata!" } },
"es": { "stringUnit": { "state": "translated", "value": "¡Largo detectado!" } },
"pl": { "stringUnit": { "state": "translated", "value": "Wykryto długość!" } },
"pt": { "stringUnit": { "state": "translated", "value": "Comprimento detectado!" } },
"dk": { "stringUnit": { "state": "translated", "value": "Bane registreret!" } }
}
},
"m pool": {
"extractionState": "manual",
"localizations": {
"en": { "stringUnit": { "state": "translated", "value": "%@m pool" } },
"nl": { "stringUnit": { "state": "translated", "value": "%@m bad" } },
"de": { "stringUnit": { "state": "translated", "value": "%@m Becken" } },
"fr": { "stringUnit": { "state": "translated", "value": "Bassin de %@m" } },
"it": { "stringUnit": { "state": "translated", "value": "Piscina di %@m" } },
"es": { "stringUnit": { "state": "translated", "value": "Piscina de %@m" } },
"pl": { "stringUnit": { "state": "translated", "value": "Basen %@m" } },
"pt": { "stringUnit": { "state": "translated", "value": "Piscina de %@m" } },
"dk": { "stringUnit": { "state": "translated", "value": "%@m pool" } }
}
},
"m total": {
"extractionState": "manual",
"localizations": {
"en": { "stringUnit": { "state": "translated", "value": "%@m total" } },
"nl": { "stringUnit": { "state": "translated", "value": "%@m totaal" } },
"de": { "stringUnit": { "state": "translated", "value": "%@m gesamt" } },
"fr": { "stringUnit": { "state": "translated", "value": "%@m au total" } },
"it": { "stringUnit": { "state": "translated", "value": "%@m totale" } },
"es": { "stringUnit": { "state": "translated", "value": "%@m total" } },
"pl": { "stringUnit": { "state": "translated", "value": "%@m łącznie" } },
"pt": { "stringUnit": { "state": "translated", "value": "%@m total" } },
"dk": { "stringUnit": { "state": "translated", "value": "%@m i alt" } }
}
},
"kcal": {
"extractionState": "manual",
"localizations": {
"en": { "stringUnit": { "state": "translated", "value": "%@kcal" } },
"nl": { "stringUnit": { "state": "translated", "value": "%@kcal" } },
"de": { "stringUnit": { "state": "translated", "value": "%@kcal" } },
"fr": { "stringUnit": { "state": "translated", "value": "%@kcal" } },
"it": { "stringUnit": { "state": "translated", "value": "%@kcal" } },
"es": { "stringUnit": { "state": "translated", "value": "%@kcal" } },
"pl": { "stringUnit": { "state": "translated", "value": "%@kcal" } },
"pt": { "stringUnit": { "state": "translated", "value": "%@kcal" } },
"dk": { "stringUnit": { "state": "translated", "value": "%@kcal" } }
}
}
}
}
@@ -24,7 +24,6 @@ struct SwimView: View {
workoutManager.currentSetIndex < workout.sets.count {
let currentSet = workout.sets[workoutManager.currentSetIndex]
// Set/Rep info
HStack {
Text("Set \(workoutManager.currentSetIndex + 1)/\(workout.sets.count)")
.font(.caption2)
@@ -36,18 +35,15 @@ struct SwimView: View {
.foregroundColor(.secondary)
}
// Stroke name
Text(currentSet.stroke)
.font(.headline)
.foregroundColor(.cyan)
// Current set distance
Text("\(Int(workoutManager.currentSetDistanceSwum))/\(Int(workoutManager.currentSetTotalDistance))m")
.font(.system(size: 36, weight: .bold, design: .rounded))
.foregroundColor(.cyan)
}
// Total distance
HStack(spacing: 4) {
Image(systemName: "figure.pool.swim")
.font(.caption)
@@ -57,11 +53,9 @@ struct SwimView: View {
.foregroundColor(.secondary)
}
// Elapsed time
Text(formatTime(workoutManager.elapsedSeconds))
.font(.system(.title3, design: .monospaced))
// Calories + Heart rate
HStack(spacing: 12) {
HStack(spacing: 4) {
Image(systemName: "flame.fill")
@@ -82,7 +76,6 @@ struct SwimView: View {
}
}
// Lap detection indicator
if workoutManager.lapDetected {
Text("Lap detected!")
.font(.caption)
@@ -138,7 +131,6 @@ struct RestView: View {
.foregroundColor(.secondary)
}
// Show stats during rest too
HStack(spacing: 12) {
HStack(spacing: 4) {
Image(systemName: "figure.pool.swim")
+1 -1
View File
@@ -8,7 +8,7 @@ struct WorkoutDetailView: View {
if let workout = workoutManager.selectedWorkout {
Section {
VStack(spacing: 8) {
Text(workout.name ?? "Workout")
Text(workout.name ?? String(localized: "Workout"))
.font(.headline)
Text("\(Int(workout.poolLength))m pool")