docs(34): create phase plans for i18n foundation
This commit is contained in:
364
.planning/phases/34-i18n-foundation/34-05-PLAN.md
Normal file
364
.planning/phases/34-i18n-foundation/34-05-PLAN.md
Normal file
@@ -0,0 +1,364 @@
|
||||
---
|
||||
phase: 34-i18n-foundation
|
||||
plan: 05
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on: [01, 02, 03, 04]
|
||||
files_modified:
|
||||
- src/client/locales/de/common.json
|
||||
- src/client/locales/de/collection.json
|
||||
- src/client/locales/de/threads.json
|
||||
- src/client/locales/de/setups.json
|
||||
- src/client/locales/de/onboarding.json
|
||||
- src/client/locales/de/settings.json
|
||||
- src/client/lib/i18n.ts
|
||||
- tests/i18n/locales.test.ts
|
||||
autonomous: true
|
||||
requirements: [D-13, D-14, D-15]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "German locale files exist at src/client/locales/de/ for all 6 namespaces"
|
||||
- "Every key in en/*.json has a corresponding key in de/*.json"
|
||||
- "German translations are natural German, not word-for-word translations"
|
||||
- "i18n.ts loads both en and de resources"
|
||||
- "Switching to de locale renders German text throughout the app"
|
||||
- "A test verifies key parity between en and de locales"
|
||||
artifacts:
|
||||
- path: "src/client/locales/de/common.json"
|
||||
provides: "German common namespace translations"
|
||||
contains: "Speichern"
|
||||
- path: "src/client/locales/de/settings.json"
|
||||
provides: "German settings translations"
|
||||
contains: "Gewichtseinheit"
|
||||
- path: "src/client/lib/i18n.ts"
|
||||
provides: "Updated i18n init with de resources"
|
||||
contains: "deCommon"
|
||||
- path: "tests/i18n/locales.test.ts"
|
||||
provides: "Key parity test"
|
||||
min_lines: 20
|
||||
key_links:
|
||||
- from: "src/client/lib/i18n.ts"
|
||||
to: "src/client/locales/de/common.json"
|
||||
via: "import deCommon"
|
||||
pattern: "deCommon"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create German translations for all namespaces and register them in the i18n configuration.
|
||||
|
||||
Purpose: Ship the first additional language — German (de) alongside English (en), making the app fully bilingual.
|
||||
Output: Complete German translation files, i18n config updated, key parity test.
|
||||
</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>
|
||||
English locale files (source of truth for key structure):
|
||||
- src/client/locales/en/common.json
|
||||
- src/client/locales/en/collection.json
|
||||
- src/client/locales/en/threads.json
|
||||
- src/client/locales/en/setups.json
|
||||
- src/client/locales/en/onboarding.json
|
||||
- src/client/locales/en/settings.json
|
||||
|
||||
i18n.ts resources structure:
|
||||
```typescript
|
||||
resources: {
|
||||
en: {
|
||||
common: enCommon,
|
||||
collection: enCollection,
|
||||
// ...
|
||||
},
|
||||
// de needs to be added here
|
||||
},
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Create German translation files for all namespaces</name>
|
||||
<files>src/client/locales/de/common.json, src/client/locales/de/collection.json, src/client/locales/de/threads.json, src/client/locales/de/setups.json, src/client/locales/de/onboarding.json, src/client/locales/de/settings.json</files>
|
||||
<read_first>src/client/locales/en/common.json, src/client/locales/en/collection.json, src/client/locales/en/threads.json, src/client/locales/en/setups.json, src/client/locales/en/onboarding.json, src/client/locales/en/settings.json</read_first>
|
||||
<behavior>
|
||||
- Each de/*.json has the exact same key structure as its en/*.json counterpart
|
||||
- Values are natural German translations, not literal word-for-word
|
||||
- German translations use formal "Sie" form (standard for apps)
|
||||
- Common action buttons: Save→Speichern, Cancel→Abbrechen, Delete→Loeschen, Edit→Bearbeiten, Create→Erstellen, Close→Schliessen, Back→Zurueck, Search→Suchen
|
||||
- Navigation: Home→Startseite, Collection→Sammlung, Setups→Setups (keep English), Discover→Entdecken, Settings→Einstellungen
|
||||
- Interpolation variables ({{count}}, {{name}}) remain unchanged
|
||||
- Pluralization keys (_one, _other) have German plural forms
|
||||
</behavior>
|
||||
<action>
|
||||
Create directory `src/client/locales/de/`.
|
||||
|
||||
For EACH English locale file (`src/client/locales/en/*.json`):
|
||||
1. Read the file to get the exact key structure
|
||||
2. Create the corresponding `src/client/locales/de/*.json` with the same key structure
|
||||
3. Translate every value to natural German
|
||||
|
||||
**Translation guidelines:**
|
||||
- Use formal "Sie" address form (standard for web apps)
|
||||
- Keep brand names and technical terms in English where German speakers would expect it (e.g., "Setup" stays "Setup", "Thread" can stay "Thread" or become "Recherche")
|
||||
- Weight units (g, oz, lb, kg) are universal — keep as-is
|
||||
- Currency symbols stay as-is
|
||||
- Interpolation placeholders like `{{count}}` or `{{name}}` must remain exactly as-is in the German text
|
||||
- Pluralization: German uses the same _one/_other pattern as English for most cases
|
||||
|
||||
**Key German translations reference:**
|
||||
|
||||
| English | German |
|
||||
|---------|--------|
|
||||
| Save | Speichern |
|
||||
| Cancel | Abbrechen |
|
||||
| Delete | Loeschen |
|
||||
| Edit | Bearbeiten |
|
||||
| Create | Erstellen |
|
||||
| Close | Schliessen |
|
||||
| Back | Zurueck |
|
||||
| Search | Suchen |
|
||||
| Confirm | Bestaetigen |
|
||||
| Loading... | Laden... |
|
||||
| Something went wrong | Etwas ist schiefgelaufen |
|
||||
| Sign in | Anmelden |
|
||||
| Sign out | Abmelden |
|
||||
| Settings | Einstellungen |
|
||||
| Collection | Sammlung |
|
||||
| Items | Gegenstaende |
|
||||
| Weight | Gewicht |
|
||||
| Price | Preis |
|
||||
| Name | Name |
|
||||
| Brand | Marke |
|
||||
| Model | Modell |
|
||||
| Notes | Notizen |
|
||||
| Category | Kategorie |
|
||||
| No items yet | Noch keine Gegenstaende |
|
||||
| Weight Unit | Gewichtseinheit |
|
||||
| Currency | Waehrung |
|
||||
| Language | Sprache |
|
||||
| Import / Export | Import / Export |
|
||||
| API Keys | API-Schluessel |
|
||||
|
||||
**IMPORTANT:** Read each en/*.json file fully before translating. Every single key must have a German value. Do not leave any English strings in the de/*.json files.
|
||||
</action>
|
||||
<acceptance_criteria>
|
||||
- src/client/locales/de/common.json exists and is valid JSON
|
||||
- src/client/locales/de/collection.json exists and is valid JSON
|
||||
- src/client/locales/de/threads.json exists and is valid JSON
|
||||
- src/client/locales/de/setups.json exists and is valid JSON
|
||||
- src/client/locales/de/onboarding.json exists and is valid JSON
|
||||
- src/client/locales/de/settings.json exists and is valid JSON
|
||||
- de/common.json "actions.save" value is "Speichern" (not "Save")
|
||||
- de/common.json "nav.settings" value is "Einstellungen" (not "Settings")
|
||||
- de/settings.json contains "Gewichtseinheit" for weight unit label
|
||||
- All interpolation variables ({{count}}, {{name}}) preserved in German translations
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>cd /home/jlmak/Projects/jlmak/GearBox && for f in common collection threads setups onboarding settings; do node -e "JSON.parse(require('fs').readFileSync('src/client/locales/de/$f.json','utf8')); console.log('de/$f.json: valid')"; done</automated>
|
||||
</verify>
|
||||
<done>All 6 German translation files created with complete translations</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Register German locale in i18n configuration</name>
|
||||
<files>src/client/lib/i18n.ts</files>
|
||||
<read_first>src/client/lib/i18n.ts</read_first>
|
||||
<behavior>
|
||||
- i18n.ts imports all 6 de/*.json files
|
||||
- resources object includes "de" key with all 6 namespaces
|
||||
- supportedLngs is set to ["en", "de"] to prevent loading unsupported locales
|
||||
</behavior>
|
||||
<action>
|
||||
Update `src/client/lib/i18n.ts`:
|
||||
|
||||
1. Add imports for all German locale files (after the English imports):
|
||||
```typescript
|
||||
import deCommon from "../locales/de/common.json";
|
||||
import deCollection from "../locales/de/collection.json";
|
||||
import deThreads from "../locales/de/threads.json";
|
||||
import deSetups from "../locales/de/setups.json";
|
||||
import deOnboarding from "../locales/de/onboarding.json";
|
||||
import deSettings from "../locales/de/settings.json";
|
||||
```
|
||||
|
||||
2. Add `de` entry to the `resources` object:
|
||||
```typescript
|
||||
resources: {
|
||||
en: {
|
||||
common: enCommon,
|
||||
collection: enCollection,
|
||||
threads: enThreads,
|
||||
setups: enSetups,
|
||||
onboarding: enOnboarding,
|
||||
settings: enSettings,
|
||||
},
|
||||
de: {
|
||||
common: deCommon,
|
||||
collection: deCollection,
|
||||
threads: deThreads,
|
||||
setups: deSetups,
|
||||
onboarding: deOnboarding,
|
||||
settings: deSettings,
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
3. Add `supportedLngs: ["en", "de"]` to the init config (after `fallbackLng`). This prevents i18next from trying to load unsupported locales and forces fallback to "en" per D-12.
|
||||
</action>
|
||||
<acceptance_criteria>
|
||||
- i18n.ts imports deCommon, deCollection, deThreads, deSetups, deOnboarding, deSettings
|
||||
- i18n.ts resources object has "de" key with all 6 namespaces
|
||||
- i18n.ts has supportedLngs: ["en", "de"]
|
||||
- `bun run build` succeeds
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>cd /home/jlmak/Projects/jlmak/GearBox && grep -c "deCommon\|deCollection\|deThreads\|deSetups\|deOnboarding\|deSettings\|supportedLngs" src/client/lib/i18n.ts</automated>
|
||||
</verify>
|
||||
<done>i18n config loads both English and German resources</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: Write key parity test between en and de locales</name>
|
||||
<files>tests/i18n/locales.test.ts</files>
|
||||
<read_first>src/client/locales/en/common.json, src/client/locales/de/common.json</read_first>
|
||||
<behavior>
|
||||
- Test reads all en/*.json and de/*.json files
|
||||
- For each namespace, flattens keys to dot notation
|
||||
- Asserts every en key exists in de
|
||||
- Asserts every de key exists in en (no orphan keys)
|
||||
- Asserts no de values are empty strings
|
||||
- Test fails if a key is missing from either locale
|
||||
</behavior>
|
||||
<action>
|
||||
Create directory `tests/i18n/` if not exists.
|
||||
|
||||
Create `tests/i18n/locales.test.ts`:
|
||||
|
||||
```typescript
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const LOCALES_DIR = join(import.meta.dir, "../../src/client/locales");
|
||||
|
||||
function flattenKeys(obj: Record<string, unknown>, prefix = ""): string[] {
|
||||
const keys: string[] = [];
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const fullKey = prefix ? `${prefix}.${key}` : key;
|
||||
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
||||
keys.push(...flattenKeys(value as Record<string, unknown>, fullKey));
|
||||
} else {
|
||||
keys.push(fullKey);
|
||||
}
|
||||
}
|
||||
return keys.sort();
|
||||
}
|
||||
|
||||
function loadLocale(locale: string): Record<string, Record<string, unknown>> {
|
||||
const dir = join(LOCALES_DIR, locale);
|
||||
const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
|
||||
const result: Record<string, Record<string, unknown>> = {};
|
||||
for (const file of files) {
|
||||
const ns = file.replace(".json", "");
|
||||
result[ns] = JSON.parse(readFileSync(join(dir, file), "utf8"));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
describe("locale key parity", () => {
|
||||
const en = loadLocale("en");
|
||||
const de = loadLocale("de");
|
||||
|
||||
test("en and de have the same namespaces", () => {
|
||||
expect(Object.keys(en).sort()).toEqual(Object.keys(de).sort());
|
||||
});
|
||||
|
||||
for (const ns of Object.keys(en)) {
|
||||
test(`${ns}: every en key exists in de`, () => {
|
||||
const enKeys = flattenKeys(en[ns]);
|
||||
const deKeys = flattenKeys(de[ns]);
|
||||
const missing = enKeys.filter((k) => !deKeys.includes(k));
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
|
||||
test(`${ns}: every de key exists in en`, () => {
|
||||
const enKeys = flattenKeys(en[ns]);
|
||||
const deKeys = flattenKeys(de[ns]);
|
||||
const orphan = deKeys.filter((k) => !enKeys.includes(k));
|
||||
expect(orphan).toEqual([]);
|
||||
});
|
||||
|
||||
test(`${ns}: no empty de values`, () => {
|
||||
const deFlat = flattenKeys(de[ns]);
|
||||
for (const key of deFlat) {
|
||||
const value = key.split(".").reduce(
|
||||
(obj, k) => (obj as Record<string, unknown>)?.[k],
|
||||
de[ns] as unknown,
|
||||
);
|
||||
expect(typeof value === "string" && value.length > 0).toBe(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
This test automatically discovers all namespace files and checks key parity without hardcoding namespace names. When future languages are added, the test structure can be extended.
|
||||
</action>
|
||||
<acceptance_criteria>
|
||||
- tests/i18n/locales.test.ts exists
|
||||
- Test checks namespace parity between en and de
|
||||
- Test checks key parity for each namespace (both directions)
|
||||
- Test checks no empty strings in de translations
|
||||
- `bun test tests/i18n/locales.test.ts` passes
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>cd /home/jlmak/Projects/jlmak/GearBox && bun test tests/i18n/locales.test.ts</automated>
|
||||
</verify>
|
||||
<done>Key parity test ensures en and de locales stay in sync</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| locale JSON→i18n | Static bundled files — trusted, no runtime injection vector |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-34-06 | Spoofing | de locale files | accept | German translations are AI-generated per D-14. No security implication — worst case is awkward German. Users correct organically. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- All 6 de/*.json files are valid JSON
|
||||
- `bun test tests/i18n/locales.test.ts` passes (key parity)
|
||||
- `bun run build` succeeds
|
||||
- Switching to "de" in settings renders German text
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Complete German translations for all 6 namespaces
|
||||
- i18n config loads both en and de resources
|
||||
- Key parity test prevents translation drift
|
||||
- Build passes with both locales
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/34-i18n-foundation/34-05-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user