diff --git a/docs/optimizable/i18n-migration-inventory.md b/docs/optimizable/i18n-migration-inventory.md new file mode 100644 index 000000000..c195b3cd0 --- /dev/null +++ b/docs/optimizable/i18n-migration-inventory.md @@ -0,0 +1,91 @@ +# i18n Migration Inventory + +**Status as of 2026-04-22.** Run `pnpm run audit:i18n-coverage` for current numbers. + +## The gap + +The unified Mana web app has `@mana/shared-i18n` + `svelte-i18n` +**fully wired** — per-module translation files live under +`apps/mana/apps/web/src/lib/i18n/locales/{module}/{de,en,it,fr,es}.json` +for 35 modules, with ~3500 lines of German and matching English / +Italian / French / Spanish. The infrastructure is done. + +What's missing is the **usage**: most module `.svelte` templates +hardcode German strings like `"Abbrechen"`, `"+ Neues Mood"`, +`"Keine Daten"` instead of calling `{$_('module.key')}`. + +Scanning `lib/modules/**/*.svelte`: + +- **FULL** (4 modules) — all files import `$_()` from svelte-i18n +- **PARTIAL** (9 modules) — mixed usage +- **NONE** (65 modules) — German hardcoded throughout + +## Why not auto-migrate? + +String migration is per-site careful work: +- pick a key name (or reuse existing one from the locale file) +- pick the German source text (may differ slightly from existing keys) +- add key to all 5 language files (de/en/it/fr/es) +- replace in template, test UI visually +- tests covering copy may break + +It's not a mechanical codemod. Each module is a session of its own. + +## Priorities + +Rank by `keyword-hits × user-impact`. Run the audit for current numbers: + +```bash +pnpm run audit:i18n-coverage --summary --top 20 +``` + +Top offenders (2026-04-22 snapshot): + +| Rank | Module | Hits | Files | Locale? | Notes | +|---|---|---|---|---|---| +| 1 | broadcast | 26 | 10 | ✗ | Content broadcast compose + recipient UI | +| 2 | articles | 24 | 16 | ✗ | Reader view, highlights, filter labels | +| 3 | events | 23 | 12 | ✗ | RSVP, guest list, discovery | +| 4 | invoices | 22 | 9 | ✗ | Business-critical: line items, payment states | +| 5 | quiz | 20 | 3 | ✗ | Edit + play flows, question types | +| 6 | stretch | 20 | 6 | ✗ | Session flows, reminders, routines | +| 7 | library | 19 | 11 | ✗ | Book/media tracker: status, filtering | +| 8 | profile | 17 | 4 | ✓ | Interview flow, context overview | +| 9 | skilltree | 15 | 11 | ✓ | PARTIAL — modals done, ListView hardcoded | +| 10 | calendar | 14 | 15 | ✓ | PARTIAL — EventForm done, ListView labels | + +**Already FULL:** `body`, `todo`, `times`, and the modules whose +keyword count is zero (either trivially small templates or consistent +use of `$_()`). + +## Recommended workflow per module + +1. **If no locale file exists** — run the skilltree/moodlit layout as + a template. Start with: `nav.*`, `common.{save,cancel,delete,confirm}`, + `list.{empty,count,newItem}`, `create.{title,save,namePlaceholder}`, + plus module-specific vocabulary. +2. **Extract German strings** into `de.json` — keep them verbatim so + existing copy doesn't regress. +3. **Translate to `en.json`** (required for sync across language + switch). Keep `it/fr/es.json` with English fallbacks if translator + not available. +4. **Replace in templates** — `import { _ } from 'svelte-i18n'` at the + top, then `{$_('module.key')}` inline. For attributes: + `placeholder={$_('module.namePlaceholder')}`. +5. **Update `i18n/index.ts`** — add the module to `registerLocale()` if + it isn't already listed (broadcast/articles/events/invoices/quiz/ + stretch/library/wishes/guides/habits/dreams/firsts/companion/ + ai-missions all need entries). +6. **Re-run the audit** — `pnpm run audit:i18n-coverage --summary` — + expect the module to move from NONE → FULL. +7. **Manual browser test** — the audit is pattern-based; visual QA + catches anything it missed. + +## Prevention (future work) + +The audit currently reports only — it doesn't fail CI. A future step +could graduate it to a gate that: +- blocks PRs that *add* hardcoded German to ListViews already migrated +- continues to tolerate existing debt until it's cleared + +Until then: `audit:i18n-coverage` is the contract. diff --git a/package.json b/package.json index e24f56557..18e4d0f0d 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "audit:coupling": "node scripts/audit-module-coupling.mjs", "audit:complexity": "node scripts/audit-complexity.mjs", "audit:map": "node scripts/build-complexity-map.mjs", + "audit:i18n-coverage": "node scripts/audit-i18n-coverage.mjs", "generate:dockerfiles": "node scripts/generate-dockerfiles.mjs", "setup:env": "node scripts/generate-env.mjs", "setup:secrets": "node scripts/setup-secrets.mjs", diff --git a/scripts/audit-i18n-coverage.mjs b/scripts/audit-i18n-coverage.mjs new file mode 100644 index 000000000..c1c533b95 --- /dev/null +++ b/scripts/audit-i18n-coverage.mjs @@ -0,0 +1,232 @@ +#!/usr/bin/env node +/** + * Audit i18n coverage across module UI files. + * + * Background: @mana/shared-i18n + svelte-i18n are fully wired. Per-module + * translation files exist under `apps/mana/apps/web/src/lib/i18n/locales/ + * {module}/{de,en,it,fr,es}.json` for ~35 modules. Yet most module + * `.svelte` templates hardcode German strings instead of calling `$_()`. + * + * This audit flags the gap without being a blocker: it prints a per-module + * report of `.svelte` files that likely contain hardcoded German UI + * strings AND don't yet import from `svelte-i18n` / `$_`. The stats guide + * migration priorities without forcing a failing check. + * + * Detection heuristic: look for common German UI keywords inside Svelte + * template text nodes (Abbrechen, Speichern, Löschen, Hinzufügen, + * Erstellen, Bearbeiten, etc.). Not foolproof — can miss embedded + * placeholder text and hit false positives — but good enough to prioritise. + * + * Usage: + * node scripts/audit-i18n-coverage.mjs # full report + * node scripts/audit-i18n-coverage.mjs --summary # one-line per module + * node scripts/audit-i18n-coverage.mjs --top 10 # top N offenders only + */ + +import { readFileSync, readdirSync, existsSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = join(__dirname, '..'); +const MODULES_DIR = join(REPO_ROOT, 'apps/mana/apps/web/src/lib/modules'); +const LOCALES_DIR = join(REPO_ROOT, 'apps/mana/apps/web/src/lib/i18n/locales'); + +const args = process.argv.slice(2); +const SUMMARY = args.includes('--summary'); +const TOP_IDX = args.indexOf('--top'); +const TOP_N = TOP_IDX >= 0 ? Number(args[TOP_IDX + 1] || 10) : null; + +// Common German UI keywords that indicate hardcoded strings. Not every hit +// is a real violation (e.g. code comments, type names) — we scan only +// Svelte template bodies (between