You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
i18n — en source bundle, zh-CN translation, CI coverage gate #18
Duly is sold worldwide. Post-v1 — v1 is M1+M2, English only.
Scope
src/translations/en.ts (source) and zh-CN.ts, pushed into dulyTranslations. The config already declares i18n: { defaultLocale: 'en', supportedLocales: ['en', 'zh-CN'], fallbackLocale: 'en' }.
Every authored label, description, option label, validation message, action label and nav label gets a key.
A CI gate that fails on an untranslated declared label, and on a bundle key with no source.
Constraints
en is generated from the source metadata, not hand-maintained. A hand-edited en entry beats the source string silently and the served text drifts from the code under a green gate. Translated locales are hand-written; en is a copy.
Terminology decisions belong in the bundle with a comment, not in a translator's head: duty, task, period, standing, governed, caliber each carry product meaning that a literal translation loses.
Note
Moving this to v1 is a one-card decision if the first deployment is Chinese-language. Nothing else in the plan depends on the ordering.
Note from round 2 — one label needs a translator's judgement, not a mechanical swap.
due_offset_days.label on both duly_duty and duly_catalog_item is now Offset (days, 0 = anchor day) (landed in #36). It deliberately carries arithmetic inside display text, because the zero point is where a configurer will otherwise make an off-by-one that no gate can see — at catalog-import scale, a customer's whole compliance calendar shifted by a day.
That makes it harder to translate well than a plain noun: the parenthetical has to survive as an explanation, not a literal rendering. In zh-CN something closer to 「偏移天数(0 = 锚点当天)」 carries the meaning; a word-for-word translation of "anchor day" probably does not.
The same field's description carries two worked examples per anchor and should be translated for sense rather than structure.
Flagging it here rather than reworking the label — the misreading it prevents is worth more than the translation friction it costs.
Promoted to v1 by the maintainer — the first deployment is Chinese-language, so the one-card decision in the Note has been made.
Aware of two in-flight neighbours: #69 (src/email-templates/ + emailTemplates: wiring) and #52 (two duly_task fields + a dashboard tile + dataset measures). The coverage gate will discover string-bearing slots by shape, not from an enumerated list, so both land inside it without a second check. Expecting to be second; will merge main and re-run all four gates before pushing.
{
"issue": 18,
"status": "done",
"branch": "claude/issue-18-i18n-bundles",
"pr": "https://github.com/objectstack-ai/duly/pull/103",
"premise_still_valid": true,
"summary": "dulyTranslations was empty while the config advertised zh-CN, so every authored label reached a Chinese deployment in English. src/translations/en.ts now DERIVES the English bundle at config load from a walk over the metadata — there is no English literal to hand-edit, so staleness is structurally impossible rather than gate-checked — and src/translations/zh-CN.ts is hand-written, 231 keys, with the duty/task/period/standing/governed/caliber decisions recorded in the file. The gate (test/i18n-coverage.test.ts, 29 tests) discovers rather than enumerates: it visits every string leaf and demands a verdict per normalised path, so an unclassified slot is a red test. It caught #69 on merge — the exemptions for the three notify nodes' inline title/message failed as STALE and nine new paths failed the tripwire. Email templates are deliberately NOT walked (they localize by (name, locale) row, not by bundle key); the row-shaped equivalent is asserted instead, against #69's six real rows.",
"tests": "Four-gate union re-run on the FINAL commit 0a70856 (after merging main, which had taken #69): `validate=0 typecheck=0 test=0 build=0 @ 0a70856` — validate `✓ Validation passed (407ms)` with the one expected hierarchy-security warning; `Test Files 28 passed (28) / Tests 703 passed (703)`; `✓ Build complete — dist/objectstack.json (166.8 KB)`. Exit codes captured directly, never after a pipe. REVERSE VERIFICATION, three mutations, each confirmed on disk by an anchored grep count BEFORE the reading and restored by a trap on EXIT/INT/TERM: (a) deleting objects.duly_task.fields.status.label from zh-CN — count of \"label: '状态',\" 3→2 — exit 1, 2 tests red naming the key; (b) renaming duly_task_undo→duly_task_undoo in zh-CN — exit 1, 3 red including 'every zh-CN key still has a source'; (c) adding a placeholder: to duly_task.note, a slot the walk had never seen — injected-string count 0→1 — exit 1, tripwire fires naming `object.fields{}.placeholder`. Also measured the PLATFORM's half with the same mutation (b): `objectstack validate` reports it as a WARNING and exits 0 (`⚠ locale \"zh-CN\" · object \"duly_task\" · action \"duly_task_undoo\": ... Did you mean \"duly_task_undo\"?`), which is why this gate makes it an error. No ablation was run — nothing here needed a dist rebuild; the walk and the gate import source directly through vitest. BROWSER: pnpm demo on port 4318 (own port; killed by its own pgid, never by name), Chromium locale zh-CN + Accept-Language zh-CN, signed in as the demo admin. Task list, my-week, by-unit lens and the dashboard all render in Chinese; GET /api/v1/i18n/locales returns en + zh-CN and /translations/zh-CN returns the tree. Screenshots published on the throwaway branch claude/issue-18-screenshots and embedded in the PR, so no binaries enter the diff.",
"open_questions": [],
"out_of_scope_findings": [
"filed as objectstack-ai/objectstack#14253: three authored display surfaces have NO bundle key at all — bulkActionDefs (35 strings), dataset label/description/dimension/measure labels (26), and custom validation rule messages (11). All three are end-user-facing; the first two are screenshot-confirmed rendering in English inside an otherwise fully Chinese screen.",
"filed as objectstack-ai/objectstack#14254: translateAction ignores _actions.<a>.description and _actions.<a>.params.*, which TranslationItemSchema declares AND validateTranslationReferences lints — measured against the shipped resolver. A Chinese button opens an English parameter dialog.",
"filed as #102 (finding, area:i18n): the bundle half of the gate compares against 'zh-CN' by name, so a third locale added later would pass the locale-set assertion with its key coverage checked by nothing. Deliberately left out of this card; the email-template half already iterates supportedLocales generically."
]
}
Notes for the reviewer, outside the JSON
The dispatch's expected finding did not reproduce, in the good direction. The console chrome does not stay English: the platform ships its own zh-CN console bundle and picks it up from the browser locale, so 新建 / 导入 / 筛选 / 分组 / 排序 / 已选择 1 项 / 清除 are all Chinese. Nothing to file against objectui.
The coordinator's mid-task correction on email templates matched what I had already measured, so no rework: the walk never touched them, COLLECTION_HANDLING names the skip with its mechanism (so a deliberate skip cannot read like an unhandled key), and I took the unclaimed per-locale completeness check — now asserting against A flow cannot send a localizable notification — the app wires no emailTemplates barrel, so only the explicitly non-localizable path exists #69's six real rows, plus that each locale row carries its own words rather than a copy of the source row's.
One card-scope line could not be honoured as written.i18n — en source bundle, zh-CN translation, CI coverage gate #18 asks that a validation message "gets a key"; measured on objectql 17.2.0 a rule's message is emitted verbatim and messages['validation.field.*'] addresses only the platform's built-in field catalog. Recorded as a declared exemption and filed rather than faked with a key nothing reads.
Worktree removed, demo server stopped by its own pgid, ../duly-issue-18 cleaned up.
Activity
os-warren commentedon Sep 1, 2026
Note from round 2 — one label needs a translator's judgement, not a mechanical swap.
due_offset_days.labelon bothduly_dutyandduly_catalog_itemis nowOffset (days, 0 = anchor day)(landed in #36). It deliberately carries arithmetic inside display text, because the zero point is where a configurer will otherwise make an off-by-one that no gate can see — at catalog-import scale, a customer's whole compliance calendar shifted by a day.That makes it harder to translate well than a plain noun: the parenthetical has to survive as an explanation, not a literal rendering. In
zh-CNsomething closer to 「偏移天数(0 = 锚点当天)」 carries the meaning; a word-for-word translation of "anchor day" probably does not.The same field's
descriptioncarries two worked examples per anchor and should be translated for sense rather than structure.Flagging it here rather than reworking the label — the misreading it prevents is worth more than the translation friction it costs.
Generated by Claude Code
os-warren commentedon Sep 1, 2026
Claiming this card.
session_01SqkTcrxUFci7nqXdbBSe2pclaude/issue-18-i18n-bundles../duly-issue-18offmain(75d086f)Promoted to v1 by the maintainer — the first deployment is Chinese-language, so the one-card decision in the Note has been made.
Aware of two in-flight neighbours: #69 (
src/email-templates/+emailTemplates:wiring) and #52 (twoduly_taskfields + a dashboard tile + dataset measures). The coverage gate will discover string-bearing slots by shape, not from an enumerated list, so both land inside it without a second check. Expecting to be second; will mergemainand re-run all four gates before pushing.Generated by Claude Code
Generated by Claude Code
os-warren commentedon Sep 1, 2026
os-dev-report
{ "issue": 18, "status": "done", "branch": "claude/issue-18-i18n-bundles", "pr": "https://github.com/objectstack-ai/duly/pull/103", "premise_still_valid": true, "summary": "dulyTranslations was empty while the config advertised zh-CN, so every authored label reached a Chinese deployment in English. src/translations/en.ts now DERIVES the English bundle at config load from a walk over the metadata — there is no English literal to hand-edit, so staleness is structurally impossible rather than gate-checked — and src/translations/zh-CN.ts is hand-written, 231 keys, with the duty/task/period/standing/governed/caliber decisions recorded in the file. The gate (test/i18n-coverage.test.ts, 29 tests) discovers rather than enumerates: it visits every string leaf and demands a verdict per normalised path, so an unclassified slot is a red test. It caught #69 on merge — the exemptions for the three notify nodes' inline title/message failed as STALE and nine new paths failed the tripwire. Email templates are deliberately NOT walked (they localize by (name, locale) row, not by bundle key); the row-shaped equivalent is asserted instead, against #69's six real rows.", "tests": "Four-gate union re-run on the FINAL commit 0a70856 (after merging main, which had taken #69): `validate=0 typecheck=0 test=0 build=0 @ 0a70856` — validate `✓ Validation passed (407ms)` with the one expected hierarchy-security warning; `Test Files 28 passed (28) / Tests 703 passed (703)`; `✓ Build complete — dist/objectstack.json (166.8 KB)`. Exit codes captured directly, never after a pipe. REVERSE VERIFICATION, three mutations, each confirmed on disk by an anchored grep count BEFORE the reading and restored by a trap on EXIT/INT/TERM: (a) deleting objects.duly_task.fields.status.label from zh-CN — count of \"label: '状态',\" 3→2 — exit 1, 2 tests red naming the key; (b) renaming duly_task_undo→duly_task_undoo in zh-CN — exit 1, 3 red including 'every zh-CN key still has a source'; (c) adding a placeholder: to duly_task.note, a slot the walk had never seen — injected-string count 0→1 — exit 1, tripwire fires naming `object.fields{}.placeholder`. Also measured the PLATFORM's half with the same mutation (b): `objectstack validate` reports it as a WARNING and exits 0 (`⚠ locale \"zh-CN\" · object \"duly_task\" · action \"duly_task_undoo\": ... Did you mean \"duly_task_undo\"?`), which is why this gate makes it an error. No ablation was run — nothing here needed a dist rebuild; the walk and the gate import source directly through vitest. BROWSER: pnpm demo on port 4318 (own port; killed by its own pgid, never by name), Chromium locale zh-CN + Accept-Language zh-CN, signed in as the demo admin. Task list, my-week, by-unit lens and the dashboard all render in Chinese; GET /api/v1/i18n/locales returns en + zh-CN and /translations/zh-CN returns the tree. Screenshots published on the throwaway branch claude/issue-18-screenshots and embedded in the PR, so no binaries enter the diff.", "open_questions": [], "out_of_scope_findings": [ "filed as objectstack-ai/objectstack#14253: three authored display surfaces have NO bundle key at all — bulkActionDefs (35 strings), dataset label/description/dimension/measure labels (26), and custom validation rule messages (11). All three are end-user-facing; the first two are screenshot-confirmed rendering in English inside an otherwise fully Chinese screen.", "filed as objectstack-ai/objectstack#14254: translateAction ignores _actions.<a>.description and _actions.<a>.params.*, which TranslationItemSchema declares AND validateTranslationReferences lints — measured against the shipped resolver. A Chinese button opens an English parameter dialog.", "filed as #102 (finding, area:i18n): the bundle half of the gate compares against 'zh-CN' by name, so a third locale added later would pass the locale-set assertion with its key coverage checked by nothing. Deliberately left out of this card; the email-template half already iterates supportedLocales generically." ] }Notes for the reviewer, outside the JSON
zh-CNconsole bundle and picks it up from the browser locale, so新建/导入/筛选/分组/排序/已选择 1 项/清除are all Chinese. Nothing to file againstobjectui.COLLECTION_HANDLINGnames the skip with its mechanism (so a deliberate skip cannot read like an unhandled key), and I took the unclaimed per-locale completeness check — now asserting against A flow cannot send a localizable notification — the app wires noemailTemplatesbarrel, so only the explicitly non-localizable path exists #69's six real rows, plus that each locale row carries its own words rather than a copy of the source row's.objectql17.2.0 a rule'smessageis emitted verbatim andmessages['validation.field.*']addresses only the platform's built-in field catalog. Recorded as a declared exemption and filed rather than faked with a key nothing reads.../duly-issue-18cleaned up.Generated by Claude Code
Generated by Claude Code
Ship the zh-CN bundle and a coverage gate that discovers (#103)