# I18N-1 — One export, so a consumer cannot half-register the kit's strings

**Target repo:** `ui-core-micha` (branch `main`)
**Tier:** 3 — shared-core, and it adds a public export every consumer will depend on
**Target version:** `2.36.0` — minor. A new capability, fully backwards compatible.
**Prototype:** none. Nothing visual changes.

---

## A. Envelope

### Goal

`ui-core-micha` exports its UI strings as **six separate bundles** that each consuming app must
spread into its own i18n resources by hand. Adopting a component without also adding its bundle
produces **a raw key rendered as visible UI text** — silently, with no type error and no runtime
error. Ship one aggregate export so that cannot happen, and so a *future* ucm feature is carried
automatically instead of requiring twelve apps to notice.

### The evidence

**Two apps hit by the same mechanism, one of them still live.**

- **cockpit, 2026-08-11 (`UI-3`).** Adopted `UserMenu`; the matching `userMenuTranslations` was not
  spread, and the header button rendered the literal string `UserMenu.TITLE`. **Both independent
  static reviews were clean** — the defect is structurally invisible to `Read`/`Grep` review. It
  was caught by the rendered click-through, and only by DOM inspection at that, because screenshot
  capture was unavailable in that session. Fixed there.
- **fitness-monitor, live now.** Imports `ChartFrame` (`frontend/src/pages/BodyHistoryPage.jsx:7`,
  used at `:295` and `:445`) but registers **only** `authTranslations`
  (`frontend/i18n/index.js`), and carries no `ChartFrame.*` key of its own. So
  `ChartFrame.jsx:74`'s `t('ChartFrame.LOADING')` and its sibling empty/error states render their
  raw keys. Nobody noticed because those are transient edge states — which is exactly the class
  `DESIGN.md` #7 says must be designed rather than left to chance.

**The estate survey, measured 2026-08-11.** Twelve apps register ucm bundles:

| App | Bundles registered | ucm components used beyond auth | Verdict |
|---|---|---|---|
| jg-ferien | all six + surveyRenderer | Bell, Settings, Wizard, UserMenu | correct |
| cockpit | auth, notifications, onboarding, userMenu | Bell, Settings, Wizard, UserMenu | correct (since `UI-3`) |
| hram | auth, charts, userMenu | ChartFrame, UserMenu | correct |
| **fitness-monitor** | **auth only** | **ChartFrame** | **defective** |
| spesix, survey_app, kerzenziehen, webshop-guenter, webapp-template, hpc-bridge, bigler-consult, survey_contact_app | auth only | none | correct today, exposed the moment they adopt anything |

Eight of the twelve are correct only because they have not yet adopted a second component. That is
not a safety property; it is a coincidence with an expiry date.

**The merge is safe.** All six bundles together hold **504 keys with zero collisions**, verified by
enumerating them; every key is namespace-prefixed (`Auth.*`, `ChartFrame.*`, `Messaging*.*`,
`NotificationBell.*`, `Onboarding.*`, `UserMenu.*`, plus the bare allauth error codes
`AUTH-2` deals with). A flat aggregate therefore changes no resolution today.

### Scope

1. **Export `uiCoreTranslations`** from the package root — the flat merge of all six bundles.
   Consumers spread one thing instead of six.
2. **Keep the six per-feature exports.** Removing them would break twelve apps for no gain; this
   work order is additive. An app may spread both during migration — harmless, because the values
   are identical and there are no collisions.
3. **A ratchet test, and this is the part that makes the fix permanent.** Assert that the aggregate
   contains every key of every bundle in `src/i18n/`, **enumerated from the directory rather than
   from a hand-written list**. A new bundle added later that is not folded into the aggregate must
   fail this test. Without that, the aggregate is correct today and rots the same way the six
   exports did.
4. **A collision test.** Assert the union has no duplicate key across bundles. It holds today at
   504 keys; the test is what stops a future bundle from silently shadowing an existing string.
5. **Document the contract** where a consumer will actually meet it: one line in the README's i18n
   section saying that spreading `uiCoreTranslations` is the supported way, and that the
   per-feature exports remain for apps that want to be selective and accept the pairing burden.

### Non-goals / do not touch

- **Self-registering components.** Feasible — ucm components use `useTranslation()` against the
  consumer's own instance, so a component could call `addResourceBundle` itself — but it is a
  separate question and a separate work order (`I18N-2`), deliberately: all twelve consumers flatten
  everything into the **default namespace** with dotted keys, so the registration shape has to match
  exactly, and registering at render time risks a first-paint flash of the raw key. That would
  replace an invisible defect with a visible one. Framed as a bundle-size optimisation, not a
  correctness fix, because this work order already closes the correctness hole.
