Repository navigation
Commit bee9836
fix(service-settings)!: Setup's default language offers and accepts the languages the deployment supports, not four hard-coded ones (#30446)
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg21NveYnJ8STuX2gJsTgYaA)_
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF
## 维护者速读
- **改了什么:** Setup → 本地化
→「默认语言」不再写死四种语言,改成部署实际支持的语言:和用户右上角语言菜单读同一个来源(i18n 服务的 `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
条全绿,其中"后加的翻译项不重启即可保存"和"已不再提供的旧值照常可读并标注"两条。
- **需要你做的:** 这是 BREAKING 收窄,合入需要你本人同意(合约审查由席位安排)。
Fixes #30286
Clause-②: yes (narrowing) — `SettingsService` gains
`registerOptionSource(namespace, key, source)` and the package exports
the `SettingsOptionSource`, `SettingsOptionSourceAnswer` and
`SettingsServedOption` types; `getNamespace` takes an optional third
`display` argument. The save of `localization.locale` (`PUT
/api/settings/localization`, `SettingsService.setMany`) and an
`OS_LOCALIZATION_LOCALE` override now accept the i18n service's locale
list instead of the declared `en-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-JP` on an app that declares
`i18n.supportedLocales: ['en', 'zh-CN']`, as `examples/app-showcase` and
`examples/app-crm` do).
Metadata-protocol: none
Skills-docs: none — `git grep -n "localization.locale" skills
content/docs` answers 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, at `registerManifest`; the
`valueDomain` vocabulary is closed (three members) and `bcp47_locale` is
refused by name in `packages/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) }`, or `undefined` to leave the
declared table standing.
- **Save** (`validatePatch`) and **`OS_*` 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 through `firstRejectedOption`,
unchanged. Each site changes by two lines; the refusal is the existing
`invalid_option` with `constraint.allowed` listing the offered values.
The TOUCH gate is unchanged, so a stored value is judged only by a save
that writes it.
- **Registration-time env audit** (`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).
- **Page read** (`getNamespace`, optional `display.locale`; the `GET
/api/settings/:namespace` route 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.
- A source that throws is answered as absent (declared table) and logged
once per cause at `warn`.
**`localization.locale`'s source (`localization-locale-options.ts`,
new).** Offered codes = the i18n service's `getLocales()`,
de-duplicated, in its order: the app's declared `i18n.supportedLocales`,
else every loaded bundle and authored translation item, the list `GET
/api/v1/i18n/locales` serves to the language menu.
- **Names** in their own language: the platform locale table's name
(`PLATFORM_LOCALES`, #30001) for the language the code means through
`platformLocaleTagOf`, else `Intl.DisplayNames([code])` with `fallback:
'none'`, capitalized, else the code. The console's language menu names
locales the same way
(`ui/packages/app-shell/src/layout/LocaleSwitcher.tsx`, `localeLabel`).
- **Accepted:** each offered code, and a stored spelling of an offered
table language (`en-US` where `en` is offered, `zh-CN` where `zh-Hans`
is), through the table's read aliases. Without this the manifest default
`en-US` would not be a member on any deployment whose list says `en`,
which is all three examples. A code the table does not alias is accepted
only as spelled (`fr-FR` is not `fr`).
- **The value in effect:** a stored second spelling is served under its
own spelling so the select shows it chosen; a stored value no longer
offered is served as an extra option named `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.
- No i18n service, or one reporting nothing ⇒ the declared four stand.
**Wiring (`settings-service-plugin.ts`).** `init` registers the source
before the manifests, with `ctx.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.ts`
header).
**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/main`
fe835b4, not exercised end to end:
| Reader | Where | With `vi-VN` |
|:--|:--|:--|
| Execution context locale (number/date formatting,
`resolveLocalizationContext`) |
`packages/core/src/security/assemble-execution-context.ts`,
`resolve-authz-context.ts` | `Intl` formats any BCP 47 tag. |
| Auth email locale | `plugin-auth/src/auth-plugin.ts`
(`applyLocalizationLocale`), `auth-manager.ts`
(`normalizeAuthEmailLocale`) | Passed through unchanged;
`EmailService.sendTemplate`'s ladder finds no `vi-VN` row and falls to
`en-US` (`plugin-email/src/email-service.ts`). `en` still maps to
`en-US`. |
| Auth SMS bodies | `plugin-auth/src/phone-sms-texts.ts` |
`phoneSmsLocaleChain` ends at `en`; never renders nothing. |
| Seed dataset locale | `packages/runtime/src/seed-locale.ts` →
`metadata-protocol/src/seed-loader.ts` (`filterByLocale`) | Datasets
scoped to other locales are skipped and named in one log line; unscoped
ones load, as for any unmatched locale. |
| Activity summaries, @mention titles |
`plugin-audit/src/audit-plugin.ts`, `audit-writers.ts` | i18n `t()`
falls back to the default locale. |
| Messaging template locale |
`service-messaging/src/messaging-service-plugin.ts` | Names the setting
in a comment only; reads `i18n.getDefaultLocale()` at delivery, unset
lands the `en-US` default. Unaffected. |
| `GET /auth/me/localization` |
`plugin-hono-server/src/current-user-endpoints.ts` | Echoes the value;
the console shows English for a language with no console pack, as its
language menu already does. |
## Tests
New `src/localization-locale-options.test.ts` (14 tests), on a real
`LiteKernel` boot of `SettingsServicePlugin` with a stand-in i18n
service whose list the cases change, through the real settings routes
(`GET` / `PUT /api/settings/:namespace`):
- serves the list, named in each language, with `en-US` in effect shown
as the `en` option; the registered manifest unchanged;
- accepts `fr-FR` when offered; follows the list at runtime (`vi-VN`
refused, then accepted after the list grows, no reboot);
- refuses an unoffered `ja-JP`: 400 `SETTINGS_VALIDATION`,
`invalid_option` at `locale`, `constraint.allowed: 'en, zh-CN'`, nothing
written;
- accepts `zh-CN` / `en-US` where `zh-Hans` / `en` are offered, refuses
`fr-FR` where `fr` is;
- a stored `ja-JP` the list drops: still the value, served as `日本語
(已不再提供)` for a zh-CN reader, a save of `fiscal_year_start` passes, a
re-write of `ja-JP` is refused;
- empty list ⇒ the declared four;
- `OS_LOCALIZATION_LOCALE=vi-VN` in 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;
- no source ⇒ declared table; a throwing source ⇒ declared table, one
`warn`;
- `localeSelfName`: table names, the code for an unnamed tag, a
malformed tag and a runtime without `Intl.DisplayNames`, and the
runtime's ICU name for `vi-VN`.
Ablation through `scripts/ablation-replace.mjs`: `optionTableOf` made to
ignore the source ⇒ 6 of 14 red; restored, blob equal to `HEAD`.
Package: `pnpm --filter @objectstack/service-settings test` 69 files,
1170 passed; `typecheck` exit 0.
## Acceptance notes
- Setup's renderer needs no change:
`ui/apps/console/src/pages/settings/SettingsField.tsx` draws a `select`
from the served `spec.options` and names each through
`labels.optionLabel`, which falls back to the served label when the
bundle has no entry. Read, not changed (`ui/**` is out of scope).
- #30206 (first-run checklist language row) should read the same list:
`GET /api/settings/localization`'s served `locale` options, or the i18n
service's `getLocales()` server-side. No change here.
- The new component's pins are in its own test file
(`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 with `origin/main` merged), after
`pnpm 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 --commands` derived 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-test` first 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.
- Full dogfood (BREAKING, narrowing arm), `pnpm --filter
@objectstack/dogfood test` under the lock: exit 0, 419 files passed and
3 skipped, 3492 tests passed and 36 skipped.
- CI check-runs: not yet reported at opening.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6CZEhtHfQH5TLEVe3TF)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 8cc709d commit bee9836
10 files changed
Lines changed: 650 additions & 14 deletions
File tree
- .changeset
- packages/services/service-settings/src
- manifests
- translations
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
54 | 54 | | |
55 | 55 | | |
56 | 56 | | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
57 | 61 | | |
58 | 62 | | |
59 | 63 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
0 commit comments