Skip to content

Commit 8ec00cf

Browse files
fix(platform-objects): Setup's user screens show a user's real state, and the copy-password line only when one was generated (#28065)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DB7otGmwMKQJqKhPxGv4D83)_ Fixes #27858 Fixes #27859 Fixes #27863 Part of #27869 Clause-②: no Metadata-protocol: none Skills-docs: none — `git grep` of `skills/**` and `content/docs/**` for the four touched strings ("temporary password was generated", "copy it now", "Preferred language for this user", `all_users`) has zero hits. ## 维护者速读 - 改了什么:设置 → 用户的四处界面文案与布局:创建用户/设置密码的结果框只在生成了临时密码时才提示「立即复制」;用户详情页显示已保存的语言和封禁状态与原因;用户列表加「已封禁」「平台角色」两列;「语言」字段的说明改为它决定界面语言(四种语言包同改)。 - 为什么改:HotCRM 18.2.0 设置走查发现管理员看到的是错的或缺的状态:手输密码后仍被要求复制不存在的密码、被封禁的用户看起来正常、列表上认不出封禁用户和管理员、语言说明只说通知。 - 风险与回滚:只改 `platform-objects` 的 sys_user 元数据、详情页定义和翻译包,无契约、无存储、无接口变化;回滚即还原本 PR。 - 先看:`packages/platform-objects/src/identity/sys-user.object.ts`(结果框与列表列)、`packages/platform-objects/src/pages/sys-user.page.ts`(Details 分区)、`packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts`。 - 两分钟验证:`pnpm --filter @objectstack/platform-objects exec vitest run --maxWorkers=2 src/identity/create-user-result-dialog-copy.test.ts src/pages/sys-user-details-tab.test.ts src/identity/sys-user-list-status-role-columns.test.ts src/identity/sys-user-locale-help.test.ts`,预期 4 个文件全绿。 - 需要你做的:无,席位落地。 ## What changes One commit per card, all on the platform's own `sys_user` metadata in `@objectstack/platform-objects`: 1. **#27858 — Create User / Set Password result.** Both result dialogs drop their dialog-level `description` ("If a temporary password was generated, copy it now — it is shown only once and never stored."). `ActionSchema.resultDialog` has no per-outcome copy, so that sentence showed on both outcomes, including a typed password, where nothing was generated. The instruction is now the label of the `temporaryPassword` field ("Temporary password: copy it now, it is shown only once and never stored"). The dialog skips a field whose path is absent from the response, and the endpoint returns `temporaryPassword` only when it minted one, so the line shows exactly when there is a password to copy. The zh-CN, ja-JP and es-ES packs carry the same change (description leaves dropped by `pnpm i18n:extract`, field labels translated by hand). 2. **#27859 — user record, Details tab.** The details renderer draws only authored sections, and none named `locale`, while `banned`, `ban_reason` and `ban_expires` were in `hideFields`. `locale` joins the Identity section, and a new Account Status section (inline labels in all four locales) carries `banned`, `ban_reason` and `ban_expires`. 3. **#27863 — Users list.** `all_users` gains `banned` and `role` columns after `email_verified`. 4. **#27869 (part) — Locale help.** `sys_user.locale`'s help now says it is the language the user sees the interface in as well as their notifications (the console applies it to the whole UI through `useUserLocale`). Source and all four shipped packs. ### What this PR does not carry, and who does - **#27869 stays open (`Part of`).** Seat ruling in the seat-1 thread: (a) the English "Invalid password" refusal is a `plugin-auth` refusal, and it goes with #27857's PR; (b) French `sys_user` labels need French as a fifth platform locale (platform-objects ships en / zh-CN / ja-JP / es-ES only, and HotCRM declares the same four), and the silent fallback is `service-i18n`'s. Both are reported as findings for the seat to file. - **The role label (#27859 / #27863 "reads through its label").** `sys_user.role` stays `Field.text`, so the highlight and the new Role column show the stored value (`user`, `admin`). A label needs `Field.select`, which changes the column the SQL driver emits (`text` gives TEXT; `select` falls to the default `varchar(255)`), and `schema-drift` would read existing deployments as a `narrow_varchar`. Seat ruling A: this half is carried by its own card, which the seat files at ACCEPT. ## Tests - New pins: `sys-user-list-status-role-columns.test.ts` (#27863), `sys-user-locale-help.test.ts` (#27869). Rewritten: `create-user-result-dialog-copy.test.ts` (#27858, both actions, all four packs). Extended: `sys-user-details-tab.test.ts` (#27859). - `pnpm --filter @objectstack/platform-objects test`: Test Files 102 passed (102), Tests 1445 passed (1445). `typecheck`: exit 0. - Ablations (one per row, each through `scripts/ablation-replace.mjs`, restored to the HEAD blob, `git diff HEAD` empty afterwards): - #27858: `set_user_password` regains a `description` → `set_user_password: no dialog-level description` red (1 failed / 8 passed). - #27859: `locale` dropped from the Identity section → 2 failed / 3 passed. - #27863: `banned`, `role` dropped from `all_users` → `pins the default list columns` red (1 failed / 1 passed). - #27869: the old notifications-only source help restored → `the source description names the interface` red (1 failed / 4 passed). ## Acceptance notes - The role label half (above) and #27869's (a) and (b) halves are reported in the card report for the seat to file or carry. - `role` in the new list column reads the raw stored value until the role-label card lands. ## Verification All local readings are on head `24220933`. <details><summary>Derived gate families: 58 of 59 exit 0, 1 NOT MEASURED</summary> - `node scripts/pm/dispatch-gates.mjs --commands` derived 59 families from the 11 changed paths (merge base `bc496bf7d`). Each ran after the build closure `turbo run build --filter=@objectstack/cli --filter=@objectstack/platform-objects` plus the nine other i18n-bundle packages `check:i18n` names (exit 0). - 58 families exit 0, including `check:i18n`, `check:i18n-stale-fill`, `check:page-declaration-shape`, `check:published-files`, `check:closing-keyword-parity`, `check:nul-bytes` and `check-plugin-teardown-shape --self-test` (pinned commit fetched). - NOT MEASURED: `check:dual-build-cjs-loads` exit 3 (PREREQUISITE NOT MET: it needs every package's dist; CI builds the whole tree). - `check:dts-closure` and `check:sourcemap-no-sources-content` swept the 62 built packages only; CI sweeps the whole tree. - `dispatch-gates --ran`: "59 derived famil(ies) accounted for — 58 run, 1 NOT-MEASURED", exit 0. - `pnpm lint` (`eslint . --no-inline-config`): exit 0. </details> CI check-runs on this PR are the authority for every family CI runs. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (not touched) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. (a seat fold: one card per commit) - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (not touched) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016WV4cwXFwjU2YZW2dRuZZt --- _Generated by [Claude Code](https://claude.ai/code/session_016WV4cwXFwjU2YZW2dRuZZt)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 91a423e commit 8ec00cf

11 files changed

Lines changed: 194 additions & 66 deletions
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
---
4+
5+
fix(platform-objects): Setup's user screens show a user's real state, and the copy-password line only when a password was generated
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
Four rows of the Setup → Users pass, all on the platform's own `sys_user` metadata:
11+
12+
- **Create User / Set Password result.** The dialog no longer carries the sentence "If a temporary password was generated, copy it now — it is shown only once and never stored." on every result. A dialog-level description shows on both outcomes, and a typed password returns no temporary password, so the line asked the administrator to copy something that was not there. The instruction is now the label of the Temporary password field itself ("Temporary password: copy it now, it is shown only once and never stored"), which the dialog draws only when the server generated one. The zh-CN, ja-JP and es-ES packs carry the same change.
13+
- **User record, Details tab.** Details now shows the saved Locale (in Identity) and an Account Status section with Banned, Ban Reason and Ban Expires. These fields were hidden or in no section, and the details grid draws only its authored sections, so a banned user read as active.
14+
- **Users list.** The default `all_users` view gains Banned and Platform Role columns, so a banned user or an admin is spotted without opening each record.
15+
- **Locale help.** The help now says the setting is the language the user sees the interface in as well as their notifications. It used to say notifications only, while the console applies it to the whole interface. All four shipped packs carry it.

‎packages/platform-objects/src/apps/translations/en.objects.generated.ts‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
9393
},
9494
locale: {
9595
label: "Locale",
96-
help: "Preferred language for this user's notifications, as a language tag such as zh-CN or ja-JP. When unset, the deployment's default language applies."
96+
help: "The language this user sees the interface in and receives notifications in, as a language tag such as zh-CN or ja-JP. When unset, the deployment's default language applies."
9797
},
9898
manager_id: {
9999
label: "Manager",
@@ -195,13 +195,12 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
195195
},
196196
resultDialog: {
197197
title: "User Created",
198-
description: "If a temporary password was generated, copy it now — it is shown only once and never stored.",
199198
acknowledge: "Done",
200199
fields: {
201200
"user.email": "Email",
202201
"user.phoneNumber": "Phone Number",
203202
permissionSet: "Permission Set",
204-
temporaryPassword: "Temporary Password"
203+
temporaryPassword: "Temporary password: copy it now, it is shown only once and never stored"
205204
}
206205
}
207206
},
@@ -222,10 +221,9 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
222221
},
223222
resultDialog: {
224223
title: "Password Updated",
225-
description: "If a temporary password was generated, copy it now — it is shown only once and never stored.",
226224
acknowledge: "Done",
227225
fields: {
228-
temporaryPassword: "Temporary Password"
226+
temporaryPassword: "Temporary password: copy it now, it is shown only once and never stored"
229227
}
230228
}
231229
},