- **Namespaces**, and the flat dotted-key shape generally. Twelve apps depend on it.
- **The bare allauth error codes** in `authTranslations` — `AUTH-2` owns those.
- **Any consumer repository.** See the rollout note below.
- Removing or renaming any existing export.

### Risks

- **Bundle size for the eight auth-only apps.** `authTranslations` is 316 keys on its own; the other
  five add 188, of which `messagingTranslations` is 116. An auth-only app that switches to the
  aggregate ships roughly 30 KB of source strings it never renders. Judged acceptable against a
  defect class that has already fired twice — but **measure the built delta and report it**, because
  that number is the whole input to `I18N-2`, and a guess there would make that decision
  unfalsifiable.
- **The ratchet test can be faked.** A test that enumerates the directory but compares against a
  re-export (`export * from './i18n'`) would pass while proving nothing. It must compare *keys of
  the aggregate value* against *keys read from each bundle module*.
- **Consumers spreading both during migration.** Safe today because of the zero-collision property
  — which is precisely why item 4 is a test and not a comment.
- A parallel session is active in this repo (`THEME-*` work landed today up to `2.35.0`). Check
  `git status` at start and sequence.

### Rollout — and the constraint that shapes it

**A work order implements in one repository.** The twelve consumers cannot be migrated from here,
so this work order ships the export and the rollout lands as named rows in the consuming repos.

- **`fitness-monitor/FM-14` is mandatory and is the reason this is not merely hygiene** — it is the
  app with a live defect. It is written and blocked on this work order's release.
  **State the exposure plainly: fitness-monitor renders raw keys until `FM-14` lands.** The operator
  chose to route the fix through this rollout rather than patch it separately, so that it cannot be
  lost between two work orders; the cost of that choice is the delay, and it is recorded here rather
  than left implicit.
- The other eleven apps are **correct today**. Migrating them to the aggregate is prevention, not
  repair, and it can ride along the next time each is touched. Do not open eleven rows now.

### Required tests to WRITE

1. `uiCoreTranslations` contains every key of every module in `src/i18n/`, with the module list read
   from the directory, not hand-written.
2. The union has no duplicate key across bundles (504 today; assert the property, not the number).
3. Every value keeps its `{ de, fr, en, sw }` shape through the merge — no bundle is flattened or
   partially spread.
4. The six per-feature exports still exist and are unchanged, so no consumer breaks.

Run ucm's own affected set, not the full suite: the i18n tests plus whatever imports from
`src/index.js`.

### Parity guardrail

Purely additive. No existing export changes, no key changes, no rendered output changes in any app
that is correct today.

---

## B. Implementation map

*Filled by the Orchestrator on `git pull` — see `AGENTS.md` → "Work Order".*

### Context package

**Bundle files** (all in `src/i18n/`, each `export const <name>Translations = { 'Key.NAME': { de, fr,
en, sw }, ... }`, flat object, no nesting):

- `src/i18n/authTranslations.ts` → `authTranslations` (316 keys, includes bare allauth codes like
  `email_password_mismatch` alongside `Auth.*` keys — still a flat top-level key, no special casing
  needed for the merge)
- `src/i18n/chartsTranslations.ts` → `chartsTranslations` (`ChartFrame.*`)
- `src/i18n/messagingTranslations.ts` → `messagingTranslations` (`Messaging*.*`, 116 keys)
- `src/i18n/notificationsTranslations.ts` → `notificationsTranslations` (`NotificationBell.*`)
- `src/i18n/onboardingTranslations.ts` → `onboardingTranslations` (`Onboarding.*`)
- `src/i18n/userMenuTranslations.ts` → `userMenuTranslations` (`UserMenu.*`)

**Root export site:** `src/index.js` — the six per-feature translation exports currently sit at two
places: `export { authTranslations } ... export { userMenuTranslations } ...` under the `// --- 7.
Translations ---` comment (~line 78-79), and `export { notificationsTranslations } ...
onboardingTranslations ... chartsTranslations ... messagingTranslations` under `// --- 10.
Translations ---` (~line 121-124). Add the new `uiCoreTranslations` export near those, importing all
six bundle modules and merging with a plain object spread (`{ ...authTranslations, ...chartsTranslations,
... }`) — order doesn't matter, zero-collision is already verified. Do NOT remove or move the existing
six exports.

