Full i18n area run of the checklist-test skill — all 5 items driven against a live showcase in a genuine Chinese session (opus subagent, isolated port + file DB). Text-only per RUNNER.md.
Result: 1 PASS · 3 PARTIAL · 1 FAIL. The resolver itself is in good shape — zero raw translation keys rendered on any of 12 surfaces. The findings are in what the platform advertises and in two items whose clauses no longer match the shipped contract.
Environment — framework 92f26f75 (branch claude/platform-test-checklist-ocwugl) · console bundle 09987b680 (packages/console/dist/.objectui-sha; the server itself prints a drift warning against the repo-root pin 6314e87f2) · showcase app · isolated port + file DB.
✅ PASS — strict-translation-key-rejection (6/6 + both negatives)
The strongest result of the run. All 10 declared bad-key variants were rejected at both doors — the runtime PUT /api/v1/meta/translation/<n> and the built defineTranslation / defineTranslationBundle — for 20/20 rejections and zero 2xx. No #3778/#4522 asymmetry between the two doors.
Every message names the surface, echoes the key in backticks, and carries either a did-you-mean or a located prescription:
Nothing half-persisted: the item 404s before the first attempt and after every one of the 10 rejections, so the valid sibling key in the same document never landed. The corrected document then saves 200 and the new label renders live in the browser.
🔴 FAIL — surface-matrix: /i18n/locales advertises locales the app never opted into
11 of the 13 declared surfaces render fully Chinese, each expectation read from the served bundle first: nav, list headers, option labels (three places), form labels, view tabs, dashboard widget titles, SDUI pages, settings, Studio metadataForms, sys_* objects, and activity-feed verbs (localized at write time per ADR-0053 — a PATCH wrote sys_activity.summary='更新了 任务…'). A regex scan for all nine declared group prefixes across 12 surfaces returned zero raw dotted keys. /i18n/labels/<obj>/<locale> agrees with the rendered header row string-for-string. An unknown locale returns an honest empty bundle — no 500, no key dump, no synthesized default.
Clause 6 fails.
Reproduction rule — boot the showcase on a fresh file DB, sign in, then GET /api/v1/i18n/locales.
Expected descriptors for the artifact's declared i18n.supportedLocales = ['en','zh-CN'].
Actual four descriptors — en, zh-CN, ja-JP, es-ES. Reproduced on two boots.
The envelope half is fine ({success,data:{locales}} with isDefault — #3636 shape intact); the set is a superset. ja-JP and es-ES are real and servable here (200 / 200,501 bytes and 200 / 188,750 bytes) but the app never opted into them.
Root cause (located). Nothing narrows the i18n service's locale set to the app's declared supportedLocales. runtime/src/app-plugin.ts:1409 loadTranslations() reads i18nConfig.defaultLocale only, then loads every locale key of every bundle — and each platform plugin independently pushes its own 4-locale bundle at kernel:ready (platform-objects/src/plugin.ts:206-230, plus service-settings, service-storage, service-messaging, service-realtime, plugin-security, plugin-sharing, plugin-webhooks — each shipping en/zh-CN/ja-JP/es-ES). getLocales() then returns the loaded key set (core/src/fallbacks/memory-i18n.ts; same in service-i18n/src/file-i18n-adapter.ts:193), and both the dispatcher domain and the service-i18n route map that set straight to the response.
Consequence: any client building a locale picker from this route — including the platform's own Settings → Localization select, which already offers all four — offers locales in which only sys_* objects are translated. Picking ja-JP or es-ES guarantees a mixed-language session for app-owned metadata.
Honest gaps in this item's run: the object-action confirm/success clause is not-run (the declared action's menu could not be surfaced, and no confirmText is translated anywhere in the bundle to test against), and the list-filter third place for option labels was not proven (the filter panel didn't open under the driver).
🟡 PARTIAL — 3 items
notification-localized-and-clears — the clear works; the item's premise is a capability the spec retired
The mechanical half passes: clicking an entry fired POST /api/v1/notifications/read → 200 and the panel's mark-all fired POST /api/v1/notifications/read/all → 200 against the running Hono server, so the #3362 dispatcher-only regression is absent; a server re-read confirms read:true, and the clear survives a full reload (the residual badge was new mail from the digest flow firing every minute, verified by createdAt, not a failed clear).
Clauses 0 and 4 are blocked(fixture) — and this needs a checklist decision, not a code fix. The notification title renders New task assigned: Build homepage because that is a literal English string authored in the showcase flow's notify node (src/automation/flows/index.ts:160-166), not a translation key. There is no translation group for notification copy at all: notifications is a retired top-level key whose own spec guidance reads "notifications is the retired object-first dialect — notifications have no translation group, omit them", and platform-side assignment notifications were deliberately moved to app-authored flows (#3403). sys_user carries no locale field, and the notify path never consults a recipient locale.
As written, clauses 0 and 4 are unprovable on any stock app. The item assumes a per-recipient localized notification title — a capability the spec retired.
studio-follows-app-locale — Studio does follow; the clause wording collides with declared coverage debt
Studio flips with the app: designer chrome, object list, column headers and the metadataForms object form all render Chinese; the choice is stored and survives both a reload and a fresh sign-in in a new browser context; the return trip to English is symmetric (cjk=0 latin=1147), not a ratchet. No Studio surface stayed English — which is the failure this item exists to catch.
Two reasons it is not a clean pass:
- Residual English strings (
Announcement, Business Unit, Owning Business Unit, Sales Region, …) are the showcase's frozen untranslated debt — they render their EN source label, never a key — which the sibling item's knownGaps declares legitimate. The clause's wording ("no untranslated declared string next to translated ones") should be reconciled with the coverage ratchet.
- The date formatter is half-localized. Past relative forms localize (
逾期 6 天, 创建于10天前, 刚刚) while future relative forms stay English on the very same Chinese page (In 3 days, In 2 days), and absolute timestamps render en-US (8/11/2026 9:25 am). This is a genuine product gap, just not one this item's clauses name.
build-gates-hold — the gates behave correctly; packages/spec/dist is stale in this checkout
What was proven: both --self-tests pass with their classifiers staying distinct; drift detection was driven red-then-green for real (the actual gate run from a /tmp symlink farm — every workspace entry symlinked with one real copy of the package under test — going in sync → DRIFTED (1) → in sync on revert, without touching a repo file); and the unbuilt-CLI prerequisite is named correctly by both gates with "Nothing was checked/measured", while --write prints regenerat* zero times.
Clauses 0 and 3 are blocked(environment) on one shared cause: packages/spec/dist predates commit 06be54ec (#7285, which added authorisesIrreversibleAction) by 13 minutes — dist built 2026-08-10T06:29Z, commit 06:42Z. So platform-objects' extract fails with "The requested module '@objectstack/spec/system' does not provide an export named 'authorisesIrreversibleAction'", and the clean-tree green cannot be observed. The runner deliberately did not rebuild, since the workspace build is shared with concurrent batches.
Worth acting on regardless of this run: the coverage gate handles this cause well — it refuses to judge the ratchet on a partial round ("Nothing was compared… the baseline was left exactly as committed"), which is the designed #6033/#5862 honesty. The bundles gate handles the same cause poorly: it reports a stale-dist prerequisite failure as "1 bundle problem(s)", a content verdict. It should name the prerequisite the way its sibling does.
Checklist maintenance falling out of this run
build-gates-hold step 4 is a no-op as written. "Perturb one committed bundle value" leaves the gate green by design — merge mode re-uses the committed file as its own baseline, so drift is structural, not textual. It should read "remove or rename a committed bundle key". (The runner discovered this by trying the literal step first, watching it stay green, and then finding the structural perturbation that does bite.)
notification-localized-and-clears clauses 0 and 4 need rewriting or waiving — see above.
surface-matrix _sections has no addressable surface on this fixture. The record page renders the showcase's record:details block whose sections are authored with label and no name (task-detail.page.ts:74-76), and component.zod.ts:491 documents that "a nameless section renders its authored label in every locale". The bundle does carry 概览/排期/详细信息 — they are simply unreachable. Fixture authoring gap, not a resolver defect.
surface-matrix view emptyState — the showcase authors none on any view, so no translated key exists to test.
studio-follows-app-locale clause 1 should allow for the frozen untranslated debt the sibling item already declares legitimate.
Runner-environment facts worth keeping
- The console
LocaleSwitcher offers language-only codes and persists localStorage['objectui-locale']. Writing zh-CN there is rejected by isKnownLanguage() and silently boots en — the working Chinese session is zh, which the server resolves to zh-CN for both /i18n/* and Accept-Language-driven metadata localization.
- The app shell bell does issue read/read-all requests; only the console home header bell is inert (confirming the earlier finding).
I18nServicePlugin/FileI18nAdapter is not installed on this app — the i18n routes are served by the dispatcher /i18n domain over core's in-memory fallback. A language-only or script tag resolves by prefix match (/translations/zh and /translations/zh-Hans both return the zh-CN bundle).
Full
i18narea run of thechecklist-testskill — all 5 items driven against a live showcase in a genuine Chinese session (opus subagent, isolated port + file DB). Text-only per RUNNER.md.Result: 1 PASS · 3 PARTIAL · 1 FAIL. The resolver itself is in good shape — zero raw translation keys rendered on any of 12 surfaces. The findings are in what the platform advertises and in two items whose clauses no longer match the shipped contract.
Environment — framework
92f26f75(branchclaude/platform-test-checklist-ocwugl) · console bundle09987b680(packages/console/dist/.objectui-sha; the server itself prints a drift warning against the repo-root pin6314e87f2) · showcase app · isolated port + file DB.✅ PASS —
strict-translation-key-rejection(6/6 + both negatives)The strongest result of the run. All 10 declared bad-key variants were rejected at both doors — the runtime
PUT /api/v1/meta/translation/<n>and the builtdefineTranslation/defineTranslationBundle— for 20/20 rejections and zero 2xx. No #3778/#4522 asymmetry between the two doors.Every message names the surface, echoes the key in backticks, and carries either a did-you-mean or a located prescription:
helpTexton a field translation → "Did you meanhelpText→help?"labelon a widget translation → "Did you meanlabel→title?"o→ "useobjects.<object_name>" ·nav→ "useapps.<app_name>.navigation.<node_id>.label" ·dashboard→ "usedashboards.<dashboard_name>(plural)"validationMessages→ "removed in @objectstack/spec 17.0.0 (清空剩余 6 条 authorWarn 死键 —— book ×2 / job.id / translation.validationMessages / app.homePageId / app.areas[].order(ADR-0049,v17 限时) #4667, ADR-0049) — no resolver ever read it… author the message on the rule itself… Runos migrate meta --from 16"Nothing half-persisted: the item 404s before the first attempt and after every one of the 10 rejections, so the valid sibling key in the same document never landed. The corrected document then saves 200 and the new label renders live in the browser.
🔴 FAIL —
surface-matrix:/i18n/localesadvertises locales the app never opted into11 of the 13 declared surfaces render fully Chinese, each expectation read from the served bundle first: nav, list headers, option labels (three places), form labels, view tabs, dashboard widget titles, SDUI pages, settings, Studio
metadataForms,sys_*objects, and activity-feed verbs (localized at write time per ADR-0053 — aPATCHwrotesys_activity.summary='更新了 任务…'). A regex scan for all nine declared group prefixes across 12 surfaces returned zero raw dotted keys./i18n/labels/<obj>/<locale>agrees with the rendered header row string-for-string. An unknown locale returns an honest empty bundle — no 500, no key dump, no synthesized default.Clause 6 fails.
Reproduction rule — boot the showcase on a fresh file DB, sign in, then
GET /api/v1/i18n/locales.Expected descriptors for the artifact's declared
i18n.supportedLocales=['en','zh-CN'].Actual four descriptors —
en,zh-CN,ja-JP,es-ES. Reproduced on two boots.The envelope half is fine (
{success,data:{locales}}withisDefault— #3636 shape intact); the set is a superset.ja-JPandes-ESare real and servable here (200 / 200,501 bytes and 200 / 188,750 bytes) but the app never opted into them.Root cause (located). Nothing narrows the i18n service's locale set to the app's declared
supportedLocales.runtime/src/app-plugin.ts:1409loadTranslations()readsi18nConfig.defaultLocaleonly, then loads every locale key of every bundle — and each platform plugin independently pushes its own 4-locale bundle atkernel:ready(platform-objects/src/plugin.ts:206-230, plus service-settings, service-storage, service-messaging, service-realtime, plugin-security, plugin-sharing, plugin-webhooks — each shippingen/zh-CN/ja-JP/es-ES).getLocales()then returns the loaded key set (core/src/fallbacks/memory-i18n.ts; same inservice-i18n/src/file-i18n-adapter.ts:193), and both the dispatcher domain and the service-i18n route map that set straight to the response.Consequence: any client building a locale picker from this route — including the platform's own Settings → Localization select, which already offers all four — offers locales in which only
sys_*objects are translated. Pickingja-JPores-ESguarantees a mixed-language session for app-owned metadata.Honest gaps in this item's run: the object-action confirm/success clause is not-run (the declared action's menu could not be surfaced, and no
confirmTextis translated anywhere in the bundle to test against), and the list-filter third place for option labels was not proven (the filter panel didn't open under the driver).🟡 PARTIAL — 3 items
notification-localized-and-clears— the clear works; the item's premise is a capability the spec retiredThe mechanical half passes: clicking an entry fired
POST /api/v1/notifications/read→ 200 and the panel's mark-all firedPOST /api/v1/notifications/read/all→ 200 against the running Hono server, so the #3362 dispatcher-only regression is absent; a server re-read confirmsread:true, and the clear survives a full reload (the residual badge was new mail from the digest flow firing every minute, verified bycreatedAt, not a failed clear).Clauses 0 and 4 are
blocked(fixture)— and this needs a checklist decision, not a code fix. The notification title rendersNew task assigned: Build homepagebecause that is a literal English string authored in the showcase flow's notify node (src/automation/flows/index.ts:160-166), not a translation key. There is no translation group for notification copy at all:notificationsis a retired top-level key whose own spec guidance reads "notificationsis the retired object-first dialect — notifications have no translation group, omit them", and platform-side assignment notifications were deliberately moved to app-authored flows (#3403).sys_usercarries nolocalefield, and the notify path never consults a recipient locale.As written, clauses 0 and 4 are unprovable on any stock app. The item assumes a per-recipient localized notification title — a capability the spec retired.
studio-follows-app-locale— Studio does follow; the clause wording collides with declared coverage debtStudio flips with the app: designer chrome, object list, column headers and the
metadataFormsobject form all render Chinese; the choice is stored and survives both a reload and a fresh sign-in in a new browser context; the return trip to English is symmetric (cjk=0 latin=1147), not a ratchet. No Studio surface stayed English — which is the failure this item exists to catch.Two reasons it is not a clean pass:
Announcement,Business Unit,Owning Business Unit,Sales Region, …) are the showcase's frozen untranslated debt — they render their EN source label, never a key — which the sibling item'sknownGapsdeclares legitimate. The clause's wording ("no untranslated declared string next to translated ones") should be reconciled with the coverage ratchet.逾期 6 天,创建于10天前,刚刚) while future relative forms stay English on the very same Chinese page (In 3 days,In 2 days), and absolute timestamps renderen-US(8/11/2026 9:25 am). This is a genuine product gap, just not one this item's clauses name.build-gates-hold— the gates behave correctly;packages/spec/distis stale in this checkoutWhat was proven: both
--self-tests pass with their classifiers staying distinct; drift detection was driven red-then-green for real (the actual gate run from a/tmpsymlink farm — every workspace entry symlinked with one real copy of the package under test — goingin sync→DRIFTED (1)→in syncon revert, without touching a repo file); and the unbuilt-CLI prerequisite is named correctly by both gates with "Nothing was checked/measured", while--writeprintsregenerat*zero times.Clauses 0 and 3 are
blocked(environment)on one shared cause:packages/spec/distpredates commit06be54ec(#7285, which addedauthorisesIrreversibleAction) by 13 minutes — dist built2026-08-10T06:29Z, commit06:42Z. Soplatform-objects' extract fails with "The requested module '@objectstack/spec/system' does not provide an export named 'authorisesIrreversibleAction'", and the clean-tree green cannot be observed. The runner deliberately did not rebuild, since the workspace build is shared with concurrent batches.Worth acting on regardless of this run: the coverage gate handles this cause well — it refuses to judge the ratchet on a partial round ("Nothing was compared… the baseline was left exactly as committed"), which is the designed #6033/#5862 honesty. The bundles gate handles the same cause poorly: it reports a stale-dist prerequisite failure as "1 bundle problem(s)", a content verdict. It should name the prerequisite the way its sibling does.
Checklist maintenance falling out of this run
build-gates-holdstep 4 is a no-op as written. "Perturb one committed bundle value" leaves the gate green by design — merge mode re-uses the committed file as its own baseline, so drift is structural, not textual. It should read "remove or rename a committed bundle key". (The runner discovered this by trying the literal step first, watching it stay green, and then finding the structural perturbation that does bite.)notification-localized-and-clearsclauses 0 and 4 need rewriting or waiving — see above.surface-matrix_sectionshas no addressable surface on this fixture. The record page renders the showcase'srecord:detailsblock whose sections are authored withlabeland noname(task-detail.page.ts:74-76), andcomponent.zod.ts:491documents that "a nameless section renders its authored label in every locale". The bundle does carry 概览/排期/详细信息 — they are simply unreachable. Fixture authoring gap, not a resolver defect.surface-matrixviewemptyState— the showcase authors none on any view, so no translated key exists to test.studio-follows-app-localeclause 1 should allow for the frozen untranslated debt the sibling item already declares legitimate.Runner-environment facts worth keeping
LocaleSwitcheroffers language-only codes and persistslocalStorage['objectui-locale']. Writingzh-CNthere is rejected byisKnownLanguage()and silently bootsen— the working Chinese session iszh, which the server resolves tozh-CNfor both/i18n/*andAccept-Language-driven metadata localization.I18nServicePlugin/FileI18nAdapteris not installed on this app — the i18n routes are served by the dispatcher/i18ndomain over core's in-memory fallback. A language-only or script tag resolves by prefix match (/translations/zhand/translations/zh-Hansboth return thezh-CNbundle).