From cb9983e9a741cbf06bde315d40b8931982d6b6ba Mon Sep 17 00:00:00 2001 From: Marcel Wege Date: Wed, 19 Aug 2026 17:26:58 +0200 Subject: [PATCH 1/2] =?UTF-8?q?fix(#687):=20triage=20the=20I3=20literal=20?= =?UTF-8?q?tail=20=E2=80=94=2021=20sites,=207=20files=20on=20the=20ratchet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-on to #747, which made the category measurable. This spends that measurement: it works the `translate` list down from 40 to 19 and puts six more files on the per-file ratchet. Two scanner rules, both learned from the triage rather than guessed: 1. Version, phase and build stamps are `diagnostic`, not prose. `omadia · v1`, `B.0 Draft-Store`, `Phase B.5 (Workspace-UI)` and `omadia · v1 · Slice 1.1` sit in product footers and read like text, but localising them changes an identifier an operator quotes in a bug report. (One of them already had a translated LABEL — `t('footer.phaseLabel')` — with the value left hardcoded beside it.) Whether internal roadmap vocabulary belongs in a shipped footer at all is a product question, flagged rather than answered here. 2. Every `placeholder=` is a judgement call, not only the one-word ones. Of six hits, five were example VALUES that must not be translated (`Release sign-off`, `Release approver`, `id-1, id-2, …`, `email | uri | date-time | uuid`) and one was a real instruction (`Reason (optional)`). Nothing in the syntax separates them, so multi-word placeholders now land in `review`. The `diagnostic` bucket was checked in the dangerous direction: it contains exactly those four stamps and nothing legitimate. 27 keys across six files. The judgement calls worth naming: `block`/`boost` are actions an operator picks, so by GLOSSARY.md they are German (`blockieren`/`hervorheben`) while their `value` attributes stay; `Vault` and `Bucket` are product/loan nouns that stay English but are now catalogue keys with explicit allowlist reasons rather than implicit exemptions. They would have passed silently either way — `i18n-validate` exempts short capitalised nouns by design, a gap its own comment names as a follow-up. Verification: i18n:check OK (3760 keys), tsc clean, eslint clean, 742/742 vitest. Mutation check: restoring one literal turns exactly the new per-file ratchet red. Remaining: 19 `translate`, most of them sentences split across JSX elements (`rendered markdown (`, `Stream live ·`, `s tool-time`) that need ICU restructuring rather than a 1:1 key — deliberately left for a separate pass. --- web-ui/app/_lib/i18n-structural.test.ts | 10 ++++- web-ui/app/admin/duplicates/[id]/page.tsx | 8 ++-- .../admin/duplicates/excerpt/[id]/page.tsx | 8 ++-- .../app/admin/inconsistencies/[id]/page.tsx | 10 ++--- web-ui/app/admin/kg-priorities/page.tsx | 18 ++++---- web-ui/app/graph/_components/GraphCanvas.tsx | 8 ++-- .../system/_components/VaultStatusCard.tsx | 8 ++-- web-ui/messages/de.json | 42 ++++++++++++++++--- web-ui/messages/en.json | 42 ++++++++++++++++--- web-ui/scripts/i18n-identical-allowlist.json | 7 +++- web-ui/scripts/i18n-literal-scan.mjs | 28 ++++++++++--- 11 files changed, 140 insertions(+), 49 deletions(-) diff --git a/web-ui/app/_lib/i18n-structural.test.ts b/web-ui/app/_lib/i18n-structural.test.ts index 8411e0980..49533b3cc 100644 --- a/web-ui/app/_lib/i18n-structural.test.ts +++ b/web-ui/app/_lib/i18n-structural.test.ts @@ -130,7 +130,15 @@ describe('#679 / I3 — swept components carry no user-facing literals', () => { // So the ratchet now asks the scanner the same question the CLI asks: // does this file still contain a user-facing literal? Files join `SWEPT` // once they answer no, and can never silently regress afterwards. - const SWEPT_COMPONENTS = ['graph/_components/ListView.tsx']; + const SWEPT_COMPONENTS = [ + 'graph/_components/ListView.tsx', + 'graph/_components/GraphCanvas.tsx', + 'admin/duplicates/[id]/page.tsx', + 'admin/duplicates/excerpt/[id]/page.tsx', + 'admin/inconsistencies/[id]/page.tsx', + 'admin/kg-priorities/page.tsx', + 'system/_components/VaultStatusCard.tsx', + ]; for (const file of SWEPT_COMPONENTS) { it(`${file} has no untranslated user-facing literal`, () => { diff --git a/web-ui/app/admin/duplicates/[id]/page.tsx b/web-ui/app/admin/duplicates/[id]/page.tsx index 187568b64..2ffeda3d7 100644 --- a/web-ui/app/admin/duplicates/[id]/page.tsx +++ b/web-ui/app/admin/duplicates/[id]/page.tsx @@ -124,11 +124,11 @@ export default function DuplicateDetailPage(): React.ReactElement { })}

-
Status
+
{t('status')}
{detail.props.status}
{detail.props.resolution && ( <> -
Resolution
+
{t('resolution')}
{detail.props.resolution}
)} @@ -136,8 +136,8 @@ export default function DuplicateDetailPage(): React.ReactElement {
- - + +
{detail.props.status === 'open' && ( diff --git a/web-ui/app/admin/duplicates/excerpt/[id]/page.tsx b/web-ui/app/admin/duplicates/excerpt/[id]/page.tsx index d4de33a37..9295be094 100644 --- a/web-ui/app/admin/duplicates/excerpt/[id]/page.tsx +++ b/web-ui/app/admin/duplicates/excerpt/[id]/page.tsx @@ -122,11 +122,11 @@ export default function ExcerptDuplicateDetailPage(): React.ReactElement { })}

-
Status
+
{t('status')}
{detail.props.status}
{detail.props.resolution && ( <> -
Resolution
+
{t('resolution')}
{detail.props.resolution}
)} @@ -135,12 +135,12 @@ export default function ExcerptDuplicateDetailPage(): React.ReactElement {
diff --git a/web-ui/app/admin/inconsistencies/[id]/page.tsx b/web-ui/app/admin/inconsistencies/[id]/page.tsx index 22efaaa9c..561054c20 100644 --- a/web-ui/app/admin/inconsistencies/[id]/page.tsx +++ b/web-ui/app/admin/inconsistencies/[id]/page.tsx @@ -118,13 +118,13 @@ export default function InconsistencyDetailPage(): React.ReactElement {

{detail.props.summary}

-
Severity
+
{t('severity')}
{detail.props.severity}
-
Status
+
{t('status')}
{detail.props.status}
{detail.props.resolution && ( <> -
Resolution
+
{t('resolution')}
{detail.props.resolution}
)} @@ -132,8 +132,8 @@ export default function InconsistencyDetailPage(): React.ReactElement {
- - + +
{detail.props.status === 'open' && ( diff --git a/web-ui/app/admin/kg-priorities/page.tsx b/web-ui/app/admin/kg-priorities/page.tsx index b33dba097..a92b8e68a 100644 --- a/web-ui/app/admin/kg-priorities/page.tsx +++ b/web-ui/app/admin/kg-priorities/page.tsx @@ -146,7 +146,7 @@ export default function KgPrioritiesPage(): React.ReactElement {

- Knowledge-Graph Priorities + {t('title')}

{t('intro')} @@ -194,8 +194,8 @@ export default function KgPrioritiesPage(): React.ReactElement { onChange={(e) => { setDraftAction(e.target.value as 'block' | 'boost'); }} className="lg:col-span-2 rounded-md border border-[color:var(--border)] bg-[color:var(--bg)] px-3 py-2 text-sm text-[color:var(--fg-strong)]" > - - + + { setDraftReason(e.target.value); }} - placeholder="Reason (optional)" + placeholder={t('reasonPlaceholder')} className="lg:col-span-3 rounded-md border border-[color:var(--border)] bg-[color:var(--bg)] px-3 py-2 text-sm text-[color:var(--fg-strong)]" />