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 |
|
|
true |
|
|
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): ```tsxWeight Unit
Choose the unit used to display weights across the app
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
- Add imports:
import i18n from "../lib/i18n";
import { useLanguage } from "../hooks/useLanguage";
- In the
SettingsPagecomponent, add afterconst updateSetting = useUpdateSetting();:
const language = useLanguage();
- Add a
LANGUAGESconstant at the top of the file (near UNITS and CURRENCIES):
const LANGUAGES = [
{ value: "en", label: "English" },
{ value: "de", label: "Deutsch" },
];
- 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" />
- Add these keys to
src/client/locales/en/settings.jsonif 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
- 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.
- In the
RootLayoutfunction, 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 buildsucceeds </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> |
<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>