Repository navigation
fix(service-settings): judge OS_LOCALIZATION_LOCALE once the boot's locale list is whole, and one locale matcher for Setup and the IdP claim - #30589
Conversation
…til kernel:bootstrapped, correct stale auth comment 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>
Claude-Session: https://claude.ai/code/session_01Utp6CZEhtHfQH5TLEVe3TF Co-authored-by: Claude <noreply@anthropic.com>
… target Claude-Session: https://claude.ai/code/session_01Utp6CZEhtHfQH5TLEVe3TF Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 53 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 8b45fec8070f6445fcc0b7742105823aa69c2a4a && git checkout 8b45fec8070f6445fcc0b7742105823aa69c2a4a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2ffdb649775d20313707d88a8388275f280a0a7f 06b4c86ff25fa6025c44f77054d7ce298cacb487 && git checkout -B drift-repro 2ffdb649775d20313707d88a8388275f280a0a7f && git merge --no-ff 06b4c86ff25fa6025c44f77054d7ce298cacb487
node scripts/docs-audit/affected-docs.mjs --json 2ffdb649775d20313707d88a8388275f280a0a7f
|
|
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF 维护者速读
席位意见:ACCEPT(#30456)。用环境变量设的默认语言在启动时不再被误丢;Setup 和单点登录对同一种语言的不同写法认法一致。 席位核对Head-sha:
|
Requested by Bill · project thread
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF
维护者速读
OS_LOCALIZATION_LOCALE指定的语言如果要到kernel:ready期间才进语言列表(应用自建翻译项就是这时加载的),以前在kernel:ready早读的一方(auth 的邮件/短信默认语言)会把它判成 IGNORED 并丢掉;现在设置服务在kernel:bootstrapped之前不下"不在列表里"的结论,先让它生效,列表齐了再判一次,真不合法才报一次错并发变更事件让读方重读。②一个匹配器:Setup 默认语言的接受规则和 IdP 登录语言声明都改用 core 新导出的resolveOfferedLocale,两边接受同一批写法。③更正auth-manager.ts/auth-plugin.ts里"邮件模板四种语言就是该设置的选项"的过期注释。fr-FR(部署提供fr)而 IdP 声明fr-FR能落到fr,两扇门对同一种语言说法不一;注释描述的是 Default language in Setup offers the languages the deployment supports, instead of four hard-coded ones #30286 之前的世界。Clause-②: yes (widening)):Setup 现在还接受已提供语言的地区/基础写法和大小写变体(fr提供时的fr-FR、zh-CN的zh-cn),以前接受的值全部照旧。IdP 在同时提供zh-Hans/zh-Hant时把zh-TW落到繁体(以前落到先列出的简体)。一个真正不合法的OS_LOCALIZATION_LOCALE在kernel:ready期间会暂时生效,kernel:bootstrapped时报错并通知读方改回;一次性读方(应用内联种子在kernel:ready读)会用到这个暂时值,无法再通知它改回。回滚即撤本 PR。packages/core/src/fallbacks/memory-i18n.ts的resolveOfferedLocale;settings-service.ts的holdOptionSourceVerdicts/sealOptionSources/holdsOptionSourceVerdict三段和effectiveEnvOverride里改动的一行;settings-service-plugin.ts的两处接线。pnpm --filter @objectstack/service-settings exec vitest run --maxWorkers=2 src/localization-locale-options.test.ts,预期 18 条全绿,其中"read while the boot is still filling the locale list"三条是启动窗口的钉子。Fixes #30456
Clause-②: yes (widening) —
@objectstack/coreexportsresolveOfferedLocale(requested, offered);SettingsServicegainsholdOptionSourceVerdicts()andsealOptionSources(). The save oflocalization.locale(PUT /api/settings/localization,SettingsService.setMany) and anOS_LOCALIZATION_LOCALEoverride now also accept a region or base of an offered language (fr-FRwherefris offered,frwherefr-FRis) and another case of an offered code (zh-cnforzh-CN); nothing accepted before is refused. The IdP locale claim (resolveIdpLocale) lands a script or region spelling on the offered code the platform locale table says it means (zh-TWonzh-Hant, not on azh-Hanslisted first).Metadata-protocol: none
What changes
1. The boot window (
settings-service.ts,settings-service-plugin.ts)Measured on
origin/main8e5fabe:localization.locale's option source (#30286) reads the i18n service'sgetLocales(), and that list grows duringkernel:readywhen the i18n plugin syncs the app's authored translations (service-i18n/src/i18n-service-plugin.ts,replaceAuthoredTranslations). Auth'sapplyLocalizationLocale(plugin-auth/src/auth-plugin.ts) reads the key in its ownkernel:readyhandler. Whichever runs first decides: anOS_LOCALIZATION_LOCALE=vi-VNwhose language arrives as an authored translation could be judged against a list without it, reportedIGNOREDaterror, and auth kept the next layer's value for the rest of the process. That is the three-part shape AGENTS.md "Startup registry reads" names (a filling registry, a terminal "absent", recorded in a log and in the reader's state).Cure 3 of that section, seal then judge:
holdOptionSourceVerdicts():SettingsServicePlugin.initcalls it before registering the source. While held, anOS_*override on a sourced key that the source (or the declared table it stands in for, when the list is still empty) does not admit is taken in force, not reported, and remembered.effectiveEnvOverridechanges by one condition:if (rejected && !this.holdsOptionSourceVerdict(namespace, key)). The other families (pattern, value domain, bounds, format) do not depend on the list and are judged at once, as before.sealOptionSources(): the plugin calls it fromkernel:bootstrapped, which the kernel fires after everykernel:readyhandler. Each remembered key is judged again: one the list now admits stays in force and nothing is said; one it still does not admit is reported once through the existingreportRejectedEnvOverride(same message, same dedupe), and a settings change ({ namespace, key, scope: 'global', action: 'set' }) is emitted so a reader that applied the held value reads the key again. Auth's reader already subscribes tolocalizationchanges, so it re-applies without a code change.SettingsServiceconstructed without the plugin is never held and judges at once, as before.2. One matcher (
core,service-settings,plugin-auth)resolveOfferedLocale(requested, offered)inpackages/core/src/fallbacks/memory-i18n.ts, exported besideresolveLocale. It reusesresolveLocalerather than copying it:resolveLocale's exact and case-insensitive steps;platformLocaleTagOf, through the read aliases), sozh-TWiszh-Hant,zh-CNiszh-Hans,en-USisen;resolveLocaleitself for the base-language and variant steps.Step 2 is what Setup's old
admitsdid on its own, and it only changesresolveLocale's answer where two offered codes share a base language and the table tells them apart (zh-Hans/zh-Hant), whereresolveLocaletakes the first listed.localization-locale-options.ts):admits(v)isresolveOfferedLocale(v, codes) !== undefined, and a stored second spelling is served in the place of the code the matcher lands it on. The docblock's "fr-FRis notfr" is replaced by the matcher's rules.idp-locale-claim.ts):resolveIdpLocalecallsresolveOfferedLocaleinstead ofresolveLocale; the stored value is still the supported code in the column's BCP-47 case.3. The stale comment (
auth-manager.ts,auth-plugin.ts)AUTH_EMAIL_TEMPLATE_LOCALES' docblock said those four "are also the four options thelocalization.localesetting offers, so the two ends agree by construction". Since #30286 the setting offers the deployment's list. The docblock now says how they agree:normalizeAuthEmailLocalemaps a value onto a row spelling where one of the four means it and passes any other through, andEmailService.sendTemplateresolves it against the bundle's tags and falls back to theen-USrow (plugin-email/src/email-service.ts, #28880 ladder). The same claim in theapplyLocalizationLocalecomment ofauth-plugin.ts("whose four options are exactlyAUTH_EMAIL_TEMPLATE_LOCALES") is corrected the same way. Comments only.Tests
service-settings/src/localization-locale-options.test.ts(the component's shared harness, now 18): a new top-level describe boots aLiteKernelwith akernel:readyreader that runs before a stand-in i18n plugin grows its list in its ownkernel:readystep:OS_LOCALIZATION_LOCALE=vi-VN, list['en']gainingvi-VN: the reader's first read isvi-VNfromenv, every read staysvi-VN, and nothing is logged for the variable;OS_LOCALIZATION_LOCALE=xx-XX, never offered: held at the first read, then exactly oneIGNOREDreport and a re-read that lands on the manifest default;SettingsServicejudges at once; sealing it is a no-op.PUT/GETroutes:fr-FRwherefris offered (served infr's place),frwherefr-FRis,zh-cnwherezh-CNis,zh-TWwherezh-Hansandzh-Hantare (served inzh-Hant's place),de-DEstill refused. The old pin "fr-FRrefused wherefris offered" is the narrowing this card removes and is replaced.plugin-auth/src/idp-locale-claim.test.ts:zh-TW→zh-Hant,zh-CN→zh-Hanswith the scripts listed either way round,fr-FR→fr,de-DE→ unset.core/src/fallbacks/fallbacks.test.ts:resolveOfferedLocalestep by step, includingresolveLocale('zh-TW', ['zh-Hans','zh-Hant'])answeringzh-Hansbeside the new answerzh-Hant.Ablation through
scripts/ablation-replace.mjs, each restored after: the hold condition removed ⇒ both boot-window pins red; the seal'semitChangeremoved ⇒ the re-read pin red; thekernel:bootstrappedhook removed ⇒ 2 red (including the existing "not in force once the list drops it"); Setup'sadmitsback to exact codes ⇒ 2 red;resolveIdpLocaleon a first-match matcher ⇒ thezh-Hantpin red; the table step removed fromresolveOfferedLocale⇒ the core table pin red.Acceptance notes
packages/runtime/src/seed-locale.ts): the app plugin's inline seed reads the key in itskernel:readyhandler (app-plugin.ts), so it sees a held value. For an override the boot does offer, that is now the right value where it used to be the next layer's; for one the boot never offers, the seed uses it beforekernel:bootstrappedreports it, and a one-shot reader cannot be told to read again. When demo staff or seed files defer the inline seed tokernel:bootstrapped, its hook is registered duringkernel:ready, after the settings plugin's seal hook, so it reads after the seal.metadata:reloadedadding a translation item) is outside this card: by then the hold is sealed, and an override is judged at each read against the list of that moment, as Default language in Setup offers the languages the deployment supports, instead of four hard-coded ones #30286 left it.auth-prelude-query-count.pin.test.ts, seat 5's fix(platform-objects): name an organization membership, a two-factor enrollment and the SCIM join rows by the people and groups they point at #30520): untouched, no query added.Verification
All at head
06b4c86f(the branch withorigin/mainc23eafa merged), 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/core test: 135 files, 3017 passed;typecheckexit 0.pnpm --filter @objectstack/service-settings test: 69 files, 1175 passed;typecheckexit 0.pnpm --filter @objectstack/plugin-auth test: 224 files, 3985 passed and 10 skipped;typecheckexit 0.node scripts/pm/dispatch-gates.mjs --commandsderived 67 families from the change set; each run with its exit recorded before any pipe: 67 of 67 exit 0.--ran: 67 derived, 67 run, 0 NOT-MEASURED, exit 0.pnpm lint: exit 0.pnpm --filter @objectstack/dogfood testunder the lock: exit 0, 420 files passed and 3 skipped, 3496 tests passed and 36 skipped.Generated by Claude Code