ADORSYS-GIS / ADORSYS-GIS/converse-frontends

[Ticket]: finish the console i18n cutover — take the hard-coded-copy ratchet from 28 to 0

Open
#490 0 comments 0 reactions 1 assignee Claimed by @stephane-segning View on GitHub
ticket
Dominant language
TypeScript
Stars
0
Forks
0
Avg merge
1h 49m
Merged PRs (30d)
253

Description

### Type

Feature

### Summary

We need to finish translating the console, because ADR 0017 translated the shell, `/settings/*`,
`/admin/*`, the declarative dashboards and the report templates — and stopped there. A German
operator today reads a German rail and a German admin area, then lands on the account overview and
reads English.

Expected result:

> Every string a person reads in `apps/console` and in the screens `packages/ui-web` draws for it
> resolves through the `en`/`de` bundles, and
> `apps/console/src/i18n-hardcoded-copy.test.ts`'s pinned count is **0** with an allow-list that
> contains only strings no person ever reads.

### Intent

ADR 0017 chose to MEASURE what was left rather than claim completeness: the ratchet counts the
remaining hard-coded user-visible strings and pins the number, so the gap is a checkable fact and
the next English screen someone writes is a build failure. This ticket closes that gap.

The intent is not "translate more words". It is that the console has one answer to "where does copy
live" — the bundles — so a reviewer never has to ask whether a given screen is the translated kind.

### Source of truth (links)

- The PR that established the framework and left this measured:
https://github.com/ADORSYS-GIS/converse-frontends/pull/489
- `docs/adr/0017-i18n-app-router-i18next.md` — "What is not translated yet"
- Owner directive, 2026-09-03: *"I wanna introduce i18n (next-i18next) with English first, German
second."*
- Epic: https://github.com/ADORSYS-GIS/converse-frontends/issues/443

### Current Behavior

`pnpm --filter console exec vitest run src/i18n-hardcoded-copy.test.ts` passes at a pinned count of
**28**. Those 28, by area:

**1. The account area** — `/accounts//overview` and `/accounts//api-keys`

- `app/(console)/accounts/[accountId]/overview/loading.tsx` — `Overview`, `loading scope…`, `Budget`
- `app/(console)/accounts/[accountId]/api-keys/loading.tsx` — `API keys`, `loading scope…`
- `containers/overview-centre.tsx` — the Budget card's label (its range vocabulary is already
translated)
- `containers/api-keys-centre.tsx` — `API keys`
- `containers/api-key-rows.ts` — `7 days`
- `containers/use-account-overview-zones.ts` — `Spend this period`, `Budget remaining`,
`Active API keys`, `Projects`, `Project`, `All projects`, and two captions
(`Spend since the last reset could not be read.`, `Next reset unknown — …`)
- `containers/use-api-keys-screen.ts` — `New API key`, `Project`, `All projects`

**2. The settings account drill-down**

- `containers/projects-centre.tsx` — `Projects`
- `containers/refill-centre.tsx` — `Request a budget refill`
- `containers/use-refill-screen.ts` — the no-amount caption
- `containers/use-account-detail-screen.ts` — `Could not load the budget ceiling.`

**3. `/settings/info`'s backend probes**

- `containers/use-build-info.ts` — `GET /version, server-side`, `This service reported nothing.`

**4. The dashboard engine's row vocabulary**

- `dashboards/panel-adapters.tsx` — `Unassigned` (×3 call sites), `Previous period`, and the delta
wording (`new this period`, `no change`, `N% vs previous month`) built from
`containers/comparison-window.ts`'s `comparisonLabel`.
This one is visible today as an inconsistency, not just a gap: several translated `dashboards`
subtitles already say *„Nicht zugeordnet“* while the ROW itself renders `Unassigned`.

**5. Not in the 28, because it composes a sentence rather than labelling a prop**