‎packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
9393
},
9494
locale: {
9595
label: "Idioma",
96-
help: "Idioma preferido para las notificaciones de este usuario, como una etiqueta de idioma, por ejemplo zh-CN o ja-JP. Si no se define, se aplica el idioma predeterminado del despliegue."
96+
help: "Idioma en el que este usuario ve la interfaz y recibe las notificaciones, como una etiqueta de idioma, por ejemplo zh-CN o ja-JP. Si no se define, se aplica el idioma predeterminado del despliegue."
9797
},
9898
manager_id: {
9999
label: "Gerente",
@@ -195,13 +195,12 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
195195
},
196196
resultDialog: {
197197
title: "Usuario creado",
198-
description: "Si se generó una contraseña temporal, cópiela ahora: se muestra una sola vez y no se almacena.",
199198
acknowledge: "Hecho",
200199
fields: {
201200
"user.email": "Correo electrónico",
202201
"user.phoneNumber": "Número de teléfono",
203202
permissionSet: "Conjunto de permisos",
204-
temporaryPassword: "Contraseña temporal"
203+
temporaryPassword: "Contraseña temporal: cópiela ahora, se muestra una sola vez y no se almacena"
205204
}
206205
}
207206
},
@@ -222,10 +221,9 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
222221
},
223222
resultDialog: {
224223
title: "Contraseña actualizada",
225-
description: "Si se generó una contraseña temporal, cópiela ahora: se muestra una sola vez y no se almacena.",
226224
acknowledge: "Hecho",
227225
fields: {
228-
temporaryPassword: "Contraseña temporal"
226+
temporaryPassword: "Contraseña temporal: cópiela ahora, se muestra una sola vez y no se almacena"
229227
}
230228
}
231229
},

