13 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 | 05 | execute | 3 |
|
|
true |
|
|
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.
<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 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.jsoni18n.ts resources structure:
resources: {
en: {
common: enCommon,
collection: enCollection,
// ...
},
// de needs to be added here
},
For EACH English locale file (src/client/locales/en/*.json):
- Read the file to get the exact key structure
- Create the corresponding
src/client/locales/de/*.jsonwith the same key structure - 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. <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> 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`:- Add imports for all German locale files (after the English imports):
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";
- Add
deentry to theresourcesobject:
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,
},
},
- Add
supportedLngs: ["en", "de"]to the init config (afterfallbackLng). This prevents i18next from trying to load unsupported locales and forces fallback to "en" per D-12. <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 buildsucceeds </acceptance_criteria> 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
Create tests/i18n/locales.test.ts:
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.
<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>
cd /home/jlmak/Projects/jlmak/GearBox && bun test tests/i18n/locales.test.ts
Key parity test ensures en and de locales stay in sync
<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> |
<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>