- `containers/budget-period-caption.ts` (landed in #479) — the budget-period line
`/admin/overview`, `/accounts//overview` and the settings account lens all render. It
assembles a cadence sentence, a relative *"next in 6 h"* phrase and a per-mode tick clause.

**6. Outside `apps/console`** — `packages/ui-web`'s own English defaults, which no ratchet in
`apps/console` can see: `LedgerTable`'s `Actions` header, `SessionLedger`/`ReviewQueue`/
`PlatformRoleGrants`/`ProjectsLedger`'s column headers, `RuleSetForm` and `BudgetScheduleForm`'s
field labels, `scaleAxisCaption`, `RESET_SCHEDULE_ENFORCEMENT_CAPTION`, `NO_QUOTA_TIER_LABEL`.

### Expected Behavior

- The ratchet's `REMAINING_HARDCODED_COPY` is `0`.
- `packages/ui-web` still owns no translations (ADR 0017 D3): its remaining defaults move to props
the console fills, or — where a prop would have to be threaded through every intermediate section
— to `UiCopy`, which grows.
- `panel-adapters.tsx` and `comparison-window.ts` take the caller's `t`, the same way
`rangeLabels`, `budgetPressureTruncation` and `familyTruncationCap` already do.
- `budgetPeriodCaption` produces a translated sentence whose CLAUSE ORDER can differ per locale —
German would not want the same slots in the same places, so an interpolated template with three
`{{placeholders}}` is probably the wrong shape and this deserves a decision rather than a
mechanical port.

### Acceptance Criteria

- [ ] Given a session with `lb.locale=de`, when I open `/accounts//overview`,
`/accounts//api-keys`, `/settings/accounts//projects` and
`/settings/accounts//request-refill`, then every visible string is German.
- [ ] Given a German session, when a dashboard row has no attributed project or model, then it
reads *„Nicht zugeordnet“* — matching the panel subtitle above it.
- [ ] Given a German session, when a stat card shows a comparison, then the delta reads in German
(`12 % gegenüber dem Vormonat`).
- [ ] Given a German session, when any ledger renders, then its column headers are German.
- [ ] `REMAINING_HARDCODED_COPY` is `0`, and `ALLOWED` contains only files whose strings a person
never reads (each with its reason, as today).
- [ ] `i18n/resources.test.ts` still passes: `en`/`de` parity, no empty values, identical
`{{placeholders}}`.
- [ ] `packages/ui-web` renders identically in Storybook with no provider mounted — English
defaults intact.
- [ ] Error cases are handled safely (a missing key renders as the key, never as blank).
- [ ] Existing behavior is not broken.
- [ ] Relevant tests are added or updated.
- [ ] Verification evidence is provided.

### Out of Scope

- A third locale, and any translation-management tooling or vendor workflow.
- RTL support.
- Translating STORED data — account names, project names, model ids, `azp` values. Those are facts
in a database read by everyone with access to the account, in whatever language each of them
reads; bending them to a browser preference is the opposite of correct (see
`DEFAULT_PROJECT_NAME`'s doc comment for the same argument made once already).
- Re-opening ADR 0017's decisions: the cookie (D2), the ui-web copy contract (D3), keys in
`dashboards.yaml` (D4), USD in both languages (D5).

### Technical Context

- `apps/console/src/i18n/` — `config.ts` (locales, namespaces, `Translate`), `server.ts`
(per-request instance), `client.tsx` (`useTranslation`, `useIntlLocale`), `resources.ts` (the
explicit importer table — a namespace added without a file is a type error).
- `apps/console/locales/{en,de}/*.json` — `en` is the source of truth; `de` must mirror it exactly.
- `packages/ui-web/src/lib/copy.tsx` — `UiCopy`, `CopyProvider`, `useCopy`, `fillCopy`. Filled by
`apps/console/src/client/console-copy.tsx`.
- The established pattern for a pure module that used to return English: return the FACT, put the
sentence in the bundle. `familyTruncationCap` and `budgetPressureTruncation` are the two worked
examples.
- `packages/ui-web/src/pages-stories/i18n-german.stories.tsx` is the `de` review surface, and reads
the shipped bundles rather than retyped fixtures.

### Risks

- **Threading `t` through `panel-adapters.tsx` touches its 69 tests.** Mitigation: the same
`englishT()` helper the nav tests already use resolves against the shipped bundle, so those tests
keep asserting real English rather than a `(key) => key` stub.
- **Growing `UiCopy` invites it to become a second bundle.** Mitigation: ADR 0017 D3's rule is the
gate — the context is only for copy baked into a PRIMITIVE's own behaviour; anything a screen
decides stays a prop.
- **German phrasing drifting between screens.** Mitigation: the domain vocabulary is fixed (Konto,
Projekt, API-Schlüssel, Guthaben, Aufladung, Sitzung, Kanal, Nutzung) and formal *Sie* throughout;
a reviewer reads `locales/de/*.json` as a whole, not per diff hunk.

### Test Plan

```bash
pnpm -r typecheck
pnpm --filter console test # the ratchet + the parity test
pnpm --filter @lightbridge/ui-web test
pnpm --filter @lightbridge/ui-web build-storybook
pnpm --filter console build:web
```

Expected: the ratchet reports `0`, parity passes, Storybook's English stories are unchanged, and the
`Pages/I18n Deutsch` stories show no English left in the chrome, the ledgers or the panel rows.

Manual: set `document.cookie = 'lb.locale=de; Path=/'`, reload, and walk every route in
`sitemap.ts`.

### Verification evidence

To be provided by the implementing PR. At minimum: the ratchet output showing `0`, the
`pnpm -r test` tail, and `Pages/I18n Deutsch` screenshots at both themes covering the account
overview and one ledger.

### Human accountable owner

@stephane-segning

### AI Usage Declaration

Generating code, Generating tests, Drafting documentation

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.