‎packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
9393
},
9494
locale: {
9595
label: "言語",
96-
help: "このユーザーへの通知に使用する優先言語。zh-CN や ja-JP のような言語タグで指定します。未設定の場合はデプロイのデフォルト言語が適用されます。"
96+
help: "このユーザーの画面表示と通知に使用する言語。zh-CN や ja-JP のような言語タグで指定します。未設定の場合はデプロイのデフォルト言語が適用されます。"
9797
},
9898
manager_id: {
9999
label: "マネージャー",
@@ -195,13 +195,12 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
195195
},
196196
resultDialog: {
197197
title: "ユーザーを作成しました",
198-
description: "一時パスワードが生成された場合は今すぐコピーしてください。表示は一度きりで、保存されません。",
199198
acknowledge: "完了",
200199
fields: {
201200
"user.email": "メールアドレス",
202201
"user.phoneNumber": "電話番号",
203202
permissionSet: "権限セット",
204-
temporaryPassword: "一時パスワード"
203+
temporaryPassword: "一時パスワード:今すぐコピーしてください。表示は一度きりで、保存されません"
205204
}
206205
}
207206
},
@@ -222,10 +221,9 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
222221
},
223222
resultDialog: {
224223
title: "パスワードを更新しました",
225-
description: "一時パスワードが生成された場合は今すぐコピーしてください。表示は一度きりで、保存されません。",
226224
acknowledge: "完了",
227225
fields: {
228-
temporaryPassword: "一時パスワード"
226+
temporaryPassword: "一時パスワード:今すぐコピーしてください。表示は一度きりで、保存されません"
229227
}
230228
}
231229
},

