Skip to content

Commit deabe3b

Browse files
fix(settings): settings copy is written in all eleven platform languages, zh-TW reads Traditional, and sms/test answers in the caller's language (#30630) (#30734)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9D9ZFYWnf6P6tsGrp2D14eMY)_ Fixes #30630 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:设置里 7 张"按调用者语言写"的文案表(IP 白名单拒存、登录方式下限、两处 MFA 拒存与开启提示、邮件实时投递状态与测试邮件答复、默认币种锁定、"已不再提供")补齐平台 11 种语言;"发送测试短信"改为按调用者语言答复;设置服务自带的"重置"结果与"未注册处理程序"也按语言答复。 - 为什么改:这些表只有 4 种(邮件只有 2 种)语言,法语、德语、韩语等用户看到英文;繁体中文(zh-TW)用户被回成简体;测试短信从不读语言,永远英文。 - 风险与回滚:只改文案与选语言的查表,不改任何判定、错误码或接口;英文原句逐字不变。回滚即撤本 PR。 - 先看:`packages/services/service-sms/src/sms-plugin.ts`(测试短信接上语言);`packages/services/service-settings/src/settings-service.ts` 的 `runAction` 两处内置答复;`packages/plugins/plugin-auth/src/settings-copy-locale.test.ts`(逐表 11 语言 pin)。 - 两分钟验证:`pnpm --filter @objectstack/service-sms exec vitest run src/sms-test-recipient.pin.test.ts`,预期全绿,其中 zh-TW 的测试短信答复以「測試簡訊已傳送至」开头。 - 需要你做的:无,席位落地。 ## What changes Every module-local settings copy table (sentences composed per request, which the console's translation bundle cannot carry) now has one entry per language on the platform locale table (`PLATFORM_LOCALES`: `en`, `zh-Hans`, `zh-Hant`, `es`, `ar`, `fr`, `pt-BR`, `ru`, `de`, `ja`, `ko`). The four languages the tables served first keep their codes (`zh-CN`, `ja-JP`, `es-ES`); the rest use the table's tags, the shape `storage/test` uses (PR #30627). Translations are real text in every language, with no English fill. The seven tables the card names, measured on `origin/main` (`grep` for the four-locale key sets and for `resolveLocale` callers in the three packages): | Package | Table | Before | | --- | --- | --- | | `plugin-auth` | `allowed-ip-ranges.ts` save refusal (now exported as `ALLOWED_IP_RANGES_REFUSAL_COPY` for its pin) | 4 | | `plugin-auth` | `sign-in-method-floor.ts` `SIGN_IN_FLOOR_COPY` | 4 | | `plugin-auth` | `mfa-required-enrollment-predicate.ts` `MFA_REQUIRED_ENROLLMENT_COPY` | 4 | | `plugin-auth` | `organization-mfa-enrollment-guard.ts` refusal + save notice (two tables, one lookup) | 4 | | `plugin-email` | `live-transport-status.ts` `MAIL_DELIVERY_COPY` | 2 (`en`, `zh-CN`) | | `service-settings` | `localization-currency-lock.ts` `CURRENCY_LOCK_COPY` | 4 | | `service-settings` | `localization-locale-options.ts` `NOT_OFFERED_COPY` | 4 | The lookup stays core's `resolveLocale`. #30538 is closed and its fix landed: `resolveLocale` is now `resolveLocaleAmong`, which reads a code through the platform table's read aliases before the base-language and variant steps. So with a `zh-Hant` key present, `zh-TW` / `zh-HK` / `zh-MO` reach it and never `zh-CN`; `pt` reaches `pt-BR`; a code naming no table language answers `undefined`, which every table serves in English. No per-table copy of the reader was added. - **`sms/test`** (`service-sms`): the handler now reads the action input's `locale` (the route passes `Accept-Language`, else `?locale`) and writes every answer from a new eleven-language table, `sms-test-copy.ts`: the five recipient refusals, missing credential, transport build failure, log-only warning, sent, and failed. A provider's or the engine's own error text passes through verbatim, behind a lead-in outside English. The English sentences are byte-identical to before. - **The two built-in settings answers** (`service-settings`, `SettingsService.runAction`): the built-in `reset` result and "No handler registered for action ..." are written from a new eleven-language table, `settings-action-copy.ts`, in `display.locale`. English unchanged. - One type per package names the eleven keys (`settings-copy-locale.ts` in `plugin-auth` and `service-settings`, `MailDeliveryCopyLocale`, `SmsTestCopyLocale`), so a table missing a language fails `tsc`. Arabic and Russian counts (MFA grace days, currency usage, reset count) use their plural forms. ## Why A Setup caller in French, German, Korean, Arabic, Russian, Brazilian Portuguese or Traditional Chinese read these sentences in English (or, for `zh-TW`, in Simplified Chinese), and `sms/test` ignored the caller's language entirely. The eleven-language rule (ruling F of #29349) holds every surface to the platform table. ## Tests - New pins, one per table, each asserting: exactly one entry per platform table language; every field present and non-empty in every entry and different from English (no fill); each table tag and each stored alias (`zh-TW`, `zh-HK`, `zh-CN`, `zh`, `ja-JP`, `es-ES`, `pt`, `en-US`, `fr-CA`, `de-AT`) served its own language; `zh-TW` never served the Simplified entry; an off-table code (`xx`, `sw-KE`) and no code served English. - `plugin-auth/src/settings-copy-locale.test.ts` (5 tables), plus Russian/Arabic plural cells. - `plugin-email/src/live-transport-status.pin.test.ts` (new top-level `describe` in the existing harness). - `service-settings/src/settings-copy-locale.test.ts` (currency lock, not-offered, built-in action answers). - `service-sms/src/sms-test-copy.test.ts`, including the English sentences pinned byte-identical. - Served doors: `service-sms/src/sms-test-recipient.pin.test.ts` drives the real settings route with an `Accept-Language` header: `zh-TW,zh;q=0.9` answers "測試簡訊已傳送至 …,經由 twilio(id=SM_test)。"; `fr`, `de-DE`, `ko` refusals are in those languages and send nothing; no header and `sw-KE` stay English. `service-settings/src/settings-service.test.ts` pins the reset result in `zh-TW` and `ru` and the no-handler refusal in `de-DE`, English for none / `sw-KE`. - Updated pins that encoded the four-language state: the currency-lock coverage pin (was "exactly the bundle's four locales") and its `fr-FR` row (was English); the email control (`fr-FR` was English, now `sw-KE`); the `mfa_required` and organization-MFA unknown-locale cells (`fr-FR` → `sw-KE`, `fr-FR` now French, `zh-TW` added); the organization notice's "1 is a digit in every language" cell accepts Arabic's word form for one day ("يوم واحد"). ## Acceptance notes - **Overlap with open PR #30448** (`allowed-ip-ranges.ts`): it rewrites the `recovery` sentence in the four languages it had. This PR translates the current one-step recovery into the seven new languages. Whichever lands second owes the four-step recovery in all eleven; a text conflict on the `es-ES` hunk is possible. - **`zh-Hant-TW` / `zh-Hans-CN` are not read through the table** by the shared resolver: `resolveLocaleAmong`'s table step looks the whole code up, misses, and the variant step returns the first `zh*` key, `zh-CN`. A caller whose display language is `zh-Hant-TW` therefore still reads Simplified here and in every other bundle the resolver serves. The resolver lives in `packages/spec` (outside this card's file surface); reported to the seat, not fixed here. - Labels the copy names on screen ("Send test SMS", "Setup → Authentication", the mail provider "None (log only)") come from the settings translation bundle, which ships four languages (#30512's surface). In the seven new languages the copy describes those controls instead of quoting a label the console does not show in that language. - The SMS body sent to the phone (`ObjectStack SMS test message.`) is unchanged: it is the message, not the answer. ## Verification Head `b1014f4d` (`a54649c1` the fix, `c3f79fc1` a merge of `origin/main` at `badf24df`, then two test-only commits). Readings under `scripts/pm/os-verify-lock.sh` on a shared box. <details><summary>Repro on origin/main (base 4bf9a2d)</summary> With the nine edited sources restored to the base (`git restore --source=4bf9a2d7`, tests from this branch), restored afterwards with `git checkout HEAD -- packages` and `git diff HEAD` empty: - `signInFloorCopy('zh-TW')` is the `zh-CN` entry (Simplified); `signInFloorCopy('fr')` is English; the table's keys are `en,zh-CN,ja-JP,es-ES`. - `sms/test` through the served route with `Accept-Language: zh-TW,zh;q=0.9` answered `Sent test SMS to +8613*******01 via twilio (id=SM_test).` - The new pins on the base: plugin-auth 22 failed / 27, service-sms 4 failed (served door), service-settings 8 failed / 156, plugin-email 4 failed / 21. </details> <details><summary>Ablations (scripts/ablation-replace.mjs, anchor 1 → 0, restored to the HEAD blob, git diff HEAD empty; tests import src/)</summary> | Ablated | Red | | --- | --- | | `sign-in-method-floor.ts`: the lookup reads only the four old keys (`resolveLocale(locale, ['en','zh-CN','ja-JP','es-ES'])`) | 2 of 26 in `settings-copy-locale.test.ts` (tags/aliases served their own language; zh-TW never Simplified) | | `sms-plugin.ts`: the handler ignores the input locale (`smsTestCopy(undefined)`) | 4 of 16 in `sms-test-recipient.pin.test.ts` (zh-TW sent, fr / de-DE / ko refusals) | </details> <details><summary>Suites and typecheck</summary> After the merge of `origin/main`: closure build of the four packages (`pnpm --filter 'PKG^...' … build`) exit 0, then per package typecheck (with `check:test-typecheck`) and `pnpm --filter PKG test`: - `@objectstack/plugin-auth`: typecheck exit 0; Test Files 228 passed (228), Tests 4088 passed, 10 skipped (4098). - `@objectstack/plugin-email`: typecheck exit 0; Test Files 48 passed (48), Tests 805 passed (805). - `@objectstack/service-settings`: typecheck exit 0; Test Files 70 passed (70), Tests 1195 passed (1195). - `@objectstack/service-sms`: on `b1014f4d` typecheck exit 0 (`check:test-typecheck: OK`); Test Files 9 passed (9), Tests 119 passed (119). </details> <details><summary>Gates</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 63 commands on `b1014f4d`; each run with its exit code recorded before any pipe. `--ran`: 63 derived, 62 run, 1 NOT-MEASURED, 0 UNRUN. NOT MEASURED: `pnpm check:dual-build-cjs-loads` (exit 3, PREREQUISITE NOT MET: it needs a whole-workspace build; CI runs it). `check:test-source-alias` first went red on the new `sms/test` copy pin (it read `@objectstack/spec/system` from the build); fixed by the anchored source alias in `service-sms/vitest.config.ts`, then exit 0. Every other command exited 0. `pnpm lint` exit 0. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_016WV4d1JeXeWGVzxc12FsUx)_ <!-- signed: domain:server#5 · os-elon-musk · session_016WV4d1JeXeWGVzxc12FsUx --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4e8bbbf commit deabe3b

