--- 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" --- 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. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.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 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 }, ``` Task 1: Create German translation files for all namespaces 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/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 - 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 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. - 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 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 All 6 German translation files created with complete translations Task 2: Register German locale in i18n configuration src/client/lib/i18n.ts src/client/lib/i18n.ts - 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 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. - 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 cd /home/jlmak/Projects/jlmak/GearBox && grep -c "deCommon\|deCollection\|deThreads\|deSetups\|deOnboarding\|deSettings\|supportedLngs" src/client/lib/i18n.ts i18n config loads both English and German resources Task 3: Write key parity test between en and de locales tests/i18n/locales.test.ts src/client/locales/en/common.json, src/client/locales/de/common.json - 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 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, 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, fullKey)); } else { keys.push(fullKey); } } return keys.sort(); } function loadLocale(locale: string): Record> { const dir = join(LOCALES_DIR, locale); const files = readdirSync(dir).filter((f) => f.endsWith(".json")); const result: Record> = {}; 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)?.[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. - 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 cd /home/jlmak/Projects/jlmak/GearBox && bun test tests/i18n/locales.test.ts Key parity test ensures en and de locales stay in sync ## 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. | - 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 - 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 After completion, create `.planning/phases/34-i18n-foundation/34-05-SUMMARY.md`