‎packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
9393
},
9494
locale: {
9595
label: "语言",
96-
help: "该用户接收通知时使用的首选语言,填写语言标签,如 zh-CN 或 ja-JP。未设置时采用部署的默认语言。"
96+
help: "该用户界面显示和接收通知所用的语言,填写语言标签,如 zh-CN 或 ja-JP。未设置时采用部署的默认语言。"
9797
},
9898
manager_id: {
9999
label: "经理",
@@ -195,13 +195,12 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
195195
},
196196
resultDialog: {
197197
title: "用户已创建",
198-
description: "如果生成了临时密码,请立即复制——它只显示一次,不会被保存。",
199198
acknowledge: "完成",
200199
fields: {
201200
"user.email": "邮箱",
202201
"user.phoneNumber": "手机号",
203202
permissionSet: "权限集",
204-
temporaryPassword: "临时密码"
203+
temporaryPassword: "临时密码:请立即复制,它只显示一次,不会被保存"
205204
}
206205
}
207206
},
@@ -222,10 +221,9 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
222221
},
223222
resultDialog: {
224223
title: "密码已更新",
225-
description: "如果生成了临时密码,请立即复制——它只显示一次,不会被保存。",
226224
acknowledge: "完成",
227225
fields: {
228-
temporaryPassword: "临时密码"
226+
temporaryPassword: "临时密码:请立即复制,它只显示一次,不会被保存"
229227
}
230228
}
231229
},

‎packages/platform-objects/src/identity/create-user-result-dialog-copy.test.ts‎

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
22

