Repository navigation
feat(spec)!: a locale must be a BCP-47 tag — zh_CN, padded values and language names are refused - #25983
Conversation
A translation item, a bundle's locale key, the stack's i18n locales and a seed dataset's locale scope accepted any string, so zh_CN or a padded value saved green under a locale no session asks for. The ADR-0087 D2 conversion translation-locale-tag-canonicalized rewrites the underscore and whitespace spellings in stored rows and built artifacts; the rest is reported as a TODO. Claude-Session: https://claude.ai/code/session_01Do245n6wxzetD6poZNSdeM Co-authored-by: Claude <noreply@anthropic.com>
…le tag shape Claude-Session: https://claude.ai/code/session_01Do245n6wxzetD6poZNSdeM Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 144 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin bdce2f25ec6274740bc3d40393edd9acc1870954 && git checkout bdce2f25ec6274740bc3d40393edd9acc1870954
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a4eae06499a8f0377960bfc9c98daced5cf19258 2f0b44d58376c8b5377f865e09570a2dc744a844 && git checkout -B drift-repro a4eae06499a8f0377960bfc9c98daced5cf19258 && git merge --no-ff 2f0b44d58376c8b5377f865e09570a2dc744a844
node scripts/docs-audit/affected-docs.mjs --json a4eae06499a8f0377960bfc9c98daced5cf19258
|
Contract reviewServed-tier: Narrowing face: the PR body and the changeset both declare ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
…d; registry walks no longer call LocaleSchema an open string Claude-Session: https://claude.ai/code/session_01Do245n6wxzetD6poZNSdeM Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Narrowing face: the PR body and the changeset both declare ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
席位核对ACCEPT · domain:spec seat 2 · 2026-10-26T09:11Z · head
|
Requested by Steedos · project thread
Fixes #25822
Clause-②: no (narrowing)
Metadata-protocol: narrowed
What changes
LocaleSchema(packages/spec/src/system/translation.zod.ts) was a barez.string(). A translator or AI author who wrotezh_CN, a padded value ornot a locale!!got a green save, and the strings were stored under a locale no session ever asks for. The resolvers fold case and nothing else, so those strings never rendered.LocaleSchemanow accepts only a tag-shaped value: a 2–8 letter language subtag, then hyphen-separated 1–8 letter-or-digit subtags. This is the same patternSYS_USER_LOCALE_TAG_PATTERNuses for a user's ownlocalecolumn. The refusal is oneinvalid_formatissue whose message names the shape and the release. Case is not judged:zh-cnstill parses, because every resolver folds case.translation-locale-tag-canonicalized(retired from the load path from day one,retiredAfter: '18.1.0', order 76) rewrites the two spellings that have one tag they can have meant:_as the separator, and surrounding whitespace. Any other value is left as stored and reported as a TODO. So is a bundle key that would land on a tag the entry already holds with different strings. An identical twin is merged.translation-locale-non-tag-refusedcarries the judgment foros migrate meta..changeset/25822-locale-tag-shape.md(@objectstack/specminor) with the FROM → TO mapping and both declaration lines.Every consumer of
LocaleSchema, and whether it is in scopeTranslationItemSchema.locale(metadata save door,defineTranslation)locale; the card's reproductionTranslationBundleSchema/PlatformTranslationBundleSchemamap key (defineTranslationBundle,stack.translations)invalid_keyat the key; the error map surfaces the nested messageTranslationConfigSchemadefaultLocale/supportedLocales/fallbackLocalei18n.*data/seed.zod.tsSeed.locale[]data[].locale[]data/seed-loader.zod.tsSeedLoaderConfig.localeAppPlugin.resolveSeedLocale(), which readsi18n.defaultLocale, itself narrowed above, and converted at artifact load (convertArtifactAtLoadruns over the definition)conversions/registry.tscomments at the submitLabel and subCaption walks ("LocaleSchemais an open string")packages/lint/src/lint-liveness-properties.tstranslation bundle walkui/packages/app-shell/.../metadata-admin/clientValidation.tsMechanism assumptions, re-measured on
0c372288git grepoverlocale/defaultLocale/fallbackLocaleliterals,supportedLocalesarrays and underscore bundle keys acrosspackages/andexamples/finds none that parse throughLocaleSchema. The non-tag hits are a user'ssys_user.locale, an email-template locale and the i18n label resolver's inputs, none of which use this schema.shared/value-domain.zod.tsrecords why a registry check was dropped).translationrows (applyConversionsToStoredItemreaches them throughSTORED_ONLY_COLLECTIONS) and built artifacts inside their declared-floor window.translation.zod.tsplus one MAJOR_18 conversion. No other producer needed a fix.Deviations from the claim's file surface
Two files outside
translation.zod.ts (+ tests), registry.ts, generated artefacts, one changeset, each forced by a red test:packages/spec/src/migrations/entries/semantic/18.translation-locale-non-tag-refused.ts, the D3 entry:migrations.test.tsreds "from protocol 18 on, every graduated D2 conversion is named by a D3 entry of its own step (ruling B)" without it (measured).packages/spec/src/shared/retired-key-removal-version.pin.test.ts: one JOIN row. The pin reds every entry retired after the newest published stable release that has no row.Verification (head
3e367787)pnpm --filter @objectstack/spec build, thencheck:generated: 14/14 artifacts up to date.pnpm --filter @objectstack/spec typecheck: exit 0.pnpm --filter @objectstack/spec test: 722 files, 21903 tests passed.translation.test.ts: tags accepted, ten non-tags refused asinvalid_formatnaming the release, refusal at the item, bundle key, platform bundle key and i18n config, and the conversion's TODOs and twin merge. The conversion fixture runs inconversions.test.ts(7 notices).dispatch-gates.mjs --commands): 92/92 run, all exit 0. Five exited 3 (prerequisite not built) on the first pass and 0 after the dependency closure was built.--ranreconciliation: 92 derived, 92 run, 0 NOT-MEASURED.pnpm --filter @objectstack/dogfood testunder the verify lock, after building the dogfood closure): VERDICT command-exit 1. Test Files 1 failed, 295 passed, 3 skipped (299); Tests 1 failed, 2522 passed, 18 skipped (2541). The one failure is not this diff's:test/authz-probe-blind-spot.test.ts>packages/runtime/src/domains/i18n.tsexpects 3 occurrences of thehandleI18nRequestcontrol and reads 4.69fa23d60(fix(runtime): GET /i18n/translations/:locale carries an ETag and Cache-Control: private, no-cache, and a matching If-None-Match is answered 304 (#25785) #25956, already onmain) added the fourth. This diff touches no runtime file.Acceptance notes
zh-cn) is deliberate: the resolvers fold case (resolveLocalein core's memory i18n, the metadata-protocol seed loader), so refusing it would refuse a locale that works.i18n.defaultLocalethat reachesAppPluginwithout passingdefineStackand outside the artifact window now fails the boot-timeSeedLoaderRequestSchema.parseloudly instead of filtering seeds on a locale no dataset can name.Generated by Claude Code