Files
GearBox/.planning/phases/34-i18n-foundation/34-04-PLAN.md

10 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
34-i18n-foundation 04 execute 2
01
03
src/client/routes/settings.tsx
src/client/routes/__root.tsx
src/client/lib/i18n.ts
true
D-09
D-10
D-11
D-12
truths artifacts key_links
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
path provides contains
src/client/routes/settings.tsx Language picker UI language
path provides contains
src/client/routes/__root.tsx i18n language sync with settings changeLanguage
from to via pattern
src/client/routes/settings.tsx src/client/hooks/useLanguage.ts useLanguage() import useLanguage
from to via pattern
src/client/routes/__root.tsx src/client/lib/i18n.ts i18n.changeLanguage 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.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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 Current settings.tsx pill-toggle pattern (weight unit): ```tsx

Weight Unit

Choose the unit used to display weights across the app

{UNITS.map((u) => ( 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} ))}
```

useLanguage hook (from Plan 03):

export const VALID_LANGUAGES = ["en", "de"] as const;
export type Language = (typeof VALID_LANGUAGES)[number];
export function useLanguage(): Language { ... }

i18n instance:

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:
import i18n from "../lib/i18n";
import { useLanguage } from "../hooks/useLanguage";
  1. In the SettingsPage component, add after const updateSetting = useUpdateSetting();:
const language = useLanguage();
  1. Add a LANGUAGES constant at the top of the file (near UNITS and CURRENCIES):
const LANGUAGES = [
  { value: "en", label: "English" },
  { value: "de", label: "Deutsch" },
];
  1. Add the language picker section BEFORE the weight unit section (first item in the settings card). It uses the same pill-toggle pattern:
<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" />
  1. Add these keys to src/client/locales/en/settings.json if not already present:
{
  "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. <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> 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:
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.

  1. In the RootLayout function, add after existing hooks:
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 <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> 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

<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>
- `bun run build` succeeds - Language picker visible in settings page - Clicking a language option changes the UI language - Language preference persists across page reloads

<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>
After completion, create `.planning/phases/34-i18n-foundation/34-04-SUMMARY.md`