Skip to content

Commit bdbaf80

Browse files
fix: Setup form help speaks to an administrator; queue pages, mail status banner and test toast read in Chinese (#30551)
<!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgAb97yDcvm73kZUzyhhNWsN)_ domain:server#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE Refs #30208 (rows 1, 2, 3, 7, 8) Clause-②: yes (widening) Metadata-protocol: none Skills-docs: none — `git grep -n -I -E "fail-closed at resolution|half-open \[from, until\)|graph semantics|root of tenant|HRIS sync|upstream HRIS|expanded by graph|When true, every member|When true, the user|task/run attribution|No mail leaves this machine|Sent test email" -- skills` on `e205cf31` answers 0 lines; no published skill sentence quotes the replaced help or the mail status text. ## 维护者速读 - 改了什么:Setup 里「新建权限集分配 / 岗位分配」「外出委派规则」「新建业务单元」「编辑组织」「用户」表单的字段说明改成管理员能看懂的话(去掉 fail-closed、半开区间、图谱、Null、HRIS、「为 true 时」等,改为「开启后…」);「队列」「队列成员」两页补上中文;「邮件投递」顶部状态横幅和「发送测试邮件」的提示在中文界面下显示中文。 - 为什么改:真实用户走查 Setup 时,这些说明在描述实现而不是告诉管理员该填什么;队列两页整页英文(按钮显示「新建Queue」);邮件页横幅和测试提示是英文。 - 风险与回滚:只改服务端下发的文字,不改任何字段名、选项值或行为;唯一的接口变化是设置动作处理函数多收一个可选的 `locale`,已有处理函数不受影响。回滚即 revert 本 PR。 - 先看:`packages/plugins/plugin-email/src/live-transport-status.ts`(中英文案表与按语言取文案),`packages/services/service-settings/src/settings-routes.ts` 与 `settings-service.ts` 的 `runAction`(动作路由把请求语言交给处理函数),`packages/plugins/plugin-sharing/scripts/i18n-extract.config.ts`(队列两个对象进翻译包)。 - 两分钟验证:`pnpm --filter @objectstack/plugin-email exec vitest run src/mail-log-provider.pin.test.ts`,预期 13 条全过,其中 5 条是中文横幅与测试提示。 - 需要你做的:无,席位落地。 ## What changes **Rows 1–3: field help in administrator wording (English source and zh-CN).** Text only; no field name, key, option value or behaviour moves. | Form | Field | Before (zh-CN) | After (zh-CN) | | --- | --- | --- | --- | | 权限集分配 / 岗位分配 | `valid_from` | 在解析时以 fail-closed 方式强制执行 | 到时即生效,无需等待任何定时任务 | | 权限集分配 / 岗位分配 | `valid_until` | 半开区间 [from, until),UTC;智能体授权必填 | 到此时间即失效(按 UTC 计);授予 AI 智能体的分配必须填写 | | 权限集分配 | `reason` | 智能体授权在此记录任务/运行归属 | 授予 AI 智能体时,请写明它为哪项任务获得授权 | | 审批委派 | `valid_until` | 半开区间 [from, until) | 规则到此时间即失效(按 UTC 计) | | 业务单元 | `kind` | 不会改变图谱语义 | 仅用于分类和报表,不会改变该单元在组织中的位置和覆盖范围 | | 业务单元 | `parent_business_unit_id` | Null 表示租户根节点 | 留空表示顶级单元 | | 业务单元 | `active` | 为 false 时,图谱查询不会展开该业务单元成员 | 关闭后即停用该单元:授予该单元或其上级单元的共享等访问权限,将不再覆盖其成员及下级单元 | | 业务单元 | `effective_from` / `effective_to` / `external_ref` | HRIS 同步 / 上游 HRIS | 通常由 HR 系统同步填写 / 在 HR 系统中的编号 | | 组织 | `require_mfa` | 为 true 时 | 开启后 | | 用户 | `banned`, `must_change_password` | 为 true 时;403 PASSWORD_RESET_REQUIRED | 开启后;用户必须先修改密码才能继续使用 | The English sources (`*.object.ts`) say the same in English; `en.*.generated.ts` follow by extract. The `active` sentence states what `BusinessUnitGraphService` does with an inactive unit (contributes no members and stops the descent into its subtree, `plugin-sharing/src/business-unit-graph.ts`); `must_change_password` names the admin create, import and reset paths that set it (`plugin-auth` `admin-user-endpoints.ts`, `admin-import-users.ts`). **Row 7: the 队列 and 队列成员 pages.** `plugin-sharing`'s extract config now lists `SysQueue` and `SysQueueMember` beside its three sharing objects (the `bundle-ownership` list follows). The extract added both objects to every bundle; zh-CN is written by hand (队列 / 队列成员, every field, help, the member-type options and the two list-view names). ja-JP and es-ES carry the extract's English fill, except the member-type options, which reuse the renderings `sys_record_share` / `sys_sharing_rule` already use in those bundles (the package's `recipient-vocabulary-consistency` pin requires one rendering per option key). **Row 8: Setup → Email.** - `plugin-email`: `describeLiveMailTransport(source, locale?)` writes the banner from a per-language copy table (`MAIL_DELIVERY_COPY`, en and zh-CN; `mailDeliveryCopy` resolves exact, then by language, else English), the mechanism `service-settings`' currency lock already uses for its live status. The plugin's live-status provider passes the `locale` the settings service already hands it. The `mail/test` handler writes its sent, failed and log-only answers from the same table. - `service-settings`: `SettingsActionHandler`'s input gains an optional `locale`; `runAction(namespace, actionId, payload, ctx, display?)` passes `display.locale` to the handler; the POST action route passes `requestLocaleOf(req)`, the same reading the GET route gives the live status. The locale stays out of `ctx`. An existing handler compiles and behaves unchanged. ## Tests One pin file per row, plus a route pin: - `plugin-security/src/translations/assignment-help-wording.test.ts`: served zh-CN (`SecurityTranslations`) and the English source of `valid_from` / `valid_until` / `reason` on both assignment objects carry no `fail-closed`, interval notation or `agent grants` / 智能体授权, and the zh-CN help is Chinese. - `plugin-approvals/src/translations/delegation-window-wording.test.ts`: `sys_approval_delegation.valid_until`, same shape. - `platform-objects/src/identity/setup-form-admin-wording.test.ts`: served zh-CN (`SetupAppTranslations`) and the English source of the nine business unit, organization and user fields carry no graph vocabulary, null literal, `HRIS`, boolean literal or HTTP status code. - `plugin-sharing/src/translations/queue-pages-zh-cn.test.ts`: the served zh-CN bundle names 队列 / 队列成员 and translates every field label, help and member-type option. - `plugin-email/src/mail-log-provider.pin.test.ts` (the existing Setup → Email harness, new top-level `describe`): zh-CN banner for the out-of-the-box and the delivering transport, the zh-CN test toast, the zh-CN log-only warning, and a control (`fr-FR` answers English). - `service-settings/src/settings-routes.test.ts`: the POST action route hands the handler `zh-CN` from `Accept-Language`, `undefined` without one, and not in `ctx`. Red on `main` (the pin commit `0f390750`, before the fix): security 10 failed / 2 passed, approvals 2/2 failed, platform-objects 18/18 failed, sharing 4/4 failed, email 4 failed / 9 passed (the control passed), settings routes 1 failed / 34 passed. Ablation (once, `scripts/ablation-replace.mjs`, anchor hit x1, restored blob equals HEAD, `git diff HEAD` empty): with the live-status provider calling `describeLiveMailTransport(this.service)` again (the locale dropped), exactly the two zh-CN statement pins go red, 2 failed / 11 passed. ## Acceptance notes - **No closing keyword:** the dispatch's How section spells a closing line, its rulings say `Refs`; this PR follows the rulings, so merging leaves the card open for rows 5 and 6 (row 5 needs a settings shape; row 6 waits on the maintainer). - **ja-JP and es-ES are not re-translated here.** Their leaves for the help changed in rows 1–3 still translate the old English source (for example es-ES `valid_from` still says "modo fail-closed"); they are legacy-trusted (no provenance record), so they keep serving that text. The queue objects arrive there as English fills, recorded in each locale's `source-hashes` file. The seat's serial note puts those two languages with the pending seven-language pass on these packages. - **Mail copy is en and zh-CN.** ja-JP and es-ES callers get the English banner and toast, as before. The test handler's input refusals (`SMTP host is required — nothing was sent.`, `api_key is required`, an unsupported provider, a transport that cannot be built) stay English; only the sent, failed and log-only answers moved. - **Not touched, same class, outside these rows' forms:** `sys_setting` / `sys_platform_setting` `encrypted` and `locked` help still say 「为 true 时」 (an internal store, not a Setup form); `sys_user_position.business_unit_id` help names `readScope/writeScope`. - `mail.manifest.ts` (`plugin-email` and `service-settings`) is not touched (another lane's card edits it). - **`require_mfa` and PR #30529:** that open PR (another seat's card on the same field) rewrites this help to add that the requirement needs two-factor on for the deployment, keeping "When true". This PR's help keeps that sentence beside the administrator wording, in English and zh-CN, so whichever lands second resolves the textual conflict by taking this text; #30529's pins (`two-factor authentication on for this deployment`, `OS_AUTH_TWO_FACTOR=true` in every locale) hold against it. ja-JP and es-ES for that field are #30529's. ## Verification <details><summary>Package suites and typechecks (head e205cf3; the later main merge brought one ui-only commit)</summary> - `pnpm --filter @objectstack/PKG test` and `typecheck` for service-settings, platform-objects, plugin-security, plugin-approvals, plugin-sharing, plugin-email: all exit 0 under the verify lock (VERDICT command-exit 0). plugin-sharing 66 files / 1409 tests after the ja-JP / es-ES option fix. - `pnpm check:i18n`: all ten bundle sets in sync. `pnpm i18n:extract`: byte-identical. - `eslint --no-inline-config` on the changed TypeScript files: exit 0. `pnpm lint` (repository-wide) on `7e4a3272`: exit 0. </details> <details><summary>Derived gate families</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on `f63ad0b2` (after the `origin/main` merge) derived 66 commands over 32 changed paths. Each ran with its exit code captured before any pipe; `--ran`: 66 derived, 65 run, 1 NOT-MEASURED, 0 UNRUN. - 64 exited 0 on the first pass. - `pnpm check:dual-build-cjs-loads`: exit 3, PREREQUISITE NOT MET (it reads every package's `dist`, and only this card's closure was built). NOT MEASURED here; CI's `Lint & Repo Gates` runs it. - `pnpm check:i18n-stale-fill`: exit 1 on the first pass. It found `sys_approval_delegation.valid_until.help` in ja-JP and es-ES still holding the byte-identical English fill of the old source. Both now carry the fill of the new source (recorded by the extract), and the gate exits 0 on `07d639c3` and on the final head. After the union two small commits landed: `07d639c3` (those two fills) and `7e4a3272` (`require_mfa` help, see Acceptance notes). The changed-path set grew from 32 to 36 paths; the derived gate family did not move (the same 66 commands). Re-run on `7e4a3272`: `pnpm check:i18n` exit 0, `pnpm check:i18n-stale-fill` exit 0, `pnpm i18n:extract` byte-identical, `pnpm --filter @objectstack/platform-objects test` 112 files / 1544 tests, `pnpm --filter @objectstack/plugin-approvals test` (on `07d639c3`) 112 files / 1471 tests. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01Utp6CbDNK83SuSb8zsRt2s)_ <!-- signed: domain:server#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0e4747a commit bdbaf80

