docs(34): create phase plans for i18n foundation
This commit is contained in:
291
.planning/phases/34-i18n-foundation/34-04-PLAN.md
Normal file
291
.planning/phases/34-i18n-foundation/34-04-PLAN.md
Normal file
@@ -0,0 +1,291 @@
|
||||
---
|
||||
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>
|
||||
Reference in New Issue
Block a user