feat(i18n): editor translation runtime, English catalog and Accept-Language resolution #229
No reviewers
Labels
No labels
bug
enhancement
needs-triage
ready-for-agent
ready-for-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/deckle!229
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "afk/225"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #225
What
web/src/i18n/): a hand-writtent(key, params)reads flat JSON catalogs through a Solid locale signal.key.one/key.other, chosen withIntl.PluralRules.{name},{name|q},{name|list}.I18nProvider/useI18nprovide the language to components;tRich(inrich.tsx) renders one sentence around an inline JSX element.en.json(1171 keys) includes oneserver.<code>per code in the exported registry from #224.glossary.mdrecords the register rule (impersonal; formal "Sie") for the German issue.GET /api/sitenow returnsacceptLanguage, the header parsed into tags ordered by q. It is[]when the header is absent, and Go does no matching.boot()picks the firstacceptLanguagetag that has a catalog (exact tag, then primary subtag), then triesnavigator.languages, then English.<html lang>anddocument.title, and only then renders. There is no flash of English.serverError,looseFormErroranddecodeFieldErrorstranslate bycode/args. When the catalog has no entry for a code, they show the server's English text.reason, the tree root title, form and filterlabelCodes, and badgeunavailable.t(), and no hand-made plurals remain.[Translate to <label>:]content marker stay as written.scaffold/site/AGENTS.md: the Languages section now covers the editor's own language.setup-siteskill: an interview line asking which language the customer's editors work in.verify-siteskill: the smoke test now says to check the editor in the customer's language.Tests
i18nParity: every literalt()/tRich()key exists in every catalog, and no catalog key is unused..one/.other.server.*entry, and its English equals the registry template.i18nLeak: an AST scan of the.tsxsources for JSX text andtitle/placeholder/aria-label/altliterals. The allowlist has one entry, "Deckle".i18nShell: a smoke test that boots the full shell under a fixturezzcatalog.html[lang], the title, a translated rail label, the English fallback for a missing key, and a translated server error.Accept-Language: dewithout a German catalog renders English, and that the browser-language fallback works.i18n/i18nRich: unit tests for the runtime. Go: tests for theacceptLanguageTagsparser and the handler.acceptLanguagefield, no existing assertion changed.Verified locally:
tsc --noEmit,npm run build,vitest run(116 files, 2846 tests), andgo vet ./...plusgo test ./...all pass.<button>Save</button>makes the leak check fail and name the file and line.common.savefromen.jsonmakes the parity check fail and name the key.harness serve,curl -H 'Accept-Language: de-AT,de;q=0.9,en;q=0.8' /api/sitereturns"acceptLanguage":["de-AT","de","en"], and the same request without the header returns[].Follow-ups / notes
errorsmap (formDescription.errors) carries no codes on the wire, so it stays English for now.joinNaturalkeeps "A, B and C" (no serial comma) through{items|list} {conj} {last}keys.Intl.ListFormatwould change the English.🤖 Generated with Claude Code
[autoland] verdict: pass
Verdict: CONCERNS (non-blocking). Approved, but not auto-merged, so a human can look at the two concerns below.
CI:
labctl pr checks 229issuccess.ci / build-and-testpassed in 15m52s:npm ci && vite build && vitest run(including i18nParity, i18nLeak and i18nShell), thengo vet ./...andgo test ./...(including the server tests and the scaffold drift testsscaffold/skills_test.goandinternal/element/agentsdoc_test.go). CI does not runtsc --noEmit; the PR says the author ran it locally.Conventions:
Closes #225.scaffold/site/AGENTS.md"Languages", and thesetup-siteandverify-siteskills are updated as the issue asked.main, so there is no conflict.What was checked: all 990
t()/tRich()call sites. Each key exists inen.json, and the params match the placeholders. Every removed literal was compared with its catalog value, and every plural with its oldn === 1condition. Apart from the two items below, the English output is unchanged.Concerns
Dates now always use US English format. This affects
web/src/widgets/controls.tsx:723(DateTimeReadonly) andweb/src/components/SourceValue.tsx:225.d.toLocaleString()becameformatDateTime(d), which formats in the UI locale.resolveLocalereturns the catalog code, which is always"en"today. The old code used the browser's locale.05/01/2026, 13:04:05becomes1/5/2026, 1:04:05 PM5.1.2026, 13:04:05becomes1/5/2026, 1:04:05 PMen-GB) as the formatting locale while using theencatalog.web/src/elementcrud.ts:532:compareTextpasses"en"wherelocaleCompare(b, undefined, …)used the browser's collation.The upload hint lost its bold folder name. This is at
web/src/components/FileBrowser.tsx:592.Uploads go to <b>media/{DEFAULT_UPLOAD_FOLDER}</b>t("files.uploadHint", { folder: … }), which renders the path as plain text.tRich("files.uploadHint", undefined, { folder: () => <b>media/…</b> }), as the other sentences with elements do.Minor notes (not blocking)
formatListusesIntl.ListFormat, which throws on a non-string item where.join(", ")did not (controls.tsx~714/764). The server converts multiselect values to strings, so this is likely unreachable.buildFolderTreereads the root label "Media" throught()once, outside any reactive scope, so it won't update if the locale changes mid-session. The locale is fixed at boot, so this has no effect today.Addressed both review concerns in
1297f00.1. Dates in US format for every non-US English browser — fixed. The runtime now carries a second signal, the formatting locale, which the Intl formatters (dates, relative times, lists,
{x|list}, collation) run under while lookups keep using the catalog code.resolveLocalereturns{code, formatLocale}: the formatting locale is the first Accept-Language tag, then browser tag, in the chosen language that names a region, canonicalized by Intl (en-GB); the chosen language itself when there is none. Two details beyond the suggested "keep the matched tag":Accept-Language: en), so the matched tag alone would still have given a British user US dates. The resolver therefore looks past the matched tag to the first regional tag of that language, which is the browser'sen-GBon hosted.en-!!) falls back to the code instead of making every date throw.The deliberate call: a German browser without a German catalog now gets English dates with its English interface (
1/5/2026, 1:04:05 PM), wheretoLocaleString()printed5.1.2026, 13:04:05before. That matches the issue's "dates in the UI locale" and keeps the English UI internally consistent; it is one line in the README ("Dates, lists and sorting") if it should go the other way.compareTextfollows the same rule.2. Upload hint lost its bold folder — fixed with
tRichand a<b>slot; the MediaPicker test now checks the bold part.The minor notes stand as they are:
formatListonly ever seesstring[](FormValue), and the locale is fixed at boot sobuildFolderTree's one-timet()is correct today.Verified:
tsc --noEmit,vitest run(116 files, 2851 tests),go test ./scaffold/... ./internal/element/....[autoland] verdict: pass
Verdict: PASS.
Signal relied on:
labctl pr checks 229issuccesson the head commit1297f00(ci / build-and-test, 16m16s: web build, vitest incl. i18nParity/i18nLeak/i18nShell,go vetandgo test ./...incl. the scaffold drift tests). Nothing re-run.Conventions: Conventional Commits title; body carries
Closes #225; README,scaffold/site/AGENTS.md"Languages", and thesetup-site/verify-siteskills are updated as the issue requires. Head is at the currentmain, no conflict.Diff: autoland's review of the first two commits (all
t()/tRich()call sites) stands. The fix commit1297f00resolves both of its concerns: a separate formatting locale (web/src/i18n/translate.tsresolveLocale→{code, formatLocale}) keeps en-GB/en-AU/en-IN day-first dates and 24-hour times while lookups stay on the catalog code, an Intl-rejected tag falls back to the code instead of throwing, and the upload hint istRichwith its<b>slot again (web/src/components/FileBrowser.tsx:593). Unit, shell and MediaPicker tests cover each case, including the hosted bare-enheader with anen-GBbrowser.Note (non-blocking): a German browser without a German catalog now gets US-English dates with the English UI, where
toLocaleString()printed German dates before. This is the author's deliberate call, documented in the README under "Dates, lists and sorting", and matches the issue's "dates in the UI locale".