33
/**
4-
* `sys_user.create_user` — the result dialog reads true whether the admin
5-
* typed the password or left it empty to generate one.
4+
* `sys_user.create_user` and `set_user_password` — the result dialog says
5+
* "copy it now" only when there is a temporary password to copy.
66
*
77
* The endpoint (`plugin-auth`, `admin-user-endpoints.ts`) returns
88
* `temporaryPassword` only when it minted one, and the dialog skips a field
9-
* whose path is absent from the response. So after a typed password the
10-
* dialog shows the email alone; a sentence ordering the admin to "copy the
11-
* temporary password now" beside an "I have saved this password" button then
12-
* describes a password that was never shown. `ActionSchema.resultDialog` has
13-
* no per-outcome copy, so one sentence must hold for both outcomes — the
14-
* hedged shape `set_user_password` in the same file already uses.
9+
* whose path is absent from the response. `ActionSchema.resultDialog` has no
10+
* per-outcome copy, so a dialog-level `description` shows on both outcomes:
11+
* the hedged "If a temporary password was generated, copy it now" sentence
12+
* sat above a typed password's result, where nothing was generated and
13+
* nothing could be copied. The instruction now rides the `temporaryPassword`
14+
* field's own label, which is drawn only beside the password itself.
1515
*
1616
* Pinned in the source and in all four shipped locales: the translated leaves
1717
* are hand-written and carry no source-hash entry, so a stale translation of
@@ -35,14 +35,25 @@ const LOCALES: readonly (readonly [string, any])[] = [
3535
['es-ES', esESObjects],
3636
];
3737

38-
describe('create_user — the result dialog holds for a typed and a generated password', () => {
39-
it('the source sentence is conditional on a password having been generated', () => {
40-
const rd = action('create_user')?.resultDialog;
41-
expect(rd, 'create_user declares a resultDialog').toBeDefined();
42-
expect(rd?.description).toBe(
43-
'If a temporary password was generated, copy it now — it is shown only once and never stored.',
44-
);
45-
});
38+
describe('create_user / set_user_password — the copy line shows only beside a generated password', () => {
39+
for (const name of ['create_user', 'set_user_password']) {
40+
it(`${name}: no dialog-level description, which would show on both outcomes`, () => {
41+
const rd = action(name)?.resultDialog;
42+
expect(rd, `${name} declares a resultDialog`).toBeDefined();
43+
expect(rd).not.toHaveProperty('description');
44+
});
45+
46+
it(`${name}: the copy instruction is the temporaryPassword field's own label`, () => {
47+
const field = action(name)?.resultDialog?.fields?.find((f) => f.path === 'temporaryPassword');
48+
expect(field?.format).toBe('secret');
49+
expect(field?.label).toMatch(/^Temporary password: copy it now/);
50+
});
51+
52+
it(`${name}: no other field label asks the admin to copy a password`, () => {
53+
const others = (action(name)?.resultDialog?.fields ?? []).filter((f) => f.path !== 'temporaryPassword');
54+
for (const f of others) expect(String(f.label ?? ''), f.path).not.toMatch(/copy|password/i);
55+
});
56+
}
4657

4758
it('the button claims no saved password, since a typed one shows none', () => {
4859
const ack = action('create_user')?.resultDialog?.acknowledge ?? '';
@@ -60,12 +71,20 @@ describe('create_user — the result dialog holds for a typed and a generated pa
6071
]);
6172
});
6273

63-
it('every locale says the same thing as set_user_password, whose dialog has the same two outcomes', () => {
74+
it('every locale drops the description and translates the field label, the same for both actions', () => {
75+
const source = action('create_user')?.resultDialog?.fields?.find((f) => f.path === 'temporaryPassword')?.label;
6476
for (const [locale, bundle] of LOCALES) {
6577
const created = dialog(bundle, 'create_user');
6678
const updated = dialog(bundle, 'set_user_password');
67-
expect(created?.description, `${locale} create_user description`).toBeTruthy();
68-
expect(created.description, `${locale} description`).toBe(updated?.description);
79+
for (const [name, d] of [['create_user', created], ['set_user_password', updated]] as const) {
80+
expect(d, `${locale} ${name} resultDialog`).toBeDefined();
81+
expect(d, `${locale} ${name} description`).not.toHaveProperty('description');
82+
}
83+
const label = created?.fields?.temporaryPassword;
84+
expect(label, `${locale} temporaryPassword label`).toBeTruthy();
85+
expect(updated?.fields?.temporaryPassword, `${locale} same label on both`).toBe(label);
86+
if (locale === 'en') expect(label).toBe(source);
87+
else expect(label, `${locale} temporaryPassword label is the English source`).not.toBe(source);
6988
expect(created.acknowledge, `${locale} acknowledge`).toBe(updated?.acknowledge);
7089
}
7190
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Setup → Users: the default list shows whether a user is banned and what role
5+
* they hold, so a banned user or an admin is spotted without opening each
6+
* record (#27863).
7+
*
8+
* `all_users` is the view Setup's Users entry opens (`viewName: 'all_users'`,
9+
* pinned in `setup-users-nav-view.test.ts`). Its columns used to be name,
10+
* email, phone, email verified, identity source and created: nothing on the
11+
* list said a user was banned. The `banned` view keeps its own filter; this
12+
* pin is about the default list.
13+
*/
14+
15+
import { describe, expect, it } from 'vitest';
16+
import { SysUser } from './sys-user.object.js';
17+
18+
type ListView = { columns?: unknown[] };
19+
20+
const view = (name: string): ListView =>
21+
((SysUser as { listViews?: Record<string, ListView> }).listViews ?? {})[name] ?? {};
22+
23+
describe('sys_user all_users — Status and Role columns (#27863)', () => {
24+
it('pins the default list columns', () => {
25+
expect(view('all_users').columns).toEqual([
26+
'name', 'email', 'phone_number', 'email_verified', 'banned', 'role', 'source', 'created_at',
27+
]);
28+
});
29+
30+
it('every column is a declared sys_user field', () => {
31+
const fields = Object.keys(SysUser.fields ?? {});
32+
for (const c of view('all_users').columns ?? []) expect(fields, String(c)).toContain(c);
33+
});
34+
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* `sys_user.locale` help says what the setting does: it sets the language the
5+
* user sees the interface in, as well as their notifications (#27869).
6+
*
7+
* The console applies the signed-in user's `sys_user.locale` to the whole UI
8+
* (`useUserLocale`, and the globe switcher writes the same column), while the
9+
* help said "Preferred language for this user's notifications", so a user
10+
* reading it did not expect the interface to switch. Pinned in the source and
11+
* in every shipped pack, because a translated leaf is hand-written and a stale
12+
* one is not flagged anywhere else.
13+
*/
14+
15+
import { describe, expect, it } from 'vitest';
16+
import { SysUser } from './sys-user.object.js';
17+
import { enObjects } from '../apps/translations/en.objects.generated.js';
18+
import { zhCNObjects } from '../apps/translations/zh-CN.objects.generated.js';
19+
import { jaJPObjects } from '../apps/translations/ja-JP.objects.generated.js';
20+
import { esESObjects } from '../apps/translations/es-ES.objects.generated.js';
21+
22+
const help = (bundle: any): string => bundle?.sys_user?.fields?.locale?.help ?? '';
23+
24+
/** Per pack: the word for the interface, and the old notifications-only opening that must be gone. */
25+
const PACKS: readonly (readonly [string, any, RegExp, RegExp])[] = [
26+
['en', enObjects, /interface/, /^Preferred language for this user's notifications/],
27+
['zh-CN', zhCNObjects, /界面/, /^该用户接收通知时使用的首选语言/],
28+
['ja-JP', jaJPObjects, /画面/, /^このユーザーへの通知に使用する優先言語/],
29+
['es-ES', esESObjects, /interfaz/, /^Idioma preferido para las notificaciones/],
30+
];
31+
32+
describe('sys_user.locale help — it sets the interface language (#27869)', () => {
33+
it('the source description names the interface', () => {
34+
const d = String(SysUser.fields?.locale?.description ?? '');
35+
expect(d).toMatch(/^The language this user sees the interface in and receives notifications in/);
36+
});
37+
38+
for (const [locale, bundle, names, old] of PACKS) {
39+
it(`${locale} pack says it sets the interface language`, () => {
40+
expect(help(bundle)).toMatch(names);
41+
expect(help(bundle)).not.toMatch(old);
42+
});
43+
}
44+
});

0 commit comments

Comments
 (0)