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 {
})}