292 lines
10 KiB
Markdown
292 lines
10 KiB
Markdown
---
|
|
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>
|