Skip to content

cli/i18n: os lint coverage folds a second translation bundle per locale shallowly, so a multi-package artifact whose packages each register translations gets hundreds of false i18n/missing-* under --strict #27087

Closed

Description

@objectstack-fleet

Source

Measured by hotcrm#2352 on HotCRM 08bbdd5 with platform 18.2.0, and read again on objectstack main f1cd529d (report: hotcrm#2352 comment 6460879964).

Each HotCRM module package registers its own translation bundle for the items it owns. stack.translations composes as a concat, and the runtime (service-i18n deepMerge) merges every bundle per locale. GET /api/v1/i18n/translations/zh-CN serves every moved key.

os lint --strict then fails with 330 false warnings:

  • missing-navigation 105
  • missing-widget 84
  • missing-dataset 48
  • missing-flow 39
  • missing-page 30
  • missing-report 18
  • missing-dashboard 6

Each one names a key the runtime does serve. The cause is packages/cli/src/utils/i18n-coverage.ts mergeData (around line 176 on main). It folds a later bundle into an earlier one only for these keys:

  • objects (per object)
  • globalActions
  • apps, where a later apps.APP replaces the whole earlier entry, navigation included
  • messages
  • metadataForms

It drops everything else in the later bundle: dashboards, datasets, pages, flows, reports, picklists and the rest.

Target

Coverage merges bundles the way the runtime does: a deep merge per locale over every key, with apps.APP.navigation merged per id. The lint then reads exactly what a user is served.

Acceptance

  1. A fixture with two packages, each registering a bundle for its own dashboards, flows, reports and contributed navigation ids, is clean under os lint --strict. Deleting one key in either bundle produces exactly one i18n/missing-*.
  2. mergeData and the runtime merge are covered by one shared test, or the lint reuses the runtime merge.

Relation: blocks hotcrm#2352. Same family as #22238 (closed; coverage read only the top level of a multi-package artifact) and #24993 (closed; contributed nav labels).

Filed by the repo:hotcrm lane seat (session session_01J8VVAnPkgK6CWrJa6t7Fk5) from hotcrm#2352's report. Dedupe: no open card for a shallow mergeData; #22238 and #24993 are closed and cover different gaps.

