Files
GearBox/.planning/phases/34-i18n-foundation/34-05-PLAN.md

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
01
02
03
04
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
true
D-13
D-14
D-15
truths artifacts key_links
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
path provides contains
src/client/locales/de/common.json German common namespace translations Speichern
path provides contains
src/client/locales/de/settings.json German settings translations Gewichtseinheit
path provides contains
src/client/lib/i18n.ts Updated i18n init with de resources deCommon
path provides min_lines
tests/i18n/locales.test.ts Key parity test 20
from to via pattern
src/client/lib/i18n.ts src/client/locales/de/common.json import deCommon 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.

<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.json

i18n.ts resources structure:

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. <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`:
  1. 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";
  1. Add de entry to the resources object:
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,
  },
},
  1. 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. <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> 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:

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>
- 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

<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>
After completion, create `.planning/phases/34-i18n-foundation/34-05-SUMMARY.md`