36 files changed

Lines changed: 766 additions & 107 deletions
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
'@objectstack/plugin-security': patch
4+
'@objectstack/plugin-approvals': patch
5+
'@objectstack/plugin-sharing': patch
6+
'@objectstack/plugin-email': patch
7+
'@objectstack/service-settings': patch
8+
---
9+
10+
fix: Setup's form help speaks to an administrator, and the queue pages, the mail status banner and the test-email toast read in Chinese on a zh-CN console
11+
12+
Clause-②: yes (widening)
13+
Metadata-protocol: none
14+
15+
- **Field help (English source and zh-CN):** the permission set and position assignment forms, the out-of-office (approval delegation) rule, and the business unit, organization and user forms no longer describe the implementation. Gone: "fail-closed", "half-open [from, until)", "agent grants", "graph semantics", "expanded by graph queries", "Null = root of tenant", "HRIS", "When true / When false" (zh-CN 「为 true 时」 now reads 「开启后」) and an HTTP status code. The help now says when an assignment starts and ends, what turning a switch on does, and what to fill in. Text only: no field name, key, option value or behaviour changes.
16+
- **`@objectstack/plugin-sharing`:** `sys_queue` and `sys_queue_member` join the plugin's translation bundle, with zh-CN text, so the 队列 and 队列成员 pages, their columns, help and the 新建 button read in Chinese. ja-JP and es-ES carry English fills for now.
17+
- **`@objectstack/plugin-email`:** the live-status statement at the top of Setup → Email and the "Send test email" answer (sent, failed, logged only) are written in the caller's display language, en and zh-CN today; another language gets English.
18+
- **`@objectstack/service-settings`:** a settings action handler (`SettingsActionHandler`) receives an optional `locale`, the caller's display language read from the request the same way the live-status statement's is (`Accept-Language`, else `?locale`); `runAction` takes it as a fifth `display` argument. Existing handlers compile and behave unchanged.

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
3939
},
4040
banned: {
4141
label: "Banned",
42-
help: "When true, the user cannot sign in. Toggle via Ban User / Unban User actions."
42+
help: "When on, the user cannot sign in. Use the Ban User / Unban User actions to change it."
4343
},
4444
ban_reason: {
4545
label: "Ban Reason"
@@ -70,7 +70,7 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
7070
},
7171
must_change_password: {
7272
label: "Must Change Password",
73-
help: "When true, the user is blocked (403 PASSWORD_RESET_REQUIRED) until they change their password. Stamped by the admin user-management routes; system-managed."
73+
help: "When on, the user must change their password before they can continue. Turned on when an administrator creates or imports the account, or resets its password; system-managed."
7474
},
7575
mfa_required_at: {
7676
label: "MFA Required At",
@@ -557,7 +557,7 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
557557
},
558558
require_mfa: {
559559
label: "Require Multi-Factor Auth",
560-
help: "When true, every member of this organization must enroll an authenticator app to access data. Needs two-factor authentication on for this deployment (OS_AUTH_TWO_FACTOR=true); while it is off, saving this as true is refused."
560+
help: "Turn on to require every member of this organization to set up an authenticator app before they can access data. Needs two-factor authentication on for this deployment (OS_AUTH_TWO_FACTOR=true); while it is off, saving this as on is refused."
561561
},
562562
timezone: {
563563
label: "Timezone",
@@ -995,7 +995,7 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
995995
},
996996
kind: {
997997
label: "Kind",
998-
help: "Categorisation hint — does not change graph semantics.",
998+
help: "For grouping and reporting only; it does not change where the unit sits in the organization or who it covers.",
999999
options: {
10001000
company: "Company",
10011001
division: "Division",
@@ -1006,7 +1006,7 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
10061006
},
10071007
parent_business_unit_id: {
10081008
label: "Parent Business Unit",
1009-
help: "Self-reference for the org tree. Null = root of tenant. A unit that still has child units cannot be deleted until they are re-parented or deleted."
1009+
help: "The unit this one sits under. Leave empty for a top-level unit. A unit that still has units under it cannot be deleted until they are moved under another unit or deleted."
10101010
},
10111011
timezone: {
10121012
label: "Timezone",
@@ -1022,19 +1022,19 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
10221022
},
10231023
active: {
10241024
label: "Active",
1025-
help: "When false, members are not expanded by graph queries."
1025+
help: "Turn off to retire the unit: sharing and other access given to this unit, or to a unit above it, no longer reaches its members or the units under it."
10261026
},
10271027
effective_from: {
10281028
label: "Effective From",
1029-
help: "When this business unit came into existence (HRIS sync)."
1029+
help: "When this business unit came into existence. Usually filled in by a sync from your HR system."
10301030
},
10311031
effective_to: {
10321032
label: "Effective To",
1033-
help: "When this business unit was retired (HRIS sync)."
1033+
help: "When this business unit was retired. Usually filled in by a sync from your HR system."
10341034
},
10351035
external_ref: {
10361036
label: "External Reference",
1037-
help: "ID in upstream HRIS (Workday / SAP HR / 北森)."
1037+
help: "The unit's ID in your HR system (such as Workday, SAP HR or 北森), used to match it on each sync."
10381038
},
10391039
id: {
10401040
label: "Business Unit ID"

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
3939
},
4040
banned: {
4141
label: "已封禁",
42-
help: "为 true 时,用户无法登录。可通过“封禁用户 / 解除封禁”操作切换。"
42+
help: "开启后,用户无法登录。可通过“封禁用户 / 解除封禁”操作切换。"
4343
},
4444
ban_reason: {
4545
label: "封禁原因"
@@ -70,7 +70,7 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
7070
},
7171
must_change_password: {
7272
label: "须修改密码",
73-
help: "为 true 时,用户会被阻止访问(403 PASSWORD_RESET_REQUIRED),直到修改密码。由管理员用户管理接口写入;系统维护。"
73+
help: "开启后,用户必须先修改密码才能继续使用。管理员创建账号、导入用户或重置密码时会自动开启;系统维护。"
7474
},
7575
mfa_required_at: {
7676
label: "MFA 强制生效时间",
@@ -557,7 +557,7 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
557557
},
558558
require_mfa: {
559559
label: "强制多因素认证",
560-
help: "为 true 时,该组织的每位成员都必须注册身份验证器 App 才能访问数据。需要本部署开启双重验证(OS_AUTH_TWO_FACTOR=true);双重验证关闭时,保存为 true 会被拒绝。"
560+
help: "开启后,该组织的每位成员都必须先绑定身份验证器 App,才能访问数据。需要本部署开启双重验证(OS_AUTH_TWO_FACTOR=true);双重验证关闭时,保存为开启会被拒绝。"
561561
},
562562
timezone: {
563563
label: "时区",
@@ -995,7 +995,7 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
995995
},
996996
kind: {
997997
label: "类型",
998-
help: "分类提示——不会改变图谱语义。",
998+
help: "仅用于分类和报表,不会改变该单元在组织中的位置和覆盖范围。",
999999
options: {
10001000
company: "公司",
10011001
division: "事业部",
@@ -1006,7 +1006,7 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
10061006
},
10071007
parent_business_unit_id: {
10081008
label: "上级业务单元",
1009-
help: "组织树的自关联字段。Null 表示租户根节点。仍有下级单元的业务单元无法删除,须先将下级单元改挂到其他上级或删除。"
1009+
help: "该单元的上级单元。留空表示顶级单元。仍有下级单元的业务单元无法删除,须先将下级单元改挂到其他上级或删除。"
10101010
},
10111011
timezone: {
10121012
label: "时区",
@@ -1022,19 +1022,19 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
10221022
},
10231023
active: {
10241024
label: "启用",
1025-
help: "为 false 时,图谱查询不会展开该业务单元成员。"
1025+
help: "关闭后即停用该单元:授予该单元或其上级单元的共享等访问权限,将不再覆盖其成员及下级单元。"
10261026
},
10271027
effective_from: {
10281028
label: "生效时间",
1029-
help: "该业务单元生效的时间(HRIS 同步)。"
1029+
help: "该业务单元成立的时间,通常由 HR 系统同步填写。"
10301030
},
10311031
effective_to: {
10321032
label: "失效时间",
1033-
help: "该业务单元停用的时间(HRIS 同步)。"
1033+
help: "该业务单元撤销的时间,通常由 HR 系统同步填写。"
10341034
},
10351035
external_ref: {
10361036
label: "外部引用",
1037-
help: "上游 HRIS 中的 ID(Workday / SAP HR / 北森)。"
1037+
help: "该单元在 HR 系统(如 Workday、SAP HR、北森)中的编号,每次同步时用它对应到本单元。"
10381038
},
10391039
id: {
10401040
label: "业务单元 ID"
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* The help on Setup's New business unit, Edit organization and user forms
5+
* speaks to the administrator filling them in.
6+
*
7+
* A field's help tooltip is the served translation of its `description`.
8+
* These used to describe the implementation: the org tree as a graph
9+
* ("graph semantics", "expanded by graph queries"), a null value
10+
* ("Null = root of tenant"), an acronym for the upstream system ("HRIS"), a
11+
* boolean literal ("When true", zh-CN 「为 true 时」) and an HTTP status code.
12+
* What the administrator needs is what turning the field on does, and what to
13+
* fill in. This pins the served zh-CN text and the English source.
14+
*/
15+
16+
import { describe, expect, it } from 'vitest';
17+
import { SetupAppTranslations } from '../apps/translations/setup.translation.js';
18+
import { SysBusinessUnit } from './sys-business-unit.object.js';
19+
import { SysOrganization } from './sys-organization.object.js';
20+
import { SysUser } from './sys-user.object.js';
21+
22+
/** Implementation vocabulary an administrator's field help must not carry. */
23+
const DEVELOPER_TERMS: ReadonlyArray<readonly [string, RegExp]> = [
24+
['graph vocabulary', /图谱|\bgraph\b/i],
25+
['a null literal', /\bNull\b|为空 =/],
26+
['an acronym for the upstream system', /\bHRIS\b/],
27+
['a boolean literal', /为 ?(true|false) ?时|\bWhen (true|false)\b/i],
28+
['an HTTP status code', /\b[45]\d\d [A-Z][A-Z_]{3,}\b/],
29+
];
30+
31+
const FORMS = {
32+
sys_business_unit: {
33+
schema: SysBusinessUnit,
34+
fields: ['kind', 'parent_business_unit_id', 'active', 'effective_from', 'effective_to', 'external_ref'],
35+
},
36+
sys_organization: { schema: SysOrganization, fields: ['require_mfa'] },
37+
sys_user: { schema: SysUser, fields: ['banned', 'must_change_password'] },
38+
} as const;
39+
40+
function termsIn(text: string): string[] {
41+
return DEVELOPER_TERMS.filter(([, re]) => re.test(text)).map(([name]) => name);
42+
}
43+
44+
type ObjectCopy = { fields?: Record<string, { help?: string }> };
45+
46+
describe('Setup forms: business unit, organization and user help reads as administrator wording', () => {
47+
const zhCN = (SetupAppTranslations['zh-CN'] as { objects: Record<string, ObjectCopy> }).objects;
48+
49+
for (const [object, { schema, fields }] of Object.entries(FORMS)) {
50+
for (const field of fields) {
51+
it(`${object}.${field}: the served zh-CN help is Chinese and names no implementation term`, () => {
52+
const help = zhCN[object]?.fields?.[field]?.help ?? '';
53+
expect(help).toMatch(/[一-鿿]/);
54+
expect(termsIn(help), help).toEqual([]);
55+
});
56+
57+
it(`${object}.${field}: the English source names no implementation term`, () => {
58+
const description = (schema.fields as Record<string, { description?: string }>)[field]?.description ?? '';
59+
expect(description.length).toBeGreaterThan(0);
60+
expect(termsIn(description), description).toEqual([]);
61+
});
62+
}
63+
}
64+
});

‎packages/platform-objects/src/identity/sys-business-unit.object.ts‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export const SysBusinessUnit = ObjectSchema.create({
121121
label: 'Kind',
122122
required: true,
123123
defaultValue: 'department',
124-
description: 'Categorisation hint — does not change graph semantics.',
124+
description: 'For grouping and reporting only; it does not change where the unit sits in the organization or who it covers.',
125125
group: 'Identity',
126126
options: [
127127
{ value: 'company', label: 'Company' },
@@ -145,7 +145,7 @@ export const SysBusinessUnit = ObjectSchema.create({
145145
required: false,
146146
deleteBehavior: 'restrict',
147147
description:
148-
'Self-reference for the org tree. Null = root of tenant. A unit that still has child units cannot be deleted until they are re-parented or deleted.',
148+
'The unit this one sits under. Leave empty for a top-level unit. A unit that still has units under it cannot be deleted until they are moved under another unit or deleted.',
149149
group: 'Hierarchy',
150150
}),
151151

@@ -224,29 +224,29 @@ export const SysBusinessUnit = ObjectSchema.create({
224224
label: 'Active',
225225
required: false,
226226
defaultValue: true,
227-
description: 'When false, members are not expanded by graph queries.',
227+
description: 'Turn off to retire the unit: sharing and other access given to this unit, or to a unit above it, no longer reaches its members or the units under it.',
228228
group: 'Lifecycle',
229229
}),
230230

231231
effective_from: Field.datetime({
232232
label: 'Effective From',
233233
required: false,
234-
description: 'When this business unit came into existence (HRIS sync).',
234+
description: 'When this business unit came into existence. Usually filled in by a sync from your HR system.',
235235
group: 'Lifecycle',
236236
}),
237237

238238
effective_to: Field.datetime({
239239
label: 'Effective To',
240240
required: false,
241-
description: 'When this business unit was retired (HRIS sync).',
241+
description: 'When this business unit was retired. Usually filled in by a sync from your HR system.',
242242
group: 'Lifecycle',
243243
}),
244244

245245
external_ref: Field.text({
246246
label: 'External Reference',
247247
required: false,
248248
maxLength: 200,
249-
description: 'ID in upstream HRIS (Workday / SAP HR / 北森).',
249+
description: 'The unit\'s ID in your HR system (such as Workday, SAP HR or 北森), used to match it on each sync.',
250250
group: 'Lifecycle',
251251
}),
252252

‎packages/platform-objects/src/identity/sys-organization.object.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -404,9 +404,9 @@ export const SysOrganization = ObjectSchema.create({
404404
defaultValue: false,
405405
group: 'Configuration',
406406
description:
407-
'When true, every member of this organization must enroll an authenticator app to access data. ' +
407+
'Turn on to require every member of this organization to set up an authenticator app before they can access data. ' +
408408
'Needs two-factor authentication on for this deployment (OS_AUTH_TWO_FACTOR=true); while it is off, ' +
409-
'saving this as true is refused.',
409+
'saving this as on is refused.',
410410
}),
411411

412412
// #14238 — maintainer ruling 2026-09-02, option A (verbatim 「同意」): the

‎packages/platform-objects/src/identity/sys-user.object.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -981,7 +981,7 @@ export const SysUser = ObjectSchema.create({
981981
defaultValue: false,
982982
readonly: true, // ADR-0092 — toggled via Ban/Unban actions (session side effects)
983983
group: 'Account',
984-
description: 'When true, the user cannot sign in. Toggle via Ban User / Unban User actions.',
984+
description: 'When on, the user cannot sign in. Use the Ban User / Unban User actions to change it.',
985985
}),
986986

987987
ban_reason: Field.text({
@@ -1085,8 +1085,8 @@ export const SysUser = ObjectSchema.create({
10851085
readonly: true,
10861086
group: 'Admin',
10871087
description:
1088-
'When true, the user is blocked (403 PASSWORD_RESET_REQUIRED) until they change ' +
1089-
'their password. Stamped by the admin user-management routes; system-managed.',
1088+
'When on, the user must change their password before they can continue. ' +
1089+
'Turned on when an administrator creates or imports the account, or resets its password; system-managed.',
10901090
}),
10911091

10921092
// ADR-0069 D3 — when enforced MFA first applied to this user; starts the

‎packages/plugins/plugin-approvals/src/sys-approval-delegation.object.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export const SysApprovalDelegation = ObjectSchema.create({
137137
required: false,
138138
group: 'Delegation',
139139
description:
140-
'Rule is inactive AT and AFTER this instant (half-open [from, until), UTC). Null = never expires.',
140+
'The rule stops applying at this time (UTC). Leave empty for no end date.',
141141
}),
142142

143143
reason: Field.text({
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* An out-of-office rule's "Valid Until" help says when the rule stops, not
5+
* which interval notation the check uses.
6+
*
7+
* The help tooltip on the `sys_approval_delegation` form is the served
8+
* translation of the field's `description`. It used to read "half-open
9+
* [from, until)" (zh-CN 「半开区间 [from, until)」): the shape of the check,
10+
* which an administrator has no use for. This pins the served zh-CN text and
11+
* the English source.
12+
*/
13+
14+
import { describe, expect, it } from 'vitest';
15+
import { ApprovalsTranslations } from './index.js';
16+
import { SysApprovalDelegation } from '../sys-approval-delegation.object.js';
17+
18+
const INTERVAL_NOTATION = /半开区间|half-open|\[from, until\)/i;
19+
20+
type ObjectCopy = { fields?: Record<string, { help?: string }> };
21+
22+
describe('sys_approval_delegation.valid_until: help reads as administrator wording', () => {
23+
it('the served zh-CN help is Chinese and carries no interval notation', () => {
24+
const objects = (ApprovalsTranslations['zh-CN'] as { objects: Record<string, ObjectCopy> }).objects;
25+
const help = objects.sys_approval_delegation?.fields?.valid_until?.help ?? '';
26+
expect(help).toMatch(/[一-鿿]/);
27+
expect(help).not.toMatch(INTERVAL_NOTATION);
28+
});
29+
30+
it('the English source carries no interval notation', () => {
31+
const description = (SysApprovalDelegation.fields as Record<string, { description?: string }>).valid_until
32+
?.description ?? '';
33+
expect(description.length).toBeGreaterThan(0);
34+
expect(description).not.toMatch(INTERVAL_NOTATION);
35+
});
36+
});

‎packages/plugins/plugin-approvals/src/translations/en.objects.generated.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
366366
},
367367
valid_until: {
368368
label: "Valid Until",
369-
help: "Rule is inactive AT and AFTER this instant (half-open [from, until), UTC). Null = never expires."
369+
help: "The rule stops applying at this time (UTC). Leave empty for no end date."
370370
},
371371
reason: {
372372
label: "Reason",

0 commit comments

Comments
 (0)