25 files changed

Lines changed: 1679 additions & 71 deletions
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/plugin-auth': patch
3+
'@objectstack/plugin-email': patch
4+
'@objectstack/service-settings': patch
5+
'@objectstack/service-sms': patch
6+
---
7+
8+
fix(settings): settings refusals, live statements and action answers are written in every platform language, and `zh-TW` reads Traditional Chinese
9+
10+
Clause-②: no
11+
Metadata-protocol: none
12+
13+
- **Every module-local settings copy table carries the eleven platform languages** (the platform locale table: English, Simplified and Traditional Chinese, Spanish, Arabic, French, Brazilian Portuguese, Russian, German, Japanese, Korean). Before, they carried four (Setup → Email's live statement two), so a caller in any other language read English. Covered: the allowed-IP-ranges save refusal, the sign-in method floor refusals, the `auth.mfa_required` refusal and the organization MFA refusal and save notice (`@objectstack/plugin-auth`); Setup → Email's live-transport statement and "Send test email" answers (`@objectstack/plugin-email`); the default-currency lock and the "no longer offered" locale option (`@objectstack/service-settings`).
14+
- **`zh-TW`, `zh-HK` and `zh-MO` callers read Traditional Chinese.** Each table looks the caller's code up with core's `resolveLocale`, which reads it through the platform locale table's read aliases; a code naming no language on the table is answered in English, as before.
15+
- **"Send test SMS" (`sms/test`, `@objectstack/service-sms`) answers in the caller's language** (`Accept-Language`, else `?locale`), like the mail and storage connection tests. It read no locale before and answered English in every console language. A provider's own error text still passes through verbatim.
16+
- **The two answers `SettingsService.runAction` writes itself** (the built-in `reset` result and the refusal for an action with no registered handler) are written in the caller's language. With no locale, or one off the platform table, every sentence is the English one callers read before.

‎packages/plugins/plugin-auth/src/allowed-ip-ranges.ts‎

Lines changed: 84 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
*/
3737

3838
import { resolveLocale } from '@objectstack/core';
39+
import type { SettingsCopyLocale } from './settings-copy-locale.js';
3940
import { ipMatchesRange, type AuthManager } from './auth-manager.js';
4041
import { FORWARDED_CLIENT_ADDRESS_HEADERS } from './client-address-forwarding.js';
4142

@@ -111,14 +112,18 @@ export interface AllowedIpRangesWriteRefusal {
111112
constraint: { clientAddress: string | null };
112113
}
113114

114-
interface RefusalCopy {
115+
export interface AllowedIpRangesRefusalCopy {
115116
outside(address: string): string;
116117
undetermined: string;
117118
recovery: string;
118119
}
119120

120-
/** One entry per locale the settings bundle ships. */
121-
const COPY: Record<'en' | 'zh-CN' | 'ja-JP' | 'es-ES', RefusalCopy> = {
121+
/**
122+
* One entry per language on the platform locale table, the four served first
123+
* under their older codes; `resolveLocale` reads a caller's code through that
124+
* table (`zh-TW` → `zh-Hant`).
125+
*/
126+
export const ALLOWED_IP_RANGES_REFUSAL_COPY: Record<SettingsCopyLocale, AllowedIpRangesRefusalCopy> = {
122127
en: {
123128
outside: (address) =>
124129
`No entry in this list matches ${address}, the address you are connected from, so saving it would refuse your `
@@ -158,13 +163,85 @@ const COPY: Record<'en' | 'zh-CN' | 'ja-JP' | 'es-ES', RefusalCopy> = {
158163
+ 'dirección el servidor pueda determinar.',
159164
recovery: ` Si queda bloqueado, inicie el servidor con ${ALLOWED_IP_RANGES_ENV} con un valor vacío.`,
160165
},
166+
'zh-Hant': {
167+
outside: (address) =>
168+
`這份清單中沒有任何一項與你目前連線所用的位址 ${address} 相符,儲存後你自己下一次登入就會被拒絕。`
169+
+ '請把你的位址或所在網段加進清單,或在清單能相符的位址上儲存。',
170+
undetermined:
171+
'伺服器無法確定你目前連線所用的位址;只要設定了清單,這樣的請求都會被拒絕,你自己下一次登入也不例外。'
172+
+ '請在伺服器能確定位址的連線上儲存。',
173+
recovery: `如果已被鎖在外面,可將環境變數 ${ALLOWED_IP_RANGES_ENV} 設為空值後重新啟動伺服器。`,
174+
},
175+
ar: {
176+
outside: (address) =>
177+
`لا يطابق أي إدخال في هذه القائمة العنوان ${address} الذي تتصل منه، لذا فإن حفظها سيرفض تسجيل دخولك التالي `
178+
+ 'أنت نفسك. أضف عنوانك أو نطاقه إلى القائمة، أو احفظ القائمة من عنوان تطابقه.',
179+
undetermined:
180+
'لا يستطيع الخادم تحديد العنوان الذي تتصل منه، وما دامت هناك قائمة مضبوطة فإنه يرفض كل طلب من هذا النوع، '
181+
+ 'بما في ذلك تسجيل دخولك التالي. احفظ القائمة من اتصال يستطيع الخادم تحديد عنوانه.',
182+
recovery: ` إذا مُنعت من الدخول، فشغّل الخادم مع ضبط ${ALLOWED_IP_RANGES_ENV} على قيمة فارغة.`,
183+
},
184+
fr: {
185+
outside: (address) =>
186+
`Aucune entrée de cette liste ne correspond à ${address}, l’adresse depuis laquelle vous êtes connecté : `
187+
+ 'l’enregistrer refuserait votre propre prochaine connexion. Ajoutez votre adresse ou sa plage à la liste, ou '
188+
+ 'enregistrez la liste depuis une adresse qu’elle autorise.',
189+
undetermined:
190+
'Le serveur ne parvient pas à établir l’adresse depuis laquelle vous êtes connecté et, tant qu’une liste est '
191+
+ 'définie, il refuse toutes ces requêtes, y compris votre propre prochaine connexion. Enregistrez la liste '
192+
+ 'depuis une connexion dont le serveur peut établir l’adresse.',
193+
recovery: ` Si vous êtes bloqué, démarrez le serveur avec ${ALLOWED_IP_RANGES_ENV} défini sur une valeur vide.`,
194+
},
195+
'pt-BR': {
196+
outside: (address) =>
197+
`Nenhuma entrada desta lista corresponde a ${address}, o endereço do qual você está conectado, então salvá-la `
198+
+ 'recusaria o seu próprio próximo login. Adicione o seu endereço ou a faixa dele à lista, ou salve a lista a '
199+
+ 'partir de um endereço que ela aceite.',
200+
undetermined:
201+
'O servidor não consegue determinar o endereço do qual você está conectado e, enquanto houver uma lista '
202+
+ 'definida, recusa todas essas solicitações, inclusive o seu próprio próximo login. Salve a lista a partir de '
203+
+ 'uma conexão cujo endereço o servidor consiga determinar.',
204+
recovery: ` Se você ficar bloqueado, inicie o servidor com ${ALLOWED_IP_RANGES_ENV} definido como um valor vazio.`,
205+
},
206+
ru: {
207+
outside: (address) =>
208+
`Ни одна запись этого списка не соответствует адресу ${address}, с которого вы подключены, поэтому после `
209+
+ 'сохранения ваш собственный следующий вход будет отклонён. Добавьте в список свой адрес или его диапазон '
210+
+ 'либо сохраните список с адреса, который ему соответствует.',
211+
undetermined:
212+
'Сервер не может определить адрес, с которого вы подключены, и, пока задан любой список, отклоняет все такие '
213+
+ 'запросы, включая ваш собственный следующий вход. Сохраните список через подключение, адрес которого сервер '
214+
+ 'может определить.',
215+
recovery: ` Если доступ уже потерян, запустите сервер с пустым значением ${ALLOWED_IP_RANGES_ENV}.`,
216+
},
217+
de: {
218+
outside: (address) =>
219+
`Kein Eintrag dieser Liste passt zu ${address}, der Adresse, von der aus Sie verbunden sind. Nach dem Speichern `
220+
+ 'würde daher Ihre eigene nächste Anmeldung abgewiesen. Fügen Sie Ihre Adresse oder deren Bereich der Liste '
221+
+ 'hinzu oder speichern Sie die Liste von einer Adresse aus, die sie zulässt.',
222+
undetermined:
223+
'Der Server kann die Adresse, von der aus Sie verbunden sind, nicht ermitteln, und solange eine Liste gesetzt '
224+
+ 'ist, weist er alle solchen Anfragen ab, auch Ihre eigene nächste Anmeldung. Speichern Sie die Liste über eine '
225+
+ 'Verbindung, deren Adresse der Server ermitteln kann.',
226+
recovery: ` Wenn Sie ausgesperrt sind, starten Sie den Server mit ${ALLOWED_IP_RANGES_ENV} auf einem leeren Wert.`,
227+
},
228+
ko: {
229+
outside: (address) =>
230+
`이 목록에는 현재 접속한 주소 ${address}와 일치하는 항목이 없어, 저장하면 본인의 다음 로그인이 거부됩니다. `
231+
+ '본인의 주소나 그 대역을 목록에 추가하거나, 목록과 일치하는 주소에서 저장하세요.',
232+
undetermined:
233+
'서버가 현재 접속한 주소를 확인할 수 없으며, 목록이 설정되어 있는 동안에는 이러한 요청을 모두 거부합니다. '
234+
+ '본인의 다음 로그인도 마찬가지입니다. 서버가 주소를 확인할 수 있는 연결에서 목록을 저장하세요.',
235+
recovery: ` 접속이 차단되었다면 ${ALLOWED_IP_RANGES_ENV}를 빈 값으로 설정한 채 서버를 시작하세요.`,
236+
},
161237
};
162238

163-
const COPY_LOCALES = Object.keys(COPY);
239+
const COPY_LOCALES = Object.keys(ALLOWED_IP_RANGES_REFUSAL_COPY);
164240

165-
function copyFor(locale: string | undefined): RefusalCopy {
241+
/** The refusal copy for a caller's locale: through the platform locale table, else English. */
242+
export function allowedIpRangesRefusalCopy(locale: string | undefined): AllowedIpRangesRefusalCopy {
166243
const picked = locale ? resolveLocale(locale, COPY_LOCALES) : undefined;
167-
return COPY[(picked ?? 'en') as keyof typeof COPY];
244+
return ALLOWED_IP_RANGES_REFUSAL_COPY[(picked ?? 'en') as SettingsCopyLocale];
168245
}
169246

170247
/**
@@ -184,7 +261,7 @@ export function allowedIpRangesWritePredicate(
184261
if (!mgr) return undefined;
185262
const address = judgedClientAddress(mgr, transport);
186263
if (address && ranges.some((range) => ipMatchesRange(address, range))) return undefined;
187-
const copy = copyFor(locale);
264+
const copy = allowedIpRangesRefusalCopy(locale);
188265
return {
189266
message: (address ? copy.outside(address) : copy.undetermined) + copy.recovery,
190267
constraint: { clientAddress: address ?? null },

‎packages/plugins/plugin-auth/src/mfa-required-enrollment-predicate.test.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,8 +225,9 @@ describe('the global mfa_required switch needs two-factor enrollment', () => {
225225
expect(off({ next: null, current: true })).toBeUndefined();
226226
expect(off({ next: true, current: 'true' })).toBeUndefined();
227227
expect(mfaRequiredEnrollmentPredicate(() => true)({ next: true, current: false })).toBeUndefined();
228-
// Unknown locales fall back to English.
229-
expect(off({ next: true, current: false, locale: 'fr-FR' })?.message).toBe(MFA_REQUIRED_ENROLLMENT_COPY.en);
228+
// A language off the platform locale table falls back to English; French is on it.
229+
expect(off({ next: true, current: false, locale: 'sw-KE' })?.message).toBe(MFA_REQUIRED_ENROLLMENT_COPY.en);
230+
expect(off({ next: true, current: false, locale: 'fr-FR' })?.message).toBe(MFA_REQUIRED_ENROLLMENT_COPY.fr);
230231
});
231232

232233
it('a settings service without write predicates is said at warn, only while the pin makes it matter', () => {

‎packages/plugins/plugin-auth/src/mfa-required-enrollment-predicate.ts‎

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747

4848
import { resolveLocale } from '@objectstack/core';
4949
import { resolveTwoFactorEnabled } from './auth-manager.js';
50+
import type { SettingsCopyLocale } from './settings-copy-locale.js';
5051

5152
/** The `auth` settings key this guards. */
5253
export const MFA_REQUIRED_SETTING_KEY = 'mfa_required';
@@ -71,7 +72,13 @@ export function twoFactorEnrollableWithSwitchOn(): boolean {
7172
return resolveTwoFactorEnabled({ twoFactor: true });
7273
}
7374

74-
/** The refusal, per language. */
75+
/**
76+
* The refusal, per language: one entry per language on the platform locale
77+
* table (`@objectstack/spec/system` `PLATFORM_LOCALES`), the four this table
78+
* served first under their older codes (`zh-CN`, `ja-JP`, `es-ES`). Core's
79+
* `resolveLocale` reads the caller's code through that table, so `zh-TW`
80+
* finds the Traditional entry and `pt` the Brazilian one.
81+
*/
7582
export const MFA_REQUIRED_ENROLLMENT_COPY = {
7683
en:
7784
'Multi-factor authentication cannot be required on this deployment yet: OS_AUTH_TWO_FACTOR is set to off, ' +
@@ -89,7 +96,38 @@ export const MFA_REQUIRED_ENROLLMENT_COPY = {
8996
'desactivado y prevalece sobre este ajuste, así que nadie podría registrar una aplicación de autenticación y ' +
9097
'el requisito no se aplicaría. Establezca OS_AUTH_TWO_FACTOR=true (o elimínelo) en la implementación, ' +
9198
'reinicie y después active esta opción.',
92-
} as const;
99+
'zh-Hant':
100+
'本部署暫時無法強制多因素驗證:OS_AUTH_TWO_FACTOR 被設為關閉,它的優先順序高於本設定,' +
101+
'因此沒有人能綁定驗證器應用程式,這項要求也不會生效。請先在部署上設定 OS_AUTH_TWO_FACTOR=true(或刪除它)並重新啟動,再開啟此項。',
102+
ar:
103+
'لا يمكن فرض المصادقة متعددة العوامل على هذا النشر بعد: تم ضبط OS_AUTH_TWO_FACTOR على الإيقاف، وهو يتقدّم ' +
104+
'على هذا الإعداد، لذا لن يتمكن أحد من تسجيل تطبيق مصادقة ولن يُطبَّق هذا الشرط. اضبط OS_AUTH_TWO_FACTOR=true ' +
105+
'(أو احذفه) على النشر وأعد تشغيله، ثم فعّل هذا الخيار.',
106+
fr:
107+
'L’authentification multifacteur ne peut pas encore être exigée sur ce déploiement : OS_AUTH_TWO_FACTOR est ' +
108+
'désactivé et prévaut sur ce paramètre, si bien que personne ne pourrait enregistrer d’application ' +
109+
'd’authentification et l’exigence ne serait pas appliquée. Définissez OS_AUTH_TWO_FACTOR=true (ou supprimez-le) ' +
110+
'sur le déploiement et redémarrez, puis activez cette option.',
111+
'pt-BR':
112+
'Ainda não é possível exigir a autenticação multifator nesta implantação: OS_AUTH_TWO_FACTOR está desativado e ' +
113+
'prevalece sobre esta configuração, então ninguém conseguiria cadastrar um aplicativo autenticador e a exigência ' +
114+
'não seria aplicada. Defina OS_AUTH_TWO_FACTOR=true (ou remova-o) na implantação e reinicie; depois, ative esta ' +
115+
'opção.',
116+
ru:
117+
'Пока нельзя требовать многофакторную аутентификацию в этом развёртывании: OS_AUTH_TWO_FACTOR выключен и имеет ' +
118+
'приоритет над этой настройкой, поэтому никто не сможет подключить приложение-аутентификатор и требование не ' +
119+
'будет действовать. Установите OS_AUTH_TWO_FACTOR=true (или удалите переменную) в развёртывании и перезапустите ' +
120+
'его, затем включите этот параметр.',
121+
de:
122+
'Die Multi-Faktor-Authentifizierung kann in dieser Bereitstellung noch nicht verlangt werden: OS_AUTH_TWO_FACTOR ' +
123+
'ist ausgeschaltet und hat Vorrang vor dieser Einstellung, sodass niemand eine Authenticator-App einrichten ' +
124+
'könnte und die Anforderung nicht durchgesetzt würde. Setzen Sie OS_AUTH_TWO_FACTOR=true (oder entfernen Sie die ' +
125+
'Variable) in der Bereitstellung, starten Sie neu und schalten Sie diese Option danach ein.',
126+
ko:
127+
'이 배포에서는 아직 다단계 인증을 필수로 설정할 수 없습니다. OS_AUTH_TWO_FACTOR가 꺼짐으로 설정되어 있고 이 설정보다 ' +
128+
'우선하므로, 아무도 인증 앱을 등록할 수 없고 이 요구 사항도 적용되지 않습니다. 배포에서 OS_AUTH_TWO_FACTOR=true로 ' +
129+
'설정하거나 이를 삭제한 뒤 다시 시작하고, 그다음 이 옵션을 켜세요.',
130+
} as const satisfies Record<SettingsCopyLocale, string>;
93131

94132
const COPY_LOCALES = Object.keys(MFA_REQUIRED_ENROLLMENT_COPY);
95133

‎packages/plugins/plugin-auth/src/organization-mfa-enrollment-guard.test.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ describe('#30411 the refusal is in the caller\'s language, with the same field-l
436436

437437
it.each([
438438
['zh', 'zh-CN'], ['zh-cn', 'zh-CN'], ['zh-Hans-CN', 'zh-CN'], ['ja', 'ja-JP'], ['es-MX', 'es-ES'],
439-
['fr-FR', 'en'], ['', 'en'],
439+
['fr-FR', 'fr'], ['zh-TW', 'zh-Hant'], ['sw-KE', 'en'], ['', 'en'],
440440
] as const)('the request locale %j reads the %s copy (exact, then by language, else English)', (asked, served) => {
441441
expect((organizationMfaWithoutEnrollmentRefusal(asked) as any).message).toBe(ORGANIZATION_MFA_REFUSAL_COPY[served]);
442442
});
@@ -493,7 +493,9 @@ describe('#30411 an admitted turn-on answers the grace period in force as its sa
493493
for (const [locale, copy] of Object.entries(ORGANIZATION_MFA_REQUIRED_NOTICE_COPY)) {
494494
expect(copy(0), locale).toMatch(/\b0\b/);
495495
expect(copy(0), locale).not.toBe(copy(1));
496-
expect(copy(1), locale).toMatch(/\b1\b/);
496+
// Arabic writes one day as a word, as its grammar does ("يوم واحد"); every other language as a digit.
497+
if (locale === 'ar') expect(copy(1), locale).toContain('يوم واحد');
498+
else expect(copy(1), locale).toMatch(/\b1\b/);
497499
expect(copy(45), locale).toMatch(/\b45\b/);
498500
expect(copy(45), locale).not.toMatch(/#\d/);
499501
}

0 commit comments

Comments
 (0)