🌐 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

@ -1,7 +1,9 @@
<script lang="ts">
import '../app.css';
import '$lib/i18n';
import { onMount } from 'svelte';
import type { Snippet } from 'svelte';
import { isLoading as i18nLoading, _ as t } from 'svelte-i18n';
import { theme } from '$lib/stores/theme';
import { ToastContainer } from '@manacore/shared-ui';
@ -18,7 +20,7 @@
</script>
<svelte:head>
<title>Mana Matrix</title>
<title>{$t('app.name')}</title>
<meta name="description" content="Self-hosted Matrix chat client" />
</svelte:head>

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}

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('planta_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('planta_locale', newLocale);
}
}
// Wait for locale to be loaded (useful for SSR)
export { waitLocale };

View file

@ -0,0 +1,87 @@
{
"app": {
"name": "Planta",
"loading": "Laden...",
"tagline": "Pflanzenpflege leicht gemacht"
},
"nav": {
"plants": "Pflanzen",
"watering": "Gießen",
"identify": "Identifizieren",
"settings": "Einstellungen"
},
"plant": {
"add": "Pflanze hinzufügen",
"edit": "Pflanze bearbeiten",
"delete": "Pflanze löschen",
"name": "Name",
"species": "Art",
"location": "Standort",
"noPlants": "Noch keine Pflanzen",
"addFirst": "Füge deine erste Pflanze hinzu",
"careNotes": "Pflegehinweise",
"health": "Gesundheit"
},
"health": {
"healthy": "Gesund",
"needsAttention": "Braucht Aufmerksamkeit",
"sick": "Krank"
},
"watering": {
"water": "Gießen",
"watered": "Gegossen",
"lastWatered": "Zuletzt gegossen",
"nextWatering": "Nächstes Gießen",
"daysUntil": "in {days} Tagen",
"overdue": "Überfällig",
"today": "Heute gießen",
"noWatering": "Keine Pflanzen zum Gießen"
},
"identify": {
"takePhoto": "Foto aufnehmen",
"analyzing": "Analysiere...",
"identified": "Identifiziert",
"confidence": "Sicherheit",
"tips": "Pflegetipps"
},
"light": {
"low": "Wenig Licht",
"medium": "Mittleres Licht",
"bright": "Helles Licht",
"direct": "Direktes Sonnenlicht"
},
"humidity": {
"low": "Niedrig",
"medium": "Mittel",
"high": "Hoch"
},
"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": {
"loadPlants": "Pflanzen konnten nicht geladen werden",
"identifyFailed": "Identifizierung fehlgeschlagen",
"saveFailed": "Speichern fehlgeschlagen",
"uploadFailed": "Upload fehlgeschlagen"
},
"success": {
"plantAdded": "Pflanze hinzugefügt",
"plantDeleted": "Pflanze gelöscht",
"plantWatered": "Pflanze gegossen",
"photoUploaded": "Foto hochgeladen"
}
}

View file

@ -0,0 +1,87 @@
{
"app": {
"name": "Planta",
"loading": "Loading...",
"tagline": "Plant care made easy"
},
"nav": {
"plants": "Plants",
"watering": "Watering",
"identify": "Identify",
"settings": "Settings"
},
"plant": {
"add": "Add plant",
"edit": "Edit plant",
"delete": "Delete plant",
"name": "Name",
"species": "Species",
"location": "Location",
"noPlants": "No plants yet",
"addFirst": "Add your first plant",
"careNotes": "Care notes",
"health": "Health"
},
"health": {
"healthy": "Healthy",
"needsAttention": "Needs attention",
"sick": "Sick"
},
"watering": {
"water": "Water",
"watered": "Watered",
"lastWatered": "Last watered",
"nextWatering": "Next watering",
"daysUntil": "in {days} days",
"overdue": "Overdue",
"today": "Water today",
"noWatering": "No plants to water"
},
"identify": {
"takePhoto": "Take photo",
"analyzing": "Analyzing...",
"identified": "Identified",
"confidence": "Confidence",
"tips": "Care tips"
},
"light": {
"low": "Low light",
"medium": "Medium light",
"bright": "Bright light",
"direct": "Direct sunlight"
},
"humidity": {
"low": "Low",
"medium": "Medium",
"high": "High"
},
"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": {
"loadPlants": "Failed to load plants",
"identifyFailed": "Identification failed",
"saveFailed": "Failed to save",
"uploadFailed": "Upload failed"
},
"success": {
"plantAdded": "Plant added",
"plantDeleted": "Plant deleted",
"plantWatered": "Plant watered",
"photoUploaded": "Photo uploaded"
}
}