**Test file to add:** `tests/i18nAggregate.test.js` (new), following the directory-enumeration pattern
already used in `tests/themeCompleteness.test.js` (`fs.readdirSync` over a directory, don't hand-write
the file list) and the flat-object assertion style in `tests/authTranslations.test.js`. Enumerate
`src/i18n/*.ts`, `require`/dynamic-`import()` each module, find its exported `*Translations` object
(the module's only export), and:
1. assert every key of every bundle is present in `uiCoreTranslations` with an identical value
   (deep-equal, not just key presence — catches a bundle being spread partially);
2. assert the union of all bundle keys has no duplicates (build a `Map<key, sourceModule>`, fail with
   the two colliding module names if a key is seen twice);
3. assert every value in `uiCoreTranslations` has the `{ de, fr, en, sw }` shape (all four locale keys
   present, non-empty strings) — this must run against the aggregate, not against the individual
   bundles, or it doesn't actually check the merge preserved shape.
Import `uiCoreTranslations` from `../src/index` (the public export), not from a private merge module —
the ratchet only means something if it tests what a consumer actually imports.

**Do not touch:** the six existing named exports, any bundle file's content/keys, `src/theme/**`
(`THEME-*` work is active in parallel — check `git status`/`git log` before starting and do not touch
theme files even incidentally).

**README:** add one line to the i18n section (`grep -n "i18n" README.md` to find it) stating
`uiCoreTranslations` is the supported aggregate import and the six per-feature exports remain for
apps that want to stay selective.

**Bundle-size measurement:** after implementing, report the built size delta for an auth-only app
switching from `authTranslations` alone to `uiCoreTranslations` — e.g. `du -h` or a byte count on the
relevant JSON/object literal, or diff `dist/index.js` chunk sizes before/after a `pnpm build` if that's
cheaper than hand-measuring. A rough KB figure is enough; this number feeds `I18N-2`, not this WO's
acceptance.

### Target repo working directory (absolute)

`C:\Users\biglmi\Documents\webapps\ui-core-micha` — the repo root (no backend/frontend split here).

### Preamble

> The text above is the COMPLETE spec — the committed WO file's content, not a plan to refine; there
> is no separate plan file. Read the nearest `AGENTS.md`, the relevant `.codex/skills/<role>/SKILL.md`,
> and the app `MEMORY.md` ONLY for conventions. Stay in scope; do not touch auth/permissions/deps/
> schema/CI unless the spec says so; do not update `MEMORY.md`. **Do NOT edit `WORK_ORDERS.md` — the
> register row and the review verdicts are the orchestrator's alone.** Do NOT `git add`/`commit`/
> `push` — leave every change uncommitted in the working tree for the orchestrator's independent
> review. WRITE the tests the `Required tests` section calls for AND **RUN the tests you just wrote**
> to confirm they execute and pass — that is the ONLY test run you do (NOT the app's affected/full
> suite, NOT any review). The orchestrator re-runs the authoritative set + does the independent review
> after you finish — those are the gate; your own run does not count as the gate.
>
> Narrate continuously: a `PLAN: <step1> | <step2> | …` line up front, then a single-line
> `PROGRESS: [<n>/<total>] <present-tense action>` before every relevant action (and `… done` on
> completion), spaced so no gap exceeds ~2 min, stdout unbuffered, plus exactly one final
> `RESULT: DONE|BLOCKED <reason>`.

### Mini-handover

Repo: `ui-core-micha` (`C:\Users\biglmi\Documents\webapps\ui-core-micha`), branch `main`.
WO: `work-orders/I18N-1.md`. Target `2.36.0`. Downstream: `fitness-monitor/FM-14` is blocked on the
release and is the acceptance condition. Follow `orchestrate-codex`.

---

## C. Orchestrator only — NOT ADDRESSED TO THE IMPLEMENTER

> **If you are the implementer reading this work order as your own specification: STOP at this line.
> Everything below describes what the Orchestrator does AFTER you finish. You do none of it — no
> reviewers, no verification run, no register edit, no commit.** You ARE the invocation described
> below; do NOT shell out to `codex exec`.

### Execution directive

Implement through `codex exec` in the background — invoked directly via Bash (never the
`debugger`/`*_coder` Agent wrappers) with BOTH flags `--skip-git-repo-check` and
`--dangerously-bypass-approvals-and-sandbox`. Fallback to direct Claude implementation only on Codex
quota/rate-limit/non-zero exit — the fallback flips authorship, so an independent reviewer becomes
mandatory.

### Review routing

Independent `reviewer` **and** `ui_reviewer`, concurrent, same background batch. The `ui_reviewer`
pass is a formality here — the diff renders nothing — but Tier 3 asks for it on a frontend-touching
diff and a shared-core work order is the wrong place to argue an exemption.

### Verification

No rendered check — this work order renders nothing. The verification that matters is `FM-14`'s:
`ChartFrame`'s loading and empty states in fitness-monitor must show real text. That is this work
order's real acceptance condition even though it is executed in another repo — record in the register
Notiz that `FM-14` is the pending downstream confirmation, so the export shipping is never mistaken
for the defect being fixed.

### Register + commit

`WORK_ORDERS.md` row for `I18N-1`: `done` only once `reviewer` + `ui_reviewer` verdicts are named,
the affected-set tests pass, and the bundle-size delta is recorded in the Notiz. Commit to `main`
(this repo's trunk) on green.