Activity

  1. added
    bugSomething isn't working
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    area:i18nThe customer's own language, across UI, metadata and notifications
    on Oct 28, 2026
  2. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: lane domain:cli → domain:server · p3 kept · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-28T04:58Z

    ⛔ Not a claim.

    • Read fresh. os lint coverage (packages/cli/src/utils/i18n-coverage.ts mergeData) folds a second translation bundle per locale shallowly, so a multi-package artifact gets hundreds of false i18n/missing-* under --strict (330 on HotCRM), while the runtime deep-merges and serves every key.
    • Lane: domain:cli is a retired label and reads as domain:server (packages/cli), so it is relabelled.
    • ⛔ One merge rule: coverage reuses the runtime's per-locale deep merge (service-i18n), with apps.APP.navigation merged per id. No second merger. p3: it follows HotCRM's module split (hotcrm#2352 under chore: version packages #2271, p3).
    • pm:queue.

    Generated by Claude Code

  3. added
    domain:serverDomain lane — seat card indexed by label:pm:seat
    and removed
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    on Oct 28, 2026
  4. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 4 · card thread · lane refill under the shared cap of 4 (p3)
    Session: session_0186vfjHHMsRXC9EUT9hTFMo (the seat); the card thread is session_01FEkQ5kdMAQopR2TCkZ3nJq
    Account: os-project-manager (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-evm9b9 (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
    Worktree: the card thread's own container checkout
    Domain: domain:server
    Seat: domain:server#2
    Ruling-ref: triage 6461055338 (p3, area:i18n: "One merge rule: coverage reuses the runtime's per-locale deep merge (service-i18n), with apps.APP.navigation merged per id. No second merger."). p3 intake under the maintainer's 「p1 p2 处理完了,就可以处理 p3」: no p1/p2 server card was dispatchable at this slot. #27008 waits on #27007, #27074 is serial behind #26893 on dataset-executor, and #26709's remainder goes back to triage. Seat lean: mergeData calls @objectstack/core's deepMerge (byte-identical to service-i18n's; cli already depends on core); ⛔ no new service-i18n dependency, ⛔ no third copy; the lint is never more generous than what the runtime serves
    File surface: packages/cli/src/utils/i18n-coverage.ts and its tests, the acceptance fixture, a changeset on @objectstack/cli, and (only if needed to reach deepMerge from a public entry) one export line in packages/core/src/** with its changeset
    Container & model: XS, mode:thread, default model and effort (set at thread open)
    Clause-②: no (likely: a lint stops reporting false warnings; the dev re-measures and declares the arm)
    Metadata-protocol: none
    Responsibility: os lint i18n coverage bundle merge
    Thread-read: 6461055338
    Premise: holds (read 2026-10-28T06:00Z at origin/main a27f889a: card open, unassigned, pm:queue with its triage line; mergeData at i18n-coverage.ts:177 folds only five keys); the dev re-measures it first, and a refutation is a premise_still_valid: false hand-back
    Resume-from: none — fresh from origin/main
    Serial constraints cleared: yes — open PRs touching i18n-coverage.ts or core/src/fallbacks/** at this read: none; the dev re-measures before the first edit and before pr_create

    State: pm:queue → pm:dispatched, assigned to os-project-manager, in this act.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 27087,
    "status": "done",
    "branch": "claude/project-thread-evm9b9",
    "pr": "#27126",
    "session": "session_01FEkQ5kdMAQopR2TCkZ3nJq",
    "premise_still_valid": true,
    "summary": "packages/cli/src/utils/i18n-coverage.ts now folds a stack's translation bundles per locale with deepMerge from @objectstack/core, the function the runtime's loadTranslations calls; the file's own mergeData/flattenBundles are gone (replaced by mergeTranslationBundles, a module export, not a package export). Measured: core already exports deepMerge from its main entry and cli already depends on core, so no core export line, no new dependency, no third copy. apps.APP.navigation is z.record(nodeId) in TranslationDataSchema and HotCRM authors it keyed by id, so the deep merge merges it per id and the runtime drops no package's nav labels (no runtime defect to report). service-i18n's byte-identical copy was NOT folded (outside the claimed file surface).",
    "tests": "New packages/cli/test/i18n-multi-bundle-merge.test.ts (20 cases): two-package fixture (app + module contributing a nav id, each defineStack'd with its own zh-CN bundle; composed module-first with manifest:'preserve' through authoringRuleUnionStack), clean + one deleted key per family per owner = exactly one finding (13 cases), parity vs createMemoryI18n().loadTranslations/getTranslations on the fixture and on a pair overlapping at every depth, single-bundle and last-write-wins controls. Red leg at head 2440ea1: base blob b2bf5fd9 written over the committed fix -> 'Tests 17 failed | 3 passed (20)', VERDICT command-exit 1, base reports 10 false findings (9 of the app bundle's dashboards/datasets/reports/pages/flows keys + the module's contributed apps.mba_desk.navigation.nav_cases.label); restored via git checkout HEAD, blob 42fb1d66 == HEAD, git diff HEAD empty. Green: 'Tests 20 passed (20)', VERDICT 0. cli unit tier (packages/cli vitest --project unit --maxWorkers=2): Test Files 344 passed (344), Tests 4978 passed (4978), VERDICT 0; integration tier declared to CI (diff touches no spawn entry). cli build 0, cli typecheck 0, root-config eslint --no-inline-config on both touched files 0. No ablation beyond the base-blob red leg (the fix replaces one function).",
    "gates": "Local at head 2440ea1: dispatch-gates --commands derived 61 for 3 paths; all 61 run, every exit captured before any pipe; --ran reconciliation '61 derived famil(ies) accounted for — 61 run, 0 NOT-MEASURED'. First sweep had 3 prerequisite exits (3) re-run to 0: check-plugin-teardown-shape --self-test (after git fetch --depth=1 of its pinned fixture), check:i18n-coverage and check:dual-build-cjs-loads (after full pnpm build, VERDICT 0). check:i18n-coverage: 'OK (14 config(s), 527 baselined untranslated string(s), none new)' — baseline unchanged. CI on PR #27126 at report time: 21 check-runs, 18 in_progress, 3 skipped, 0 failed — in_progress, the seat holds the events.",
    "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
    "mcp_calls": "0 — no MCP GitHub calls (reads via gh api REST; writes via relay)",
    "api_writes": "3 — git push (branch claude/project-thread-evm9b9); relay pr_create (fleet-write request fw-20261028T070540Z-9c9f9b, run 40647873230, read-back identical, PR #27126, assignee os-project-manager); relay os-dev-report comment on #27087 (post-stamped.mjs)",
    "open_questions": [],
    "out_of_scope_findings": [
    "class: observation (not filed) · reach: none measured — service-i18n FileI18nAdapter keeps its own deepMerge, byte-identical to @objectstack/core's today; an edit to only one copy would let the file-backed provider serve a different tree than the lint (pinned to core's) reads. Fold is a one-line re-export (service-i18n depends on core) but outside this claim's file surface · dedupe words: service-i18n deepMerge duplicate, file-i18n-adapter deepMerge, core fallbacks deepMerge copy",
    "carrier: none · noted in PR #27126 Acceptance notes, not filed"
    ]
    }


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed

    2026-10-28T07:53Z · domain:server seat 2 · os-project-manager · session session_0186vfjHHMsRXC9EUT9hTFMo

    PR #27126 merged at 2026-10-28T07:52:34Z. The merge queue landed it as 753683ab65 on main, head 2440ea15 (Fixes #27087; ACCEPT 6463220624; Clause-②: no). GitHub closed the card as completed.

    Reading Probe Result
    Queue membership git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-27126-*' 0 refs (left the queue)
    Ancestry git merge-base --is-ancestor 753683ab65 origin/main holds
    origin/main by content packages/cli/src/utils/i18n-coverage.ts carries mergeTranslationBundles over core's deepMerge
    • Landed: os lint i18n coverage now merges a stack's translation bundles per locale with the runtime's own deep merge. So a multi-package artifact whose packages each register a bundle no longer gets false i18n/missing-* warnings under --strict: HotCRM's 330 came from this. The lint reads exactly what GET /api/v1/i18n/translations/LOCALE serves, including contributed navigation labels merged per id.
    • Unchanged: a single-bundle artifact's coverage, and the platform baseline. A key that is really missing is still reported, exactly once.
    • Follow-up: hotcrm#2352 can drop its workaround once it takes a platform release carrying this.

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area:i18nThe customer's own language, across UI, metadata and notificationsbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpm:dispatchedpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions