--- phase: 34-i18n-foundation plan: 04 type: execute wave: 2 depends_on: [01, 03] files_modified: - src/client/routes/settings.tsx - src/client/routes/__root.tsx - src/client/lib/i18n.ts autonomous: true requirements: [D-09, D-10, D-11, D-12] must_haves: truths: - "Language picker appears in settings page with English and Deutsch options" - "Language picker uses the pill-toggle pattern matching weight unit and currency pickers" - "Selecting a language persists via updateSetting('language', value)" - "Selecting a language calls i18n.changeLanguage(value) to update the UI immediately" - "Language picker is placed above weight unit in settings page" - "Browser auto-detection works on first visit (navigator.language)" - "Unknown browser locales fall back to English" artifacts: - path: "src/client/routes/settings.tsx" provides: "Language picker UI" contains: "language" - path: "src/client/routes/__root.tsx" provides: "i18n language sync with settings" contains: "changeLanguage" key_links: - from: "src/client/routes/settings.tsx" to: "src/client/hooks/useLanguage.ts" via: "useLanguage() import" pattern: "useLanguage" - from: "src/client/routes/__root.tsx" to: "src/client/lib/i18n.ts" via: "i18n.changeLanguage" pattern: "changeLanguage" --- Add language picker to settings page and wire language changes to i18n instance. Purpose: User controls — users can see their current language, change it, and the UI updates immediately. Output: Language picker in settings, i18n sync on language change, browser auto-detection on first visit. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/34-i18n-foundation/34-CONTEXT.md @.planning/phases/34-i18n-foundation/34-RESEARCH.md Current settings.tsx pill-toggle pattern (weight unit): ```tsx

Weight Unit

Choose the unit used to display weights across the app

{UNITS.map((u) => ( ))}
``` useLanguage hook (from Plan 03): ```typescript export const VALID_LANGUAGES = ["en", "de"] as const; export type Language = (typeof VALID_LANGUAGES)[number]; export function useLanguage(): Language { ... } ``` i18n instance: ```typescript import i18n from "../lib/i18n"; i18n.changeLanguage("de"); // switches language ```
Task 1: Add language picker to settings page src/client/routes/settings.tsx src/client/routes/settings.tsx, src/client/hooks/useLanguage.ts, src/client/locales/en/settings.json - Settings page imports useLanguage from hooks/useLanguage - Settings page imports i18n from lib/i18n - Language picker section appears ABOVE the weight unit section (first preference in the list) - Language picker uses the same pill-toggle pattern as weight unit and currency - Options: "English" (value: "en") and "Deutsch" (value: "de") - Clicking an option calls updateSetting.mutate({ key: "language", value }) AND i18n.changeLanguage(value) - Active language is highlighted with the same styling pattern - Label and description use t() keys from settings namespace Update `src/client/routes/settings.tsx`: 1. Add imports: ```typescript import i18n from "../lib/i18n"; import { useLanguage } from "../hooks/useLanguage"; ``` 2. In the `SettingsPage` component, add after `const updateSetting = useUpdateSetting();`: ```typescript const language = useLanguage(); ``` 3. Add a `LANGUAGES` constant at the top of the file (near UNITS and CURRENCIES): ```typescript const LANGUAGES = [ { value: "en", label: "English" }, { value: "de", label: "Deutsch" }, ]; ``` 4. Add the language picker section BEFORE the weight unit section (first item in the settings card). It uses the same pill-toggle pattern: ```tsx

{t("language.title")}

{t("language.description")}

{LANGUAGES.map((lang) => ( ))}
``` 5. Add these keys to `src/client/locales/en/settings.json` if not already present: ```json { "language": { "title": "Language", "description": "Change the display language of the app" } } ``` **NOTE:** Language labels ("English", "Deutsch") are intentionally NOT translated — they should always appear in their native language so users can identify their language even when the UI is in another language. - settings.tsx imports useLanguage and i18n - settings.tsx has LANGUAGES constant with "en"/"English" and "de"/"Deutsch" - Language picker section appears before weight unit section - onClick handler calls both updateSetting.mutate and i18n.changeLanguage - Language labels use native names ("English", "Deutsch"), not translated - Pill-toggle styling matches weight unit and currency pickers - settings.json has language.title and language.description keys - `bun run build` succeeds cd /home/jlmak/Projects/jlmak/GearBox && grep -c "useLanguage\|changeLanguage\|LANGUAGES" src/client/routes/settings.tsx Language picker added to settings matching existing preference UI pattern Task 2: Sync i18n language with settings on app load src/client/routes/__root.tsx src/client/routes/__root.tsx, src/client/hooks/useLanguage.ts, src/client/lib/i18n.ts - RootLayout component syncs i18n language when useLanguage() value changes - On first load, if user has a saved language preference, i18n switches to it - If no saved preference, i18n uses the browser-detected language (already configured in i18n.ts detection) - useEffect watches language value and calls i18n.changeLanguage when it changes - This handles the case where a user has "de" saved in settings but i18n initially detected "en" from browser Update `src/client/routes/__root.tsx`: 1. Add imports: ```typescript import { useEffect } from "react"; import { useTranslation } from "react-i18next"; import { useLanguage } from "../hooks/useLanguage"; ``` Note: `useState` is already imported. Check if `useEffect` is already imported — if not, add it. 2. In the `RootLayout` function, add after existing hooks: ```typescript const language = useLanguage(); const { i18n } = useTranslation(); useEffect(() => { if (language && i18n.language !== language) { i18n.changeLanguage(language); } }, [language, i18n]); ``` This syncs the i18n instance with the persisted language setting. On first load: - i18next's LanguageDetector picks browser locale or localStorage cache - useSetting("language") resolves from the DB - If they differ, useEffect syncs i18n to the DB value (DB is source of truth) On subsequent language changes via settings: - updateSetting immediately calls i18n.changeLanguage (in settings.tsx) - useLanguage() updates via React Query invalidation - useEffect acts as a safety net if the values drift - __root.tsx imports useLanguage from hooks/useLanguage - __root.tsx imports useTranslation from react-i18next - RootLayout has useEffect that calls i18n.changeLanguage(language) - useEffect depends on [language, i18n] - `bun run build` succeeds cd /home/jlmak/Projects/jlmak/GearBox && grep -c "useLanguage\|changeLanguage\|useTranslation" src/client/routes/__root.tsx Language syncs between settings DB and i18n instance on load and change ## Trust Boundaries | Boundary | Description | |----------|-------------| | settings API→i18n | Language value from DB flows into i18n.changeLanguage — validated | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-34-05 | Tampering | settings.tsx language picker | accept | Language values limited to LANGUAGES constant array ("en", "de"). Even if tampered, worst case is fallback to "en". | - `bun run build` succeeds - Language picker visible in settings page - Clicking a language option changes the UI language - Language preference persists across page reloads - Language picker in settings with English and Deutsch options - Pill-toggle pattern matches weight unit and currency pickers - Language change persists and syncs with i18n - Browser auto-detection works for first visit After completion, create `.planning/phases/34-i18n-foundation/34-04-SUMMARY.md`