🌐 feat: add i18n support to 6 web apps

Add internationalization (DE + EN) to previously missing apps:
- todo: task management translations
- skilltree: skill/XP system translations
- nutriphi: nutrition tracking translations
- planta: plant care translations
- questions: research app translations
- matrix: chat client translations (layout integration)

Each app includes:
- svelte-i18n setup with SSR support
- localStorage persistence ({app}_locale pattern)
- i18n loading state in +layout.svelte
- German (default) and English translations

Updated CONSISTENCY_REPORT.md to mark i18n task as complete.

Also includes:
- mana-tts service placeholder files
This commit is contained in:
Till-JS 2026-01-29 14:47:58 +01:00
parent a938ed86d4
commit 5a0815708c
35 changed files with 3440 additions and 56 deletions

View file

@ -0,0 +1,49 @@
import { browser } from '$app/environment';
import { init, register, locale, waitLocale } from 'svelte-i18n';
// List of supported locales
export const supportedLocales = ['de', 'en'] as const;
export type SupportedLocale = (typeof supportedLocales)[number];
// Default locale
const defaultLocale = 'de';
// Register all available locales
register('de', () => import('./locales/de.json'));
register('en', () => import('./locales/en.json'));
// Get initial locale from browser or localStorage
function getInitialLocale(): SupportedLocale {
if (browser) {
// Check localStorage first
const stored = localStorage.getItem('nutriphi_locale');
if (stored && supportedLocales.includes(stored as SupportedLocale)) {
return stored as SupportedLocale;
}
// Fall back to browser language
const browserLang = navigator.language.split('-')[0];
if (supportedLocales.includes(browserLang as SupportedLocale)) {
return browserLang as SupportedLocale;
}
}
return defaultLocale;
}
// Initialize i18n at module scope (required for SSR)
init({
fallbackLocale: defaultLocale,
initialLocale: getInitialLocale(),
});
// Set locale and persist to localStorage
export function setLocale(newLocale: SupportedLocale) {
locale.set(newLocale);
if (browser) {
localStorage.setItem('nutriphi_locale', newLocale);
}
}
// Wait for locale to be loaded (useful for SSR)
export { waitLocale };

View file

@ -0,0 +1,89 @@
{
"app": {
"name": "NutriPhi",
"loading": "Laden...",
"tagline": "Ernährung verstehen"
},
"nav": {
"dashboard": "Dashboard",
"meals": "Mahlzeiten",
"goals": "Ziele",
"favorites": "Favoriten",
"stats": "Statistiken",
"settings": "Einstellungen"
},
"meal": {
"add": "Mahlzeit hinzufügen",
"edit": "Mahlzeit bearbeiten",
"delete": "Mahlzeit löschen",
"photo": "Foto aufnehmen",
"text": "Beschreiben",
"analyzing": "Analysiere...",
"noMeals": "Noch keine Mahlzeiten",
"breakfast": "Frühstück",
"lunch": "Mittagessen",
"dinner": "Abendessen",
"snack": "Snack"
},
"nutrition": {
"calories": "Kalorien",
"protein": "Protein",
"carbs": "Kohlenhydrate",
"fat": "Fett",
"fiber": "Ballaststoffe",
"sugar": "Zucker",
"kcal": "kcal",
"grams": "g"
},
"goals": {
"daily": "Tagesziele",
"setGoals": "Ziele setzen",
"calories": "Kalorien-Ziel",
"protein": "Protein-Ziel",
"carbs": "Kohlenhydrate-Ziel",
"fat": "Fett-Ziel",
"progress": "Fortschritt"
},
"stats": {
"today": "Heute",
"week": "Diese Woche",
"remaining": "Verbleibend",
"consumed": "Verzehrt",
"average": "Durchschnitt"
},
"favorites": {
"add": "Zu Favoriten",
"remove": "Aus Favoriten entfernen",
"noFavorites": "Keine Favoriten",
"useAgain": "Erneut verwenden"
},
"auth": {
"login": "Anmelden",
"logout": "Abmelden",
"register": "Registrieren"
},
"common": {
"save": "Speichern",
"cancel": "Abbrechen",
"delete": "Löschen",
"edit": "Bearbeiten",
"add": "Hinzufügen",
"close": "Schließen",
"search": "Suchen",
"error": "Fehler",
"success": "Erfolgreich",
"loading": "Laden..."
},
"errors": {
"loadMeals": "Mahlzeiten konnten nicht geladen werden",
"analyzeFailed": "Analyse fehlgeschlagen",
"saveFailed": "Speichern fehlgeschlagen",
"loadGoals": "Ziele konnten nicht geladen werden"
},
"success": {
"mealAdded": "Mahlzeit hinzugefügt",
"mealDeleted": "Mahlzeit gelöscht",
"goalsSaved": "Ziele gespeichert",
"favoriteAdded": "Zu Favoriten hinzugefügt"
}
}

View file

@ -0,0 +1,89 @@
{
"app": {
"name": "NutriPhi",
"loading": "Loading...",
"tagline": "Understand nutrition"
},
"nav": {
"dashboard": "Dashboard",
"meals": "Meals",
"goals": "Goals",
"favorites": "Favorites",
"stats": "Statistics",
"settings": "Settings"
},
"meal": {
"add": "Add meal",
"edit": "Edit meal",
"delete": "Delete meal",
"photo": "Take photo",
"text": "Describe",
"analyzing": "Analyzing...",
"noMeals": "No meals yet",
"breakfast": "Breakfast",
"lunch": "Lunch",
"dinner": "Dinner",
"snack": "Snack"
},
"nutrition": {
"calories": "Calories",
"protein": "Protein",
"carbs": "Carbohydrates",
"fat": "Fat",
"fiber": "Fiber",
"sugar": "Sugar",
"kcal": "kcal",
"grams": "g"
},
"goals": {
"daily": "Daily goals",
"setGoals": "Set goals",
"calories": "Calorie goal",
"protein": "Protein goal",
"carbs": "Carbohydrate goal",
"fat": "Fat goal",
"progress": "Progress"
},
"stats": {
"today": "Today",
"week": "This week",
"remaining": "Remaining",
"consumed": "Consumed",
"average": "Average"
},
"favorites": {
"add": "Add to favorites",
"remove": "Remove from favorites",
"noFavorites": "No favorites",
"useAgain": "Use again"
},
"auth": {
"login": "Login",
"logout": "Logout",
"register": "Register"
},
"common": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit",
"add": "Add",
"close": "Close",
"search": "Search",
"error": "Error",
"success": "Success",
"loading": "Loading..."
},
"errors": {
"loadMeals": "Failed to load meals",
"analyzeFailed": "Analysis failed",
"saveFailed": "Failed to save",
"loadGoals": "Failed to load goals"
},
"success": {
"mealAdded": "Meal added",
"mealDeleted": "Meal deleted",
"goalsSaved": "Goals saved",
"favoriteAdded": "Added to favorites"
}
}

View file

@ -1,5 +1,7 @@
<script lang="ts">
import '../app.css';
import '$lib/i18n';
import { isLoading as i18nLoading, _ as t } from 'svelte-i18n';
import { authStore } from '$lib/stores/auth.svelte';
let { children } = $props();
@ -11,7 +13,13 @@
</script>
<svelte:head>
<title>NutriPhi - Ernährung verstehen</title>
<title>{$t('app.name')} - {$t('app.tagline')}</title>
</svelte:head>
{@render children()}
{#if $i18nLoading}
<div class="flex min-h-screen items-center justify-center">
<p>{$t('app.loading')}</p>
</div>
{:else}
{@render children()}
{/if}