---
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"
---

<objective>
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.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.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

<interfaces>
Current settings.tsx pill-toggle pattern (weight unit):
```tsx
<div className="flex items-center justify-between">
  <div>
    <h3 className="text-sm font-medium text-gray-900">Weight Unit</h3>
    <p className="text-xs text-gray-500 mt-0.5">
      Choose the unit used to display weights across the app
    </p>
  </div>
  <div className="flex items-center gap-1 bg-gray-100 rounded-full px-1 py-0.5">
    {UNITS.map((u) => (
      <button key={u} type="button"
        onClick={() => updateSetting.mutate({ key: "weightUnit", value: u })}
        className={`px-2.5 py-1 text-xs rounded-full transition-colors ${
          unit === u
            ? "bg-white text-gray-700 shadow-sm font-medium"
            : "text-gray-400 hover:text-gray-600"
        }`}>
        {u}
      </button>
    ))}
  </div>
</div>
```

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
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add language picker to settings page</name>
  <files>src/client/routes/settings.tsx</files>
  <read_first>src/client/routes/settings.tsx, src/client/hooks/useLanguage.ts, src/client/locales/en/settings.json</read_first>
  <behavior>
    - 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
  </behavior>
  <action>
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
<div className="flex items-center justify-between">
  <div>
    <h3 className="text-sm font-medium text-gray-900">{t("language.title")}</h3>
    <p className="text-xs text-gray-500 mt-0.5">
      {t("language.description")}
    </p>
  </div>
  <div className="flex items-center gap-1 bg-gray-100 rounded-full px-1 py-0.5">
    {LANGUAGES.map((lang) => (
      <button
        key={lang.value}
        type="button"
        onClick={() => {
          updateSetting.mutate({ key: "language", value: lang.value });
          i18n.changeLanguage(lang.value);
        }}
        className={`px-2.5 py-1 text-xs rounded-full transition-colors ${
          language === lang.value
            ? "bg-white text-gray-700 shadow-sm font-medium"
            : "text-gray-400 hover:text-gray-600"
        }`}
      >
        {lang.label}
      </button>
    ))}
  </div>
</div>

<div className="border-t border-gray-100" />
```

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.
  </action>
  <acceptance_criteria>
    - 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
  </acceptance_criteria>
  <verify>
    <automated>cd /home/jlmak/Projects/jlmak/GearBox && grep -c "useLanguage\|changeLanguage\|LANGUAGES" src/client/routes/settings.tsx</automated>
  </verify>
  <done>Language picker added to settings matching existing preference UI pattern</done>
</task>

<task type="auto">
  <name>Task 2: Sync i18n language with settings on app load</name>
  <files>src/client/routes/__root.tsx</files>
  <read_first>src/client/routes/__root.tsx, src/client/hooks/useLanguage.ts, src/client/lib/i18n.ts</read_first>
  <behavior>
    - 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
  </behavior>
  <action>
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
  </action>
  <acceptance_criteria>
    - __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
  </acceptance_criteria>
  <verify>
    <automated>cd /home/jlmak/Projects/jlmak/GearBox && grep -c "useLanguage\|changeLanguage\|useTranslation" src/client/routes/__root.tsx</automated>
  </verify>
  <done>Language syncs between settings DB and i18n instance on load and change</done>
</task>

</tasks>

<threat_model>
## 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". |
</threat_model>

<verification>
- `bun run build` succeeds
- Language picker visible in settings page
- Clicking a language option changes the UI language
- Language preference persists across page reloads
</verification>

<success_criteria>
- 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
</success_criteria>

<output>
After completion, create `.planning/phases/34-i18n-foundation/34-04-SUMMARY.md`
</output>
