Repository navigation
fix(service-settings)!: Setup's default language offers and accepts the languages the deployment supports, not four hard-coded ones - #30446
Conversation
…'s locale list Claude-Session: https://claude.ai/code/session_01Utp6CZEhtHfQH5TLEVe3TF Co-authored-by: Claude <noreply@anthropic.com>
… acceptance on a real boot Claude-Session: https://claude.ai/code/session_01Utp6CZEhtHfQH5TLEVe3TF Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CZEhtHfQH5TLEVe3TF Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 9 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 74481d1eaf566530496115067132dff33de57478 && git checkout 74481d1eaf566530496115067132dff33de57478
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 677ac3bbc026ae9a6d6049b3dd1ad0f5e66bb4d7 e1c377ef0e8054f799a13c64482a5b04a998304a && git checkout -B drift-repro 677ac3bbc026ae9a6d6049b3dd1ad0f5e66bb4d7 && git merge --no-ff e1c377ef0e8054f799a13c64482a5b04a998304a
node scripts/docs-audit/affected-docs.mjs --json 677ac3bbc026ae9a6d6049b3dd1ad0f5e66bb4d7
|
|
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF Contract-review-claim: session_01Utp6CZMkHvuDeVYgTD73qF · head e1c377e · 2026-11-05T11:46Z |
|
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF Contract reviewServed-tier: Inputs: card #30286 (body, 5 comments through the dev report 6657161883), PR #30446 (body, 10-file list, net diff against ① Derived judgmentsEach accept-set or public-surface change the diff implies, named right or wrong against the code read:
② Semver level
③ Boundary flags
④ Design judgmentThe narrowing, read against the published spec on Tried to break it:
Findings, none a must-fix:
Check-runs on the head at the final read: every completed run is Implemented-by: VERDICT: PASS |
|
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF 席位意见:ACCEPT(#30286)。Setup 的「默认语言」现在给出并接受部署实际支持的语言(和语言菜单读同一个列表,加了翻译就自动出现),名称用该语言自称。已保存但不再提供的值照常可读,标「已不再提供」,不会被替换。部署不支持的语言今后保存会被拒(收窄)。 席位核对Head-sha:
|
|
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF 维护者速读
席位意见:ACCEPT(#30286)。Setup 的「默认语言」现在给出并接受部署实际支持的语言(和语言菜单读同一个列表,加了翻译就自动出现),名称用该语言自称。已保存但不再提供的值照常可读,标「已不再提供」,不会被替换。部署不支持的语言今后保存会被拒(收窄)。 席位核对Head-sha:
|
Requested by Bill · project thread
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF
维护者速读
getLocales(),即GET /api/v1/i18n/locales),选项每次打开页面、每次保存时现读,所以后加的翻译项(比如越南语)马上就能选;每个选项用该语言自己的名字显示(简体中文、日本語、Tiếng Việt)。fr-FR、vi-VN一律被拒,每个新用户都从公司没选的语言起步。Clause-②: yes (narrowing)):部署不支持的语言以后存不进去,例如应用只声明en、zh-CN时,以前能存的ja-JP、es-ES现在被拒。已经存着的旧值照常生效、照常可读,页面上显示为「(已不再提供)」,保存别的字段不受影响;只有再次写入它才会被拒。en-US这类老写法在部署提供en时照常接受。没有 i18n 服务的组合仍用原来四种。回滚即撤本 PR。packages/services/service-settings/src/localization-locale-options.ts(来源、命名、接受规则、"已不再提供");settings-service.ts里新的registerOptionSource/optionTableOf/servedManifest三段,以及保存和环境变量两处改成问optionTableOf的两行;settings-service-plugin.ts在注册清单之前注册这个来源的那一段。pnpm --filter @objectstack/service-settings exec vitest run --maxWorkers=2 src/localization-locale-options.test.ts,预期 14 条全绿,其中"后加的翻译项不重启即可保存"和"已不再提供的旧值照常可读并标注"两条。Fixes #30286
Clause-②: yes (narrowing) —
SettingsServicegainsregisterOptionSource(namespace, key, source)and the package exports theSettingsOptionSource,SettingsOptionSourceAnswerandSettingsServedOptiontypes;getNamespacetakes an optional thirddisplayargument. The save oflocalization.locale(PUT /api/settings/localization,SettingsService.setMany) and anOS_LOCALIZATION_LOCALEoverride now accept the i18n service's locale list instead of the declareden-US,zh-CN,ja-JP,es-ES: a language the deployment supports is accepted where it was refused (fr-FR,vi-VN), and one of the four the deployment does not support is refused where it was accepted (ja-JPon an app that declaresi18n.supportedLocales: ['en', 'zh-CN'], asexamples/app-showcaseandexamples/app-crmdo).Metadata-protocol: none
Skills-docs: none —
git grep -n "localization.locale" skills content/docsanswers one line,content/docs/permissions/authentication.mdx:474(SMS bodies fall back to the setting), which lists no values and stays true;skills/**names the setting nowhere.What changes
A settings key can serve its options from the running system (
SettingsService,settings-service.ts,settings-service.types.ts). Measured first: a settings option table was recorded once, from the manifest, atregisterManifest; thevalueDomainvocabulary is closed (three members) andbcp47_localeis refused by name inpackages/spec/src/system/settings-manifest.test.ts, because this key's options were meant to be the supported set, not a standard. So no existing mechanism and no spec change: a service-level registration,registerOptionSource(namespace, key, source). The source answers{ options, admits(value) }, orundefinedto leave the declared table standing.validatePatch) andOS_*override (effectiveEnvOverride) both ask one helper,optionTableOf, for the values offered and the judgment of a written value: the source's answer when it has one, else the declared table throughfirstRejectedOption, unchanged. Each site changes by two lines; the refusal is the existinginvalid_optionwithconstraint.allowedlisting the offered values. The TOUCH gate is unchanged, so a stored value is judged only by a save that writes it.auditEnvOverrides) skips the options family of a sourced key: the source reads a service that registers later in the boot, so a verdict there would come from the table it replaces. The override is judged when read, by the same helper (AGENTS.md, Startup registry reads).getNamespace, optionaldisplay.locale; theGET /api/settings/:namespaceroute passes the request's language, one line) serves a copy of the manifest whose sourced specifiers carry the source's options for the value in effect. The registered manifest is never changed.GET /api/settings(the manifest list) still serves the declared table.warn.localization.locale's source (localization-locale-options.ts, new). Offered codes = the i18n service'sgetLocales(), de-duplicated, in its order: the app's declaredi18n.supportedLocales, else every loaded bundle and authored translation item, the listGET /api/v1/i18n/localesserves to the language menu.PLATFORM_LOCALES, spec(system): export the platform locale table PLATFORM_LOCALES and its read aliases (phase 1 of #29428) #30001) for the language the code means throughplatformLocaleTagOf, elseIntl.DisplayNames([code])withfallback: 'none', capitalized, else the code. The console's language menu names locales the same way (ui/packages/app-shell/src/layout/LocaleSwitcher.tsx,localeLabel).en-USwhereenis offered,zh-CNwherezh-Hansis), through the table's read aliases. Without this the manifest defaulten-USwould not be a member on any deployment whose list saysen, which is all three examples. A code the table does not alias is accepted only as spelled (fr-FRis notfr).NAME (no longer offered)in the reader's language (en, zh-CN, ja-JP, es-ES), so the page shows it rather than a blank select.Wiring (
settings-service-plugin.ts).initregisters the source before the manifests, withctx.getService('i18n')asked at each use.Translations (
translations/zh-CN.ts). The bundle translated the four option names (英语(美国), 日语, …). The console's option-label lookup prefers the bundle over the served name, so those entries would have named each language in the reader's language; they are removed. The other three bundles carried none. The coverage guard already excludes option labels for this reason (settings-translation-coverage.test.tsheader).Manifest (
localization.manifest.ts). The declared four stay as the no-i18n fallback; a comment says what a running deployment serves instead.Readers of
localization.locale(the card's question)None breaks on a locale outside the four; each was read on
origin/mainfe835b4, not exercised end to end:vi-VNresolveLocalizationContext)packages/core/src/security/assemble-execution-context.ts,resolve-authz-context.tsIntlformats any BCP 47 tag.plugin-auth/src/auth-plugin.ts(applyLocalizationLocale),auth-manager.ts(normalizeAuthEmailLocale)EmailService.sendTemplate's ladder finds novi-VNrow and falls toen-US(plugin-email/src/email-service.ts).enstill maps toen-US.plugin-auth/src/phone-sms-texts.tsphoneSmsLocaleChainends aten; never renders nothing.packages/runtime/src/seed-locale.ts→metadata-protocol/src/seed-loader.ts(filterByLocale)plugin-audit/src/audit-plugin.ts,audit-writers.tst()falls back to the default locale.service-messaging/src/messaging-service-plugin.tsi18n.getDefaultLocale()at delivery, unset lands theen-USdefault. Unaffected.GET /auth/me/localizationplugin-hono-server/src/current-user-endpoints.tsTests
New
src/localization-locale-options.test.ts(14 tests), on a realLiteKernelboot ofSettingsServicePluginwith a stand-in i18n service whose list the cases change, through the real settings routes (GET/PUT /api/settings/:namespace):en-USin effect shown as theenoption; the registered manifest unchanged;fr-FRwhen offered; follows the list at runtime (vi-VNrefused, then accepted after the list grows, no reboot);ja-JP: 400SETTINGS_VALIDATION,invalid_optionatlocale,constraint.allowed: 'en, zh-CN', nothing written;zh-CN/en-USwherezh-Hans/enare offered, refusesfr-FRwherefris;ja-JPthe list drops: still the value, served as日本語 (已不再提供)for a zh-CN reader, a save offiscal_year_startpasses, a re-write ofja-JPis refused;OS_LOCALIZATION_LOCALE=vi-VNin force while offered, not in force once the list drops it; not reported by the registration-time audit while a declared-table key (fiscal_year_start) still is;warn;localeSelfName: table names, the code for an unnamed tag, a malformed tag and a runtime withoutIntl.DisplayNames, and the runtime's ICU name forvi-VN.Ablation through
scripts/ablation-replace.mjs:optionTableOfmade to ignore the source ⇒ 6 of 14 red; restored, blob equal toHEAD.Package:
pnpm --filter @objectstack/service-settings test69 files, 1170 passed;typecheckexit 0.Acceptance notes
ui/apps/console/src/pages/settings/SettingsField.tsxdraws aselectfrom the servedspec.optionsand names each throughlabels.optionLabel, which falls back to the served label when the bundle has no entry. Read, not changed (ui/**is out of scope).GET /api/settings/localization's servedlocaleoptions, or the i18n service'sgetLocales()server-side. No change here.localization-locale-options.test.ts); the settings routes' shared harness (settings-routes.test.ts) is not extended.GET /api/settings(the manifest list the Setup hub reads for its navigation) still carries the declared four; only the namespace read serves the live list.Verification
All at head
e1c377ef(the branch withorigin/mainmerged), afterpnpm install --frozen-lockfile(exit 0) and a full build (turbo run build --filter='!@objectstack/docs', 119/119, under the verify lock):pnpm --filter @objectstack/service-settings test: 69 files, 1170 passed.typecheck: exit 0.node scripts/pm/dispatch-gates.mjs --commandsderived 63 families from the change set; each run with its exit recorded before any pipe: 63 of 63 exit 0 (check-plugin-teardown-shape --self-testfirst answered exit 3, PREREQUISITE NOT MET, on the shallow clone; it exits 0 after fetching its pinned fixture commit).--ran: 63 derived, 63 run, 0 NOT-MEASURED, exit 0.pnpm lint: exit 0.pnpm --filter @objectstack/dogfood testunder the lock: exit 0, 419 files passed and 3 skipped, 3492 tests passed and 36 skipped.Generated by Claude Code