Skip to content

Commit 57eada0

Browse files
committed
i18n: add EN/ES dictionaries, lazy-load provider, and language selector
1 parent 81b0a93 commit 57eada0

3 files changed

Lines changed: 35 additions & 2 deletions

File tree

app/i18n.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const I18nContext = createContext<I18nContextValue | null>(null);
1515

1616
// Base English messages (scaffold). Keys are dot-notation strings.
1717
let enMessages: Record<string, string> = {};
18+
let esMessages: Record<string, string> = {};
1819

1920
async function loadEnMessages(): Promise<Record<string, string>> {
2021
try {
@@ -26,6 +27,16 @@ async function loadEnMessages(): Promise<Record<string, string>> {
2627
}
2728
}
2829

30+
async function loadEsMessages(): Promise<Record<string, string>> {
31+
try {
32+
const res = await fetch('/locales/es.json', { cache: 'no-store' });
33+
if (!res.ok) return {};
34+
return (await res.json()) as Record<string, string>;
35+
} catch {
36+
return {};
37+
}
38+
}
39+
2940
function format(template: string, vars?: Vars): string {
3041
if (!vars) return template;
3142
return template.replace(/\{(\w+)\}/g, (_m, k: string) => (vars[k] !== undefined ? String(vars[k]) : `{${k}}`));
@@ -47,14 +58,18 @@ export function I18nProvider({ children }: { children: React.ReactNode }) {
4758
(async () => {
4859
if (locale === 'en') {
4960
enMessages = await loadEnMessages();
50-
setLoaded((v) => !v); // trigger rerender
61+
setLoaded((v) => !v);
62+
} else if (locale === 'es') {
63+
esMessages = await loadEsMessages();
64+
setLoaded((v) => !v);
5165
}
5266
})();
5367
}, [locale]);
5468

5569
const t = useCallback((key: string, vars?: Vars) => {
5670
// Only 'en' for now; scaffold for future locales
57-
const message = (locale === 'en' ? enMessages[key] : undefined) || key;
71+
const dict = locale === 'en' ? enMessages : locale === 'es' ? esMessages : enMessages;
72+
const message = dict[key] || key;
5873
return format(message, vars);
5974
}, [locale]);
6075

app/theme-toggle.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22

33
import { useEffect, useState } from 'react';
4+
import { useI18n } from './i18n';
45

56
function getTheme(): 'dark' | 'light' {
67
if (typeof window === 'undefined') return 'dark';
@@ -10,6 +11,7 @@ function getTheme(): 'dark' | 'light' {
1011
}
1112

1213
export function ThemeToolbar() {
14+
const { locale, setLocale } = useI18n();
1315
const [theme, setTheme] = useState<'dark' | 'light'>(getTheme());
1416
const [contrast, setContrast] = useState<'normal' | 'high'>(() => (typeof window === 'undefined' ? 'normal' : (window.localStorage.getItem('contrast') as 'normal' | 'high') || 'normal'));
1517
const [motion, setMotion] = useState<'normal' | 'reduce'>(() => (typeof window === 'undefined' ? 'normal' : (window.localStorage.getItem('motion') as 'normal' | 'reduce') || 'normal'));
@@ -45,6 +47,10 @@ export function ThemeToolbar() {
4547
<button onClick={() => setMotion(motion === 'normal' ? 'reduce' : 'normal')} aria-pressed={motion === 'reduce'} aria-label="Toggle reduced motion">
4648
Motion
4749
</button>
50+
<select value={locale} onChange={(e) => setLocale(e.target.value)} aria-label="Language">
51+
<option value="en">EN</option>
52+
<option value="es">ES</option>
53+
</select>
4854
</div>
4955
);
5056
}

locales/es.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"home.title": "Planificador de Optimización Diaria",
3+
"home.init": "Aplicación Next.js (TypeScript) inicializada.",
4+
"nav.onboarding.hard": "Comenzar onboarding: Restricciones fijas",
5+
"nav.onboarding.lifestyle": "Siguiente: Estilo de vida y capacidad",
6+
"nav.onboarding.areas": "Siguiente: Importancia por áreas",
7+
"nav.onboarding.wellbeing": "Siguiente: Objetivos de bienestar",
8+
"nav.onboarding.integrations": "Onboarding: Integraciones (ICS/CSV)",
9+
"nav.plan": "Abrir vista de Plan (arrastrar y soltar)"
10+
}
11+
12+

0 commit comments

Comments
 (0)