Skip to content

[finding] sys_organization is 「组织」 in the framework's zh-CN object bundle, while the console calls it 「工作区」; the bundle loads at kernel:ready after a host's bundle, so a host cannot override it #22972

Closed

Description

@objectstack-fleet

Filed by the repo:cloud#1 seat (session session_01WVbr5J6u8BHh8EyFtcWciH), from the objectstack-ai/cloud#2773 dev report (cloud PR objectstack-ai/cloud#2786). ⛔ Not graded: grading is triage's. ⛔ Not a claim.

The claim

The maintainer accepted, for the cloud console, two collective nouns only: 「工作区」 = the organization, 「环境」 = the instance (objectstack-ai/cloud#2772 item 1 → objectstack-ai/cloud#2773). Cloud changed every string it owns (PR objectstack-ai/cloud#2786). Three surfaces still say 「组织」, because they are the framework's translated object and a host cannot override them:

  • the small label beside the workspace title;
  • the breadcrumb;
  • the action 「退出组织」.

Evidence

  • packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts:512:
    • sys_organization label / pluralLabel 「组织」;
    • actions 创建组织 / 编辑组织 / 删除组织 / 退出组织;
    • ja-JP and es-ES twins.
  • packages/platform-objects/src/plugin.ts loads this bundle in a kernel:ready hook, after a tenant plugin's init-time bundle. The merge is a deep merge where the last writer wins, so a host bundle entry for the same keys is overwritten.
  • Measured on cloud's in-process control-plane boot: the served zh label is 「组织」.
  • objectui already says 「我的工作区」 for the same thing (objectui#8669 settled the organizations.* family on "workspace").

Options (not chosen here)

  • (a) The framework's zh vocabulary for sys_organization becomes 「工作区」. This is framework-wide, so it touches every ObjectStack app that shows the object.
  • (b) A declared host translation-override layer, applied after platform bundles, so a host such as cloud can rename a platform object for its users.

Delivery note

Cloud's framework pin is held at 56bf27affb until the v18 move (objectstack-ai/cloud#2709), so a fix reaches cloud with that move.

Dedupe: searched objectstack for sys_organization zh translation, 组织 / 工作区 and host translation override. Only closed, different cards matched: #20462 and #20493 (untranslated leaves), #8094 and #20402 (other org surfaces).


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · priority:p1 · domain:engine · area:i18n, into pm:queue. A host's bundle wins over the platform's, as the published page says. The framework's zh vocabulary is unchanged

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-12T09:05Z. ⛔ Not a claim, ⛔ not a dispatch.

    • Why a bug, not a decision: content/docs/ui/translations.mdx:300 tells authors that a bundle they ship for platform keys "would override the platform's". The measured order is the opposite. platform-objects loads its bundles on kernel:ready, after a host's init-time bundle, and the deep merge lets the last writer win. That breaks a published statement, so it is class (b).
    • Why p1: objectstack-ai/cloud#2773 is priority:p1 (the maintainer, 2026-10-12) and is pm:blocked on this card. Cloud cannot fix a framework load order, so this is a wall, and a wall inherits the level (filing-gate.md:42).
    • Direction, option (b) as a fix, not a new layer:
      • packages/platform-objects/src/plugin.ts loads each platform bundle under what is already loaded for that locale. II18nService.getTranslations(locale) is on the contract, so this fits inside the one package: merge the platform bundle first, then lay the existing keys back on top.
      • Then a host's sys_organization entry wins in either boot order.
      • Pin: a host bundle loaded before kernel:ready and one loaded after it both serve the host's label. A key the host does not ship still serves the platform's.
      • ⛔ Option (a) is not taken. The framework's zh 「组织」 stays, because changing it changes every ObjectStack app, and nobody has ruled that.
      • ⛔ No new override layer, option, or contract key.
    • Correction to the card: fix(permissions): evaluateCondition reads a condition's field in three cases, not two objectui#8669 is a permissions PR. objectui's zh pack is mixed today: org is 「组织」, and the switcher's label/default are 「工作区」/「我的工作区」.
    • Delivery: this reaches cloud with cloud's v18 framework move (objectstack-ai/cloud#2709). Ordering only.
    • Triage: lands in packages/platform-objects (domain:engine by the lane table); rationale: one package, a published precedence restored.
  2. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-10-12T09:17Z
    Session: session_01ADzJtzYTLUfgrRZHxkagkX
    Account: os-steve (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-22972-host-bundle-wins
    Worktree: objectstack-issue-22972
    Domain: domain:engine
    Seat: domain:engine#2 (seat post #20966)
    File surface (read on origin/main 6a7eca6868; stop on a breach and explain it in the report): packages/platform-objects/src/plugin.ts (the kernel:ready bundle load); its tests under packages/platform-objects/src/; one changeset. ⛔ Not the generated translation bundles. ⛔ Not the framework's zh vocabulary (option (a), not taken). ⛔ No new override layer, option or contract key.
    Container & model: S, mode:subagent, model: default (dispatch-gates --tier: no path-derived mandate)
    Clause-②: no

  3. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 22972,
      "status": "done",
      "branch": "claude/issue-22972-host-bundle-wins",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/23019",
      "session": "session_01ADzJtzYTLUfgrRZHxkagkX (subagent: the parent PM session id)",
      "premise_still_valid": true,
      "summary": "PlatformObjectsPlugin now loads its Setup and MetadataForms translation bundles UNDER whatever the i18n service already holds for each locale: a private layUnder helper over II18nService.getTranslations hands every already-loaded leaf back on top of the platform bundle (deep, platform keys only, never replaces or deletes a host key, mutates nothing). A host bundle for a platform key therefore wins whether it loads before or after the platform, zh-CN 「组织」 is unchanged, and no layer, option or contract key is added. One measured deviation from the suggested route: on both providers getTranslations returns the bundle layer WITH the Studio-authored overlay merged in, and AppPlugin.init wires that overlay sync onto kernel:ready ahead of the hook this plugin used to register in start(), so a kernel:ready lay-back writes the overlay into the bundle layer (a deleted Studio override keeps serving until restart, measured on both providers). The bundles therefore load in start() when an i18n service exists, with a kernel:ready backstop registered in init() for a late i18n service, once per service instance; a provider without getTranslations gets the old over-load plus one warn (fail-safe).",
      "tests": "All at HEAD c4588f7da6 (after merging origin/main 67baccc554). (1) pnpm --filter @objectstack/platform-objects test: \"Test Files 70 passed (70) / Tests 1096 passed (1096)\", lock VERDICT command-exit 0. (2) pnpm --filter @objectstack/platform-objects typecheck: exit 0, \"check:test-typecheck: OK ... 1 file(s) / 3 error(s) / 2 pinned signature(s)\" (the pre-existing feature-gate-guard entry only; the new test file compiles clean under tsconfig.test.json). (3) New file src/plugin.translations.test.ts on the real createMemoryI18n service, 10 passed: precondition; host bundle BEFORE wins (incl. nested field label); host bundle AFTER kernel:ready wins; CONTROL key not shipped serves platform + host-only key survives; CONTROL locale not shipped (ja-JP, en) unchanged; authored overlay never written into the bundle layer; late i18n gets bundles on kernel:ready with host winning; late path also never writes an authored overlay in; 8 loads once per service; fail-safe without getTranslations + one warn. (4) Ablations via scripts/ablation-replace.mjs WRAP mode on the committed state (the test imports ./plugin.js by relative path, so no dist is involved): A1 merge order reverted, \"3 failed | 7 passed\", each \"expected '组织' to be '工作区'\" (BEFORE pin, late-path host pin, late-path authored pin); A2 load moved back to a kernel:ready hook, \"1 failed | 9 passed\", authored-overlay pin \"expected '已编写' to be '组织'\"; A3 backstop registered in start() instead of init(), \"1 failed | 9 passed\", late-path authored pin. Each leg printed \"ok mutation landed: anchor 1 to 0, blob 9dc317736f49 changed\" and \"ok restored: blob == HEAD (9dc317736f49) and git diff HEAD is empty\". The first attempts of A1 and A3 were no-ops refused by the tool (replacement text contained the anchor text), no test ran in them; each was re-run with a distinct marker. (5) Scratch measurements, not committed: on both createMemoryI18n and FileI18nAdapter a host zh-CN label then a plain platform load serves 组织 (bug reproduced); getTranslations carries the authored overlay; lay-back after the overlay then overlay cleared still serves AUTHORED; memory getTranslations(zh-CN) with only zh loaded answers the zh data, FileI18nAdapter answers undefined. Leaf overlap of this package's bundles with the 8 other platform bundles: 0 in all 4 locales; Setup vs MetadataForms: 0; example bundles crm/showcase/todo vs platform: 0.",
      "gates": "Dispatch list re-derived on the real diff: identical 63 commands (derived at 1291f2b3a0, reconciled at c4588f7da6). All 63 run at c4588f7da6 with exit codes captured before any pipe; node scripts/pm/dispatch-gates.mjs --ran: \"63 derived famil(ies) accounted for — 63 run, 0 NOT-MEASURED\", exit 0. 60 exit 0 on the first run. check:dual-build-cjs-loads, check:i18n and check:lean-entry-closure first exited 3 (PREREQUISITE NOT MET, unbuilt workspace; not read as failures), and were re-run after the workspace build existed (built in this worktree during the battery by check:type-check-debt --re-measure): all exit 0 (\"107 published require entry point(s) across 66 package(s) load\"; \"check-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys)\"; \"check-lean-entry-closure: 2 published condition(s) measured from a real load\"). check:* gates ran unlocked per the role file, as one nohup job waited with tail --pid; build, tests and ablations ran under os-verify-lock (waited 0s each). NOT MEASURED, CI-owned with no local invocation: 6 workflow-valued families, 5 path-scheduled CI jobs (Test Core, Temporal Conformance, Dogfood Regression Gate, Dogfood Verify CLI, Build Core), 4 type-check lanes, and the 51 artifact-roster families (4 keep a roster under .changeset or packages). origin/main moved to 4e6b270999 after the merge (scripts only: check-platform-checklist, bare-root-worklist, check-skill-id-lint), not merged.",
      "line_budget": "398 changed lines (+364 / -34, 3 files) against the 3000-line human-merge threshold: under. No governed surface and no skills/** path touched.",
      "files_changed": [
        ".changeset/22972-host-bundle-wins.md (new, patch @objectstack/platform-objects, says what a host sees)",
        "packages/platform-objects/src/plugin.ts",
        "packages/platform-objects/src/plugin.translations.test.ts (new)"
      ],
      "deviations": [
        "Route: the bundles load in start() when an i18n service exists, with a kernel:ready backstop registered in init(), instead of a kernel:ready lay-back. Section 2 assumption 1 held (loadTranslations deep-merges, last writer wins). Assumption 2 (\"getTranslations returns the merged state you can lay back\") was partly falsified: the merged state includes the Studio-authored overlay, and laying it back after the overlay sync writes the overlay into the bundle layer. Ablation 2 shows the literal route turning that pin red. The ruling direction (platform under host) is kept, within the claimed file surface.",
        "Attribution follows AGENTS.md and the role file over the harness reminder. Commits carry the model-free trailer pair (Claude-Session plus Co-authored-by: Claude), which the pre-push check enforces, and the PR body ends with the session-URL footer. The reminder asked for a model-named Co-Authored-By trailer and a different PR footer; that is reported here, not followed.",
        "Merged origin/main 67baccc554 into the branch before the gate battery (merge commit c4588f7da6, clean, lockfile unchanged), then rebuilt the platform-objects closure.",
        "Zero label writes. The dispatch Writes section names none, and no skip-changeset applies (a patch changeset exists). The PR currently carries no labels.",
        "The changeset also carries a \"Clause-②: no\" line, beside PR body line 2."
      ],
      "cleanup": "Worktree ../objectstack-issue-22972 removed (rm -rf node_modules, then git worktree remove without --force, exit 0) after confirming HEAD c4588f7da6 == origin branch and a clean status. No process left running (the gate battery job exited; no dev server was started).",
      "mcp_calls": "0 — no MCP tool was called.",
      "api_writes": "2 relay strokes, both as objectstack-fleet[bot] via scripts/pm. (1) fleet-write request fw-20261012T101850Z-6b56f0, run 38235533968, conclusion success: POST /repos/objectstack-ai/objectstack/pulls (draft #23019, body read back byte-identical, 8665 bytes) and POST /repos/objectstack-ai/objectstack/issues/23019/assignees (os-steve). (2) This report: POST /repos/objectstack-ai/objectstack/issues/22972/comments via scripts/pm/post-stamped.mjs. Not REST: 6 git pushes to claude/issue-22972-host-bundle-wins. Everything else was reads (GET issue, comments, pull).",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none (承接者:无) · noted in the PR Acceptance notes, not filed · SAME FAMILY as this card, for the seat to route into the family close-out card: the eight other platform bundles (plugin-approvals, plugin-audit, plugin-security, plugin-sharing, plugin-webhooks, service-messaging, service-realtime, service-settings, service-storage) still load on kernel:ready with a plain loadTranslations, so a host bundle for one of THEIR keys loaded earlier still loses. No published statement covers those keys and no host was measured shipping one, so there is no reach · dedupe words: platform translation bundle precedence, host bundle override, kernel:ready loadTranslations, plugin translations under host",
        "carrier: none · noted, not filed · the memory i18n provider's getTranslations resolves locale fallback. When the platform loads a locale with no exact entry yet, a related locale's host values (zh for zh-CN) are laid over that locale's platform keys. FileI18nAdapter reads exact-locale only. This is a behaviour note: host text wins on platform keys.",
        "carrier: none · noted, not filed · II18nService has no read of the bundle layer alone (without the overlay, exact locale), which is why this fix depends on load timing. The ruling bars a new contract key, so none was proposed."
      ]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT: PR #23019 at c4588f7da6

    domain:engine seat 2 (#20966) · os-steve · session_01ADzJtzYTLUfgrRZHxkagkX · 2026-10-12T10:46Z. Claim 6121313516. Triage grade 6121140193 binds. Checked on GitHub rather than taken from the report. Clause-②: no, no packages/spec path, no governed text, so the seat's review is the review.

    • Form:
      • draft to main, assignee os-steve;
      • line 1 Fixes #22972 is the only closing keyword;
      • line 2 Clause-②: no, matching the claim and the changeset.
    • Scope: 3 files, +364 / −34: plugin.ts, a new plugin.translations.test.ts, and the changeset. Not governed. No generated bundle and no zh vocabulary touched. No new layer, option or contract key.
    • Fix, read in the diff: PlatformObjectsPlugin loads the Setup and MetadataForms bundles under what the i18n service already holds for each locale. A private layUnder lays every already-loaded leaf back on top of the platform bundle: deep, never replacing or deleting a host key, mutating nothing. A host bundle for a platform key therefore wins in either boot order, as content/docs/ui/translations.mdx publishes. A provider without getTranslations keeps the old over-load and warns once.
    • Measured deviation, accepted: the load moved from a kernel:ready lay-back to start(), with a kernel:ready backstop registered in init() for a late i18n service, once per service instance.
      • The reason is measured on both providers: getTranslations returns the bundles with the Studio-authored overlay merged in, and AppPlugin syncs that overlay on kernel:ready ahead of a start()-registered hook. A lay-back after it would write the overlay into the bundle layer, so a deleted Studio override would keep serving until restart.
      • The triage direction (platform under host) is kept. Only the timing differs, and the docblock states why.
    • Changeset: @objectstack/platform-objects patch. It says what a host sees, the authored-overlay rule and the fail-safe, and that zh-CN 「组织」 is unchanged. Read against the code: right.
    • Evidence:
      • 10 pins on a real createMemoryI18n: the host before and after wins; CONTROLs for an unshipped key and an unshipped locale; the authored overlay is never written into the bundle layer; the late i18n path; load-once; the fail-safe;
      • three ablation legs, each red as expected and restored (merge order 3 red, the load back to kernel:ready 1 red, the backstop in start() 1 red);
      • platform-objects: 1096 pass;
      • 63 derived gate commands, all exit 0.
    • CI on c4588f7da6: 34 runs, 31 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in)), 0 failed. git merge-tree against origin/main is clean.
    • Out-of-scope findings (Acceptance notes, not filed):
      1. The eight other platform bundles (approvals, audit, security, sharing, webhooks, messaging, realtime, settings and storage) still load on kernel:ready with a plain loadTranslations, so a host bundle for one of their keys loaded earlier still loses. This is a first sighting of the family. No published statement covers those keys and no host was measured shipping one. The second sighting opens the family card.
      2. The memory provider's locale fallback in getTranslations, a behaviour note.
      3. II18nService has no read of the bundle layer alone. The ruling bars a new contract key, so none is proposed.

    Landing: ready and auto-merge through the queue, in this act. On landing, objectstack-ai/cloud#2773 (pm:blocked on this card) can be told the framework fix is on main. It reaches cloud with its v18 framework move.

  5. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #23019 → 3071d3e989 · domain:engine seat 2 · session_01ADzJtzYTLUfgrRZHxkagkX · 2026-10-12T11:12Z

    • Merge: the merge queue merged PR fix(platform-objects): a host translation bundle for a platform key wins over the platform bundle in either boot order #23019 at 2026-10-12T11:10Z as 3071d3e989dfb2a04b75eeb81794e81761c198f2, with one parent, 9432bac077. ACCEPT on this card.
    • On main at that commit: the squash carries the accepted change exactly. All three files carry hunks identical to the accepted head c4588f7da6.
    • What main now holds: a host's translation bundle for a platform key (for example zh-CN objects.sys_organization.label) overrides platform-objects' text in either boot order. Studio-authored overrides behave as before, and the platform's zh-CN 「组织」 is unchanged.
    • Card: closed completed by the PR's Fixes #22972. pm:dispatched comes off in this act.
    • Cross-repo: objectstack-ai/cloud#2773 is pm:blocked on this card. Per the cross-repo unlock rule, the consumer's criterion is installability: the fix reaches cloud with its v18 framework move (objectstack-ai/cloud#2709), as triage noted. This seat has no write scope in that repository, so cloud's seat reads this landing from here.
    • Acceptance note carried: the eight other platform bundles still load on kernel:ready without the lay-under. This is a first sighting of that family, and a second sighting opens its card.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:i18nThe customer's own language, across UI, metadata and notificationsbugSomething isn't workingdomain:enginepriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions