Skip to content

i18n — en source bundle, zh-CN translation, CI coverage gate #18

Closed

Description

@os-warren

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.
  • No display text hard-coded in a hook, flow, action handler or job. Notification bodies included — Lead-time reminders, overdue escalation and stagnation alerts #11's digests are user-facing text.
  • 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.

Activity

  1. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    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.


    Generated by Claude Code

  2. self-assigned this
    on Sep 1, 2026
  3. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    Claiming this card.

    • Session: session_01SqkTcrxUFci7nqXdbBSe2p
    • Branch: claude/issue-18-i18n-bundles
    • Worktree: ../duly-issue-18 off main (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 (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.


    Generated by Claude Code


    Generated by Claude Code

  4. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    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

    • 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.

    Generated by Claude Code


    Generated by Claude Code

  5. added a commit that references this issue on Sep 1, 2026
    7624c99
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions