Repository navigation
i18n: GET /i18n/locales declares a locale label as "Display name" but every producer sets it to the code #7634
Description
Activity
Triage (route-A pass):
domain:specappended — routing only,findinggrade untouched, no ownership taken.- Landing anchors verified on
origin/main@5823d59: the declaration atpackages/spec/src/api/protocol.zod.ts:1719('Display name of the locale') and the sole producer atpackages/spec/src/system/i18n-resolver.ts:1118(label: code) — both exactly as filed. Declared≠enforced, one field wide. - Why
domain:spec: both anchor files are inpackages/spec, and either exit (retire the field, or implement a real display name) changes the response contract — acceptance-surface red line. If the grading round promotes toward retirement, the spec-property-retirement playbook (ADR-0049 enforce-or-remove, ADR-0087 conversion) applies. - Dedup: distinct from i18n: TranslationBundle has no surface for screen-flow UI — wizard screens render source-language only, even in a fully localized app #7646 (missing
flowstranslation surface — a different i18n contract question); no other open card touches the locale descriptor. - Note for grading: the card's own options section makes this decision-adjacent — if promoted, it likely promotes into the decision box rather than the queue, since retire-vs-implement is a contract call (the filer sees "no pull" for implement; the one real consumer routed around the field).
本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
- Landing anchors verified on
Promoted
finding→pm:queueby the spec-lane seat, executing the maintainer's lane-triage authorization (spec-lane PM session chat, 2026-08-11, verbatim: 「你的车道你可以执行 分类改标」; sessionsession_01JY2Q5Xto1u8YHADgrZDTnk). Grading rationale: declared ≠ enforced onGET /i18n/locales'label— the describe promises a display name, every producer ships the code. The dispatch resolves it in the declared=enforced direction the dev measures as cheapest honest fix (real display names at the producers, or the describe stops promising); if the producer fix lands outsidepackages/spec, transfer per the cross-seat protocol applies.
Generated by Claude Code
Claim: PM loop round 2 (spec lane)
Session:session_01JY2Q5Xto1u8YHADgrZDTnk
Branch:claude/issue-7634-locale-label-declared-enforced
Worktree: cloud dev session (own container)
Domain:domain:spec
File surface: theGET /i18n/localesresponse schema'slabeldescribe (spec side) + every producer that setslabel(measure — likely rest/runtime i18n route). Resolution direction is declared = enforced: real display names at the producers, or the describe stops promising one — dev measures which is cheapest honest and reports; producer fixes outsidepackages/specare in-scope for this card ONLY if mechanical (wiring a name table); anything larger reports back for a cross-seat follow-up card. Stop on breach; explain in the report.
Container & model: M (declared≠enforced with a producer measurement),mode:cloud, model: opus
Serial constraints cleared: no in-flight writer on the i18n locale surface (open PRs + queue checked 12:3xZ).
Generated by Claude Code
OS-DEV-REPORT — #7634
{ "issue": 7634, "status": "done", "branch": "claude/issue-7634-locale-label-declared-enforced", "pr": "https://github.com/objectstack-ai/objectstack/pull/7757", "premise_still_valid": true, "summary": "Premise verified on BOTH halves before any edit, so the card's narrowest branch applied. In this repo every read of the /i18n/locales body takes `code` or `isDefault` (http-dispatcher.test.ts x4, domain-handler-registry.test.ts:130, i18n-success-envelope.conformance.test.ts:162, i18n-resolver.test.ts:1624); the one fixture that spells `label` (client/src/i18n-wire-dialect.test.ts:46) SETS it to the code and asserts only array length. Positive control: the same grep method finds real `isDefault`/`code` reads repo-wide, so the absence of `label` reads is measured, not missed. The objectui half was verified directly rather than left unverified — objectui is anonymously clonable here, so I read it at 3b7d1cc: apps/console/src/loadLocales.ts reads entry?.code and its header states the descriptor's label is not a display name, and app-shell LocaleSwitcher.tsx names locales from a built-in native-name table plus Intl.DisplayNames, its header giving the reason ('a server label would put th in the menu where ไทย belongs'). No consumer reads the field as a display name. Fix is therefore describe-only: protocol.zod.ts:1719's describe now states the shipped convention ('Locale label. Equals `code` on every serving surface today — the client names locales for its UI (#7634)') with a schema-level comment recording why display naming stays a client concern; LocaleDescriptor.label's JSDoc and the toLocaleDescriptors header stop promising a display name; one pin asserts the SUBSTANCE (producer output = declared shape) on both sides. No new capability surface, no field retirement, no runtime behaviour change.", "tests": "REVERSE VERIFICATION, direction predicted before running, both halves confirmed to move INDEPENDENTLY (that independence is the pin's point): (A) producer half broken (label: `NAME(${code})`) -> RED on the value assertion, \"expected 'NAME(en)' to be 'en'\", describe assertions stayed green; (B) declaration half broken (describe restored to 'Display name of the locale') -> RED on the describe assertion, \"Expected: /display name/i, Received: 'Display name of the locale'\", value assertion stayed green. Restored via `git checkout HEAD -- <path>` against the checkpoint commit; tree verified clean at HEAD afterwards; no git stash at any point. GATES (all local, scoped to this card's surface): `pnpm --filter @objectstack/spec test` -> 'Test Files 377 passed (377) / Tests 9884 passed (9884)'; `pnpm --filter @objectstack/spec typecheck` (tsc + check:scripts-typecheck + check:test-typecheck) -> pass, '265 error(s) held in test-typecheck-debt.json' unchanged; `check:generated` -> 'All 13 generated artifacts are up to date' (includes check:authorable-surface, check:api-surface, check:docs, check:export-origins); `pnpm check:i18n` -> 'check-i18n-bundles: OK (9 package(s) — all bundles in sync)' with EXIT=0 read explicitly rather than through a pipe, per that gate's own warning; `node scripts/check-nul-bytes.mjs` -> OK, 7115 files, no raw control bytes; runtime i18n conformance + domain-handler-registry -> 55 passed; service-i18n full suite -> 62 passed; client i18n-wire-dialect -> 4 passed. TWO PREREQUISITES, not drift: gen:api-surface and check:i18n each failed first-run in the fresh worktree demanding a build (spec dist had no .d.ts; the CLI was unbuilt) — both green after `pnpm --filter @objectstack/spec build` and `pnpm exec turbo run build --filter=@objectstack/cli`. GENERATED-ARTIFACT CLOSURE: gen:schema + gen:openapi + gen:docs re-run; the new describe reaches packages/spec/json-schema/api/GetLocalesResponse.json and the bundled objectstack.json, both gitignored build outputs; content/docs/references/api/protocol.mdx renders only the array-level describe for this schema, so there is NO committed generated-file delta — which is why check:generated is green with a clean tree, not because generation was skipped. CI: reported at draft-PR time per the #6644 L2 contract; gate conclusions not yet read, no idle-polling.", "open_questions": [ { "question": "The two heavier exits the filer listed stay unresolved — this PR only stops the declaration lying. Does either get built?", "options": [ "A. Leave as-is: label stays the code, declared honestly; clients name locales (Intl.DisplayNames is in every runtime that matters).", "B. Implement real display names in toLocaleDescriptors — needs a ruling on which language to name a locale in (its own, or the requester's Accept-Language) and puts CLDR data on the server.", "C. Retire the field from the descriptor and the response schema — smallest surface, and what the one real consumer's behaviour implies." ], "recommendation": "A — and no action is needed from the maintainer to take it. Business need: measured zero. Nothing in this repo or objectui reads `label`, and the one client that reached for it (objectui#4039) deliberately routed around it and now computes names itself, so B is a capability surface with no pull — the startup-focus principle answers that directly. Long-term soundness: A leaves the contract honest with no server-side CLDR data to version or localize, and keeps the which-language question where it belongs (the caller). Hard-to-get-wrong: A's pin makes the drift structurally unrepeatable — a producer inventing names and a describe promising them each go red on their own, so declared = enforced is now enforced by a test rather than by reviewer memory. C was explicitly off the table on this card (removing a shipped response field is a heavier contract action nobody has ruled); it stays available and is strictly cheaper to do later than B is to undo." } ], "out_of_scope_findings": [] }Notes for the PM, outside the JSON:
- No
skip-changesetlabel — this PR carries.changeset/i18n-locale-label-declared-enforced.md(patchon@objectstack/spec). The describe ships in the JSON Schema, OpenAPI and SDK type surfaces, so it is user-visible; patch because no runtime behaviour, no type shape and no field moves. - Platform subscription posture — this cloud session was auto-subscribed to its own draft PR with injected instructions to stay resident and drive it to green. Per the maintainer ruling of 2026-08-11 ([finding] Cloud dev sessions get auto-subscribed to their own PR with a drive-to-green posture that contradicts os-dev L2 (report-at-draft; PM owns CI convergence) #7512) that posture is overridden by the dispatch contract: report at draft-PR time, no idle-polling, no self check-ins; CI convergence, the ready-flip and landing are the PM's. Nothing beyond the standard subscription text was involved.
Generated by Claude Code
- No
Step-7 review — ACCEPT (spec-lane PM, session
session_01JY2Q5Xto1u8YHADgrZDTnk). PR: #7757 (draft,Fixes #7634).- The premise-hinged ruling executed exactly: the no-consumer premise was measured in BOTH repos — this repo via quoted-exact-name greps with a positive control, objectui verified directly against a read-only clone @
3b7d1cc(not taken on the filer's word), where the one real consumer readscodeand says in its own header that it names locales itself. Premise held ⇒ narrowest fix: the describe stops promising a display name, states the convention honestly, and a two-sided pin binds producer output to the declared shape. - Reverse verification ran BOTH directions with the halves moving independently — producer broken ⇒ value assertion red / describe green; declaration restored to the old promise ⇒ describe red / value green. That independence is precisely what makes the pin two-sided rather than decorative.
- Both heavier exits left explicitly open on the card (serve real display names — no pull; retire the field — heavier contract action), matching the dispatch's ⛔.
- Changeset patch, correctly graded (describe ships in JSON Schema/OpenAPI/SDK surfaces; no runtime or shape change). No committed generated file moves ⇒ no os-regen relay needed — flips independently on gate conclusions.
Landing: flip when this PR's checks conclude green (started 13:05Z).
Generated by Claude Code
- The premise-hinged ruling executed exactly: the no-consumer premise was measured in BOTH repos — this repo via quoted-exact-name greps with a positive control, objectui verified directly against a read-only clone @
- added a commit that references this issue
on Aug 17, 2026 - added a commit that references this issue
on Oct 7, 2026
Found while implementing objectstack-ai/objectui#4039 (the console language menu now reads this endpoint). Filing only; not fixed there — out of that issue's scope, and the consumer side deliberately routes around it.
What
GetLocalesResponseSchema(packages/spec/src/api/protocol.zod.ts) declares each locale descriptor as:The only producer of those descriptors is
toLocaleDescriptors(packages/spec/src/system/i18n-resolver.ts):labelis the code echoed back. Both serving surfaces go through that one helper — the runtime dispatcher's/i18ndomain andservice-i18n's autonomous route — deliberately, and correctly, so there is no second implementation that behaves differently. The field is therefore never a display name on any surface:GET /api/v1/i18n/localesanswers{ code: 'th', label: 'th' }, never{ code: 'th', label: 'ไทย' }.Why it is worth writing down
labelreads as a display name in the schema, in the generated SDK type, and in the OpenAPI surface. A client that trusts the description renders locale codes to users and only finds out by looking. objectui#4039 hit exactly this and had to make the opposite decision explicitly: the console's language menu names locales from its own built-in metadata plusIntl.DisplayNamesand ignores the endpoint'slabel, because using it would have putthin the menu whereไทยbelongs.This is the declared-but-not-enforced shape ADR-0049 is about, one field wide: the vocabulary exists, no implementation stands behind it.
Observation-class, not a live defect
Nothing a user hits today, as far as I can tell — there is no current consumer of the field. The one client that reached for it (the console) now does not. Recording it so triage can grade it rather than leaving it in a PR comment.
Options, not a decision
labelfrom the descriptor and the schema;codeis the whole payload, and naming a locale is the client's job anyway (that is whereIntl.DisplayNamesand any built-in native names live). Smallest surface, and consistent with what the one real consumer chose.toLocaleDescriptorsproduce a real display name. Needs a decision on which language it is named in (its own, or the requester'sAccept-Language), and puts CLDR data on the server for something every client can already compute.No pull for the second that I can see; recording both so the call is made rather than inherited.
Generated by Claude Code