View file

@ -1,12 +1,15 @@
<script lang="ts">
import '../app.css';
import '$lib/i18n';
import { onMount } from 'svelte';
import { isLoading as i18nLoading, _ as t } from 'svelte-i18n';
import { theme } from '$lib/stores/theme';
import { authStore } from '$lib/stores/auth.svelte';
let { children } = $props();
let loading = $state(true);
let appReady = $derived(!loading && !$i18nLoading);
onMount(async () => {
// Initialize theme
@ -19,13 +22,13 @@
});
</script>
{#if loading}
{#if !appReady}
<div class="flex min-h-screen items-center justify-center bg-background">
<div class="text-center">
<div
class="mb-4 inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent"
></div>
<p class="text-muted-foreground">Laden...</p>
<p class="text-muted-foreground">{$t('common.loading')}</p>
</div>
</div>
{:else}

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('questions_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('questions_locale', newLocale);
}
}
// Wait for locale to be loaded (useful for SSR)
export { waitLocale };

View file

@ -0,0 +1,96 @@
{
"app": {
"name": "Questions",
"loading": "Laden...",
"tagline": "KI-gestützte Recherche"
},
"nav": {
"questions": "Fragen",
"collections": "Sammlungen",
"research": "Recherche",
"settings": "Einstellungen"
},
"question": {
"create": "Frage erstellen",
"edit": "Frage bearbeiten",
"delete": "Frage löschen",
"title": "Frage",
"description": "Beschreibung",
"status": "Status",
"priority": "Priorität",
"noQuestions": "Noch keine Fragen",
"addFirst": "Stelle deine erste Frage"
},
"status": {
"open": "Offen",
"researching": "Wird recherchiert",
"answered": "Beantwortet",
"archived": "Archiviert"
},
"priority": {
"low": "Niedrig",
"medium": "Mittel",
"high": "Hoch"
},
"collection": {
"create": "Sammlung erstellen",
"edit": "Sammlung bearbeiten",
"delete": "Sammlung löschen",
"name": "Name",
"color": "Farbe",
"noCollections": "Keine Sammlungen"
},
"research": {
"start": "Recherche starten",
"inProgress": "Recherche läuft...",
"depth": "Recherchetiefe",
"quick": "Schnell",
"standard": "Standard",
"deep": "Tiefgehend",
"sources": "Quellen",
"summary": "Zusammenfassung",
"keyPoints": "Kernpunkte",
"followUp": "Weiterführende Fragen"
},
"answer": {
"create": "Antwort erstellen",
"edit": "Antwort bearbeiten",
"accept": "Antwort akzeptieren",
"rate": "Bewerten",
"noAnswer": "Noch keine Antwort"
},
"source": {
"view": "Quelle ansehen",
"extract": "Inhalt extrahieren",
"noSources": "Keine Quellen gefunden"
},
"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": {
"loadQuestions": "Fragen konnten nicht geladen werden",
"researchFailed": "Recherche fehlgeschlagen",
"saveFailed": "Speichern fehlgeschlagen",
"loadSources": "Quellen konnten nicht geladen werden"
},
"success": {
"questionCreated": "Frage erstellt",
"questionDeleted": "Frage gelöscht",
"researchStarted": "Recherche gestartet",
"answerAccepted": "Antwort akzeptiert"
}
}

View file

@ -0,0 +1,96 @@
{
"app": {
"name": "Questions",
"loading": "Loading...",
"tagline": "AI-powered research"
},
"nav": {
"questions": "Questions",
"collections": "Collections",
"research": "Research",
"settings": "Settings"
},
"question": {
"create": "Create question",
"edit": "Edit question",
"delete": "Delete question",
"title": "Question",
"description": "Description",
"status": "Status",
"priority": "Priority",
"noQuestions": "No questions yet",
"addFirst": "Ask your first question"
},
"status": {
"open": "Open",
"researching": "Researching",
"answered": "Answered",
"archived": "Archived"
},
"priority": {
"low": "Low",
"medium": "Medium",
"high": "High"
},
"collection": {
"create": "Create collection",
"edit": "Edit collection",
"delete": "Delete collection",
"name": "Name",
"color": "Color",
"noCollections": "No collections"
},
"research": {
"start": "Start research",
"inProgress": "Research in progress...",
"depth": "Research depth",
"quick": "Quick",
"standard": "Standard",
"deep": "Deep",
"sources": "Sources",
"summary": "Summary",
"keyPoints": "Key points",
"followUp": "Follow-up questions"
},
"answer": {
"create": "Create answer",
"edit": "Edit answer",
"accept": "Accept answer",
"rate": "Rate",
"noAnswer": "No answer yet"
},
"source": {
"view": "View source",
"extract": "Extract content",
"noSources": "No sources found"
},
"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": {
"loadQuestions": "Failed to load questions",
"researchFailed": "Research failed",
"saveFailed": "Failed to save",
"loadSources": "Failed to load sources"
},
"success": {
"questionCreated": "Question created",
"questionDeleted": "Question deleted",
"researchStarted": "Research started",
"answerAccepted": "Answer accepted"
}
}

View file

@ -1,6 +1,8 @@
<script lang="ts">
import '../app.css';
import '$lib/i18n';
import { onMount } from 'svelte';
import { isLoading as i18nLoading } from 'svelte-i18n';
import { theme } from '$lib/stores/theme';
import { authStore } from '$lib/stores/auth.svelte';
import { apiClient } from '$lib/api/client';
@ -9,6 +11,7 @@
let { children } = $props();
let loading = $state(true);
let appReady = $derived(!loading && !$i18nLoading);
onMount(async () => {
theme.initialize();
@ -24,7 +27,7 @@
});
</script>
{#if loading}
{#if !appReady}
<AppLoadingSkeleton />
{:else}
<div class="min-h-screen bg-background text-foreground">

View file

@ -41,6 +41,7 @@
"@manacore/shared-theme": "workspace:*",
"@manacore/shared-utils": "workspace:*",
"idb": "^8.0.0",
"svelte-i18n": "^4.0.1",
"uuid": "^11.0.0"
},
"type": "module"

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('skilltree_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('skilltree_locale', newLocale);
}
}
// Wait for locale to be loaded (useful for SSR)
export { waitLocale };

View file

@ -0,0 +1,83 @@
{
"app": {
"name": "SkillTree",
"loading": "Laden...",
"tagline": "Level Up Your Life"
},
"nav": {
"skills": "Skills",
"activities": "Aktivitäten",
"stats": "Statistiken",
"settings": "Einstellungen"
},
"skill": {
"create": "Skill erstellen",
"edit": "Skill bearbeiten",
"delete": "Skill löschen",
"name": "Name",
"description": "Beschreibung",
"branch": "Kategorie",
"level": "Level",
"xp": "XP",
"totalXp": "Gesamt-XP",
"noSkills": "Noch keine Skills",
"addFirst": "Füge deinen ersten Skill hinzu"
},
"branch": {
"intellect": "Intellekt",
"body": "Körper",
"creativity": "Kreativität",
"social": "Soziales",
"practical": "Praktisches",
"mindset": "Mindset"
},
"level": {
"unknown": "Unbekannt",
"beginner": "Anfänger",
"intermediate": "Fortgeschritten",
"competent": "Kompetent",
"expert": "Experte",
"master": "Meister"
},
"activity": {
"log": "Aktivität loggen",
"recent": "Letzte Aktivitäten",
"xpEarned": "+{xp} XP",
"noActivities": "Noch keine Aktivitäten"
},
"stats": {
"totalXp": "Gesamt-XP",
"totalSkills": "Skills",
"highestLevel": "Höchstes Level",
"streak": "Streak"
},
"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": {
"loadSkills": "Skills konnten nicht geladen werden",
"createSkill": "Skill konnte nicht erstellt werden",
"updateSkill": "Skill konnte nicht aktualisiert werden",
"deleteSkill": "Skill konnte nicht gelöscht werden"
},
"success": {
"skillCreated": "Skill erstellt",
"skillUpdated": "Skill aktualisiert",
"skillDeleted": "Skill gelöscht",
"xpAdded": "XP hinzugefügt"
}
}

View file

@ -0,0 +1,83 @@
{
"app": {
"name": "SkillTree",
"loading": "Loading...",
"tagline": "Level Up Your Life"
},
"nav": {
"skills": "Skills",
"activities": "Activities",
"stats": "Statistics",
"settings": "Settings"
},
"skill": {
"create": "Create skill",
"edit": "Edit skill",
"delete": "Delete skill",
"name": "Name",
"description": "Description",
"branch": "Category",
"level": "Level",
"xp": "XP",
"totalXp": "Total XP",
"noSkills": "No skills yet",
"addFirst": "Add your first skill"
},
"branch": {
"intellect": "Intellect",
"body": "Body",
"creativity": "Creativity",
"social": "Social",
"practical": "Practical",
"mindset": "Mindset"
},
"level": {
"unknown": "Unknown",
"beginner": "Beginner",
"intermediate": "Intermediate",
"competent": "Competent",
"expert": "Expert",
"master": "Master"
},
"activity": {
"log": "Log activity",
"recent": "Recent activities",
"xpEarned": "+{xp} XP",
"noActivities": "No activities yet"
},
"stats": {
"totalXp": "Total XP",
"totalSkills": "Skills",
"highestLevel": "Highest Level",
"streak": "Streak"
},
"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": {
"loadSkills": "Failed to load skills",
"createSkill": "Failed to create skill",
"updateSkill": "Failed to update skill",
"deleteSkill": "Failed to delete skill"
},
"success": {
"skillCreated": "Skill created",
"skillUpdated": "Skill updated",
"skillDeleted": "Skill deleted",
"xpAdded": "XP added"
}
}

View file

@ -1,12 +1,15 @@
<script lang="ts">
import '../app.css';
import '$lib/i18n';
import { onMount } from 'svelte';
import { isLoading as i18nLoading, _ as t } from 'svelte-i18n';
import { skillStore } from '$lib/stores/skills.svelte';
import { authStore } from '$lib/stores/auth.svelte';
let { children } = $props();
let loading = $state(true);
let appReady = $derived(!loading && !$i18nLoading);
onMount(async () => {
await Promise.all([authStore.initialize(), skillStore.initialize()]);
@ -15,15 +18,15 @@
</script>
<svelte:head>
<title>SkillTree - Level Up Your Life</title>
<title>{$t('app.name')} - {$t('app.tagline')}</title>
<meta name="description" content="Track your skills like a game. Level up in real life." />
</svelte:head>
{#if loading}
{#if !appReady}
<div class="flex min-h-screen items-center justify-center bg-gray-900">
<div class="text-center">
<div class="mb-4 text-6xl">🌳</div>
<div class="text-xl text-gray-300">Loading SkillTree...</div>
<div class="text-xl text-gray-300">{$t('app.loading')}</div>
</div>
</div>
{:else}

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('todo_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('todo_locale', newLocale);
}
}
// Wait for locale to be loaded (useful for SSR)
export { waitLocale };

View file

@ -0,0 +1,104 @@
{
"app": {
"name": "Todo",
"loading": "Laden..."
},
"nav": {
"inbox": "Eingang",
"today": "Heute",
"upcoming": "Anstehend",
"projects": "Projekte",
"labels": "Labels",
"completed": "Erledigt",
"settings": "Einstellungen",
"feedback": "Feedback"
},
"task": {
"title": "Titel",
"description": "Beschreibung",
"dueDate": "Fällig am",
"dueTime": "Uhrzeit",
"priority": "Priorität",
"project": "Projekt",
"labels": "Labels",
"subtasks": "Teilaufgaben",
"reminder": "Erinnerung",
"repeat": "Wiederholen",
"addTask": "Aufgabe hinzufügen",
"editTask": "Aufgabe bearbeiten",
"deleteTask": "Aufgabe löschen",
"completeTask": "Aufgabe erledigen",
"uncompleteTask": "Als unerledigt markieren",
"noTasks": "Keine Aufgaben",
"noTasksToday": "Keine Aufgaben für heute",
"noTasksUpcoming": "Keine anstehenden Aufgaben"
},
"project": {
"create": "Projekt erstellen",
"edit": "Projekt bearbeiten",
"delete": "Projekt löschen",
"name": "Name",
"color": "Farbe",
"icon": "Symbol",
"archive": "Archivieren",
"noProjects": "Keine Projekte"
},
"label": {
"create": "Label erstellen",
"edit": "Label bearbeiten",
"delete": "Label löschen",
"name": "Name",
"color": "Farbe",
"noLabels": "Keine Labels"
},
"priority": {
"urgent": "Dringend",
"high": "Hoch",
"medium": "Normal",
"low": "Niedrig"
},
"repeat": {
"none": "Nicht wiederholen",
"daily": "Täglich",
"weekly": "Wöchentlich",
"monthly": "Monatlich",
"yearly": "Jährlich"
},
"auth": {
"login": "Anmelden",
"logout": "Abmelden",
"register": "Registrieren",
"email": "E-Mail",
"password": "Passwort",
"forgotPassword": "Passwort vergessen?"
},
"common": {
"save": "Speichern",
"cancel": "Abbrechen",
"delete": "Löschen",
"edit": "Bearbeiten",
"add": "Hinzufügen",
"close": "Schließen",
"search": "Suchen",
"error": "Fehler",
"success": "Erfolgreich",
"loading": "Laden...",
"noResults": "Keine Ergebnisse"
},
"errors": {
"loadTasks": "Aufgaben konnten nicht geladen werden",
"createTask": "Aufgabe konnte nicht erstellt werden",
"updateTask": "Aufgabe konnte nicht aktualisiert werden",
"deleteTask": "Aufgabe konnte nicht gelöscht werden",
"loadProjects": "Projekte konnten nicht geladen werden",
"loadLabels": "Labels konnten nicht geladen werden"
},
"success": {
"taskCreated": "Aufgabe erstellt",
"taskUpdated": "Aufgabe aktualisiert",
"taskDeleted": "Aufgabe gelöscht",
"taskCompleted": "Aufgabe erledigt",
"projectCreated": "Projekt erstellt",
"labelCreated": "Label erstellt"
}
}

View file

@ -0,0 +1,104 @@
{
"app": {
"name": "Todo",
"loading": "Loading..."
},
"nav": {
"inbox": "Inbox",
"today": "Today",
"upcoming": "Upcoming",
"projects": "Projects",
"labels": "Labels",
"completed": "Completed",
"settings": "Settings",
"feedback": "Feedback"
},
"task": {
"title": "Title",
"description": "Description",
"dueDate": "Due date",
"dueTime": "Time",
"priority": "Priority",
"project": "Project",
"labels": "Labels",
"subtasks": "Subtasks",
"reminder": "Reminder",
"repeat": "Repeat",
"addTask": "Add task",
"editTask": "Edit task",
"deleteTask": "Delete task",
"completeTask": "Complete task",
"uncompleteTask": "Mark as incomplete",
"noTasks": "No tasks",
"noTasksToday": "No tasks for today",
"noTasksUpcoming": "No upcoming tasks"
},
"project": {
"create": "Create project",
"edit": "Edit project",
"delete": "Delete project",
"name": "Name",
"color": "Color",
"icon": "Icon",
"archive": "Archive",
"noProjects": "No projects"
},
"label": {
"create": "Create label",
"edit": "Edit label",
"delete": "Delete label",
"name": "Name",
"color": "Color",
"noLabels": "No labels"
},
"priority": {
"urgent": "Urgent",
"high": "High",
"medium": "Normal",
"low": "Low"
},
"repeat": {
"none": "Don't repeat",
"daily": "Daily",
"weekly": "Weekly",
"monthly": "Monthly",
"yearly": "Yearly"
},
"auth": {
"login": "Login",
"logout": "Logout",
"register": "Register",
"email": "Email",
"password": "Password",
"forgotPassword": "Forgot password?"
},
"common": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit",
"add": "Add",
"close": "Close",
"search": "Search",
"error": "Error",
"success": "Success",
"loading": "Loading...",
"noResults": "No results"
},
"errors": {
"loadTasks": "Failed to load tasks",
"createTask": "Failed to create task",
"updateTask": "Failed to update task",
"deleteTask": "Failed to delete task",
"loadProjects": "Failed to load projects",
"loadLabels": "Failed to load labels"
},
"success": {
"taskCreated": "Task created",
"taskUpdated": "Task updated",
"taskDeleted": "Task deleted",
"taskCompleted": "Task completed",
"projectCreated": "Project created",
"labelCreated": "Label created"
}
}

View file

@ -1,6 +1,8 @@
<script lang="ts">
import '../app.css';
import '$lib/i18n';
import { onMount } from 'svelte';
import { isLoading as i18nLoading } from 'svelte-i18n';
import { theme } from '$lib/stores/theme';
import { authStore } from '$lib/stores/auth.svelte';
import { AppLoadingSkeleton } from '$lib/components/skeletons';
@ -8,6 +10,7 @@
let { children } = $props();
let loading = $state(true);
let appReady = $derived(!loading && !$i18nLoading);
onMount(async () => {
// Initialize theme
@ -20,7 +23,7 @@
});
</script>
{#if loading}
{#if !appReady}
<AppLoadingSkeleton />
{:else}
<div class="min-h-screen bg-background text-foreground">