diff --git a/.changeset/operation-message-zhcn-fullwidth-comma.md b/.changeset/operation-message-zhcn-fullwidth-comma.md new file mode 100644 index 0000000000..3a8a23d506 --- /dev/null +++ b/.changeset/operation-message-zhcn-fullwidth-comma.md @@ -0,0 +1,28 @@ +--- +"@objectstack/spec": patch +--- + +fix(spec): punctuate the `zh-CN` Operation Message Catalog with the full-width comma (#12717) + +Every `zh-CN` entry in `BUILTIN_OPERATION_MESSAGES` separated its clauses with +U+002C (the ASCII comma) while ending the same sentence with U+3002 (the +full-width period) — two punctuation systems inside one sentence. In Chinese +typography the half-width comma reads as a lapse rather than as a style, and +these are strings a business user reads in a toast. + +The two surfaces genuinely sit together on one screen: an approval action's own +description renders full-width (`plugin-approvals`' +`zh-CN.objects.generated.ts`, U+FF0C / U+FF1F) while a refusal from the same +feature rendered ASCII. + +Twelve U+002C across the nine `zh-CN` entries — the five situation keys +(`permission_denied`, `record_access_denied`, `record_change_not_allowed`, +`record_write_denied`, `approval_recall_not_submitter`) and the four +`delete_restricted*` variants — become U+FF0C. Nothing else moves: the U+3002 +sentence finals were already correct, the two U+3001 (、) enumeration commas in +the `_required` variants are correct Chinese and stay, and the `en`, `ja-JP` and +`es-ES` rows are untouched. + +Text-face only — no key, placeholder, wire code, status or accept/reject +behaviour changes. A deployment that overrides `errors.` through a +`translation` is unaffected; only the built-in default moves. diff --git a/packages/objectql/src/engine-delete-restricted-locale.test.ts b/packages/objectql/src/engine-delete-restricted-locale.test.ts index b0205c57be..9dfc0f31b6 100644 --- a/packages/objectql/src/engine-delete-restricted-locale.test.ts +++ b/packages/objectql/src/engine-delete-restricted-locale.test.ts @@ -169,7 +169,7 @@ describe('#7307 DELETE_RESTRICTED — user copy vs developer guidance', () => { it('renders the toast sentence in the caller locale, with TRANSLATED labels', async () => { const err = await refuseDelete(engine, 'zh-CN'); expect(err.message).toBe( - '该部门正被 1 条零星申请记录通过「申报部门」引用,且该字段为必填、无法清空,请先删除或改派这些记录。', + '该部门正被 1 条零星申请记录通过「申报部门」引用,且该字段为必填、无法清空,请先删除或改派这些记录。', ); }); diff --git a/packages/plugins/plugin-approvals/src/recall-refusal-user-copy.test.ts b/packages/plugins/plugin-approvals/src/recall-refusal-user-copy.test.ts index b11664ac8a..19de283d13 100644 --- a/packages/plugins/plugin-approvals/src/recall-refusal-user-copy.test.ts +++ b/packages/plugins/plugin-approvals/src/recall-refusal-user-copy.test.ts @@ -144,7 +144,7 @@ const ADMIN_ZH = { * itself cannot tell Chinese from English. Its twin lives in * `packages/spec/src/system/operation-message.test.ts`; the two move together. */ -const ZH_SENTENCE = '只有提交人可以撤回这条审批请求,如需撤回请联系提交人或管理员。'; +const ZH_SENTENCE = '只有提交人可以撤回这条审批请求,如需撤回请联系提交人或管理员。'; /** What `@objectstack/rest`'s `handleApprovalError` does to a thrown message. */ const WIRE_CODE = (msg: string) => /^FORBIDDEN/.test(msg); diff --git a/packages/plugins/plugin-security/src/permission-denied-user-copy.test.ts b/packages/plugins/plugin-security/src/permission-denied-user-copy.test.ts index 591284d275..41db69c8b4 100644 --- a/packages/plugins/plugin-security/src/permission-denied-user-copy.test.ts +++ b/packages/plugins/plugin-security/src/permission-denied-user-copy.test.ts @@ -254,7 +254,7 @@ describe('#7414 — the 403 an end user reads', () => { it('speaks the caller locale, not English', async () => { const zh = await denyDelete('zh-CN'); expect(zh.message).toBe(BUILTIN_OPERATION_MESSAGES['zh-CN'].permission_denied); - expect(zh.message).toBe('您没有执行此操作的权限,如需访问请联系管理员。'); + expect(zh.message).toBe('您没有执行此操作的权限,如需访问请联系管理员。'); const ja = await denyDelete('ja-JP'); expect(ja.message).toBe(BUILTIN_OPERATION_MESSAGES['ja-JP'].permission_denied); diff --git a/packages/plugins/plugin-sharing/src/write-denial-user-copy.test.ts b/packages/plugins/plugin-sharing/src/write-denial-user-copy.test.ts index b31253f25a..77d979a4b4 100644 --- a/packages/plugins/plugin-sharing/src/write-denial-user-copy.test.ts +++ b/packages/plugins/plugin-sharing/src/write-denial-user-copy.test.ts @@ -246,7 +246,7 @@ const OWNER_ZH = ctxFor(U_OWNER, 'zh-CN'); * in `packages/spec/src/system/operation-message.test.ts`; the two move * together. */ -const ZH_SENTENCE = '您无权修改或删除这条记录,如需修改请联系该记录的负责人或管理员。'; +const ZH_SENTENCE = '您无权修改或删除这条记录,如需修改请联系该记录的负责人或管理员。'; /** The legacy hardcoded reason, kept as a literal so its ABSENCE is pinned. */ const LEGACY_EN = 'insufficient privileges to'; diff --git a/packages/spec/src/system/operation-message.test.ts b/packages/spec/src/system/operation-message.test.ts index d1f3735e82..458d813629 100644 --- a/packages/spec/src/system/operation-message.test.ts +++ b/packages/spec/src/system/operation-message.test.ts @@ -17,7 +17,7 @@ describe('operation message catalog', () => { it('renders the caller locale, not English', () => { const zh = renderOperationMessage({ messageKey: 'delete_restricted', params: PARAMS }, { locale: 'zh-CN' }); - expect(zh).toBe('该部门正被 1 条零星申请记录通过「申报部门」引用,请先删除或改派这些记录。'); + expect(zh).toBe('该部门正被 1 条零星申请记录通过「申报部门」引用,请先删除或改派这些记录。'); }); it('falls back to en for an unknown locale, and en is the declared fallback', () => { @@ -115,7 +115,7 @@ describe('operation message catalog — permission_denied (#7414)', () => { it('renders the caller locale, not English', () => { expect(renderOperationMessage({ messageKey: 'permission_denied' }, { locale: 'zh-CN' })) - .toBe('您没有执行此操作的权限,如需访问请联系管理员。'); + .toBe('您没有执行此操作的权限,如需访问请联系管理员。'); expect(renderOperationMessage({ messageKey: 'permission_denied' }, { locale: 'en' })) .toBe('You do not have permission to perform this action. Contact your administrator if you need access.'); }); @@ -205,11 +205,11 @@ describe('operation message catalog — the row-level user copy (#7451)', () => it('renders the caller locale, not English', () => { expect(renderOperationMessage({ messageKey: 'record_access_denied' }, { locale: 'zh-CN' })) - .toBe('您无权访问这条记录,如需访问请联系该记录的负责人或管理员。'); + .toBe('您无权访问这条记录,如需访问请联系该记录的负责人或管理员。'); expect(renderOperationMessage({ messageKey: 'record_access_denied' }, { locale: 'en' })) .toBe('You do not have access to this record. Contact the person who owns it, or your administrator, if you need access.'); expect(renderOperationMessage({ messageKey: 'record_change_not_allowed' }, { locale: 'zh-CN' })) - .toBe('您无权将这条记录保存为当前填写的内容,请修改后重试,或联系管理员。'); + .toBe('您无权将这条记录保存为当前填写的内容,请修改后重试,或联系管理员。'); }); it('matches a base language against a regional catalog key (ja → ja-JP)', () => { @@ -322,11 +322,11 @@ describe('operation message catalog — sharing write denial and approvals recal it('renders the caller locale, not English', () => { expect(renderOperationMessage({ messageKey: 'record_write_denied' }, { locale: 'zh-CN' })) - .toBe('您无权修改或删除这条记录,如需修改请联系该记录的负责人或管理员。'); + .toBe('您无权修改或删除这条记录,如需修改请联系该记录的负责人或管理员。'); expect(renderOperationMessage({ messageKey: 'record_write_denied' }, { locale: 'en' })) .toBe('You do not have access to change or delete this record. Contact the person who owns it, or your administrator, if you need to make changes.'); expect(renderOperationMessage({ messageKey: 'approval_recall_not_submitter' }, { locale: 'zh-CN' })) - .toBe('只有提交人可以撤回这条审批请求,如需撤回请联系提交人或管理员。'); + .toBe('只有提交人可以撤回这条审批请求,如需撤回请联系提交人或管理员。'); expect(renderOperationMessage({ messageKey: 'approval_recall_not_submitter' }, { locale: 'en' })) .toBe('Only the person who submitted this approval request can recall it. Contact the submitter, or your administrator, if it needs to be recalled.'); }); diff --git a/packages/spec/src/system/operation-message.ts b/packages/spec/src/system/operation-message.ts index b1468be4dd..f12174c56b 100644 --- a/packages/spec/src/system/operation-message.ts +++ b/packages/spec/src/system/operation-message.ts @@ -221,19 +221,19 @@ export const BUILTIN_OPERATION_MESSAGES: Record> 'This {{object}} is still referenced by {{dependentObject}} record(s) through “{{field}}”, which is required and cannot be cleared. Delete or reassign them first.', }, 'zh-CN': { - permission_denied: '您没有执行此操作的权限,如需访问请联系管理员。', - record_access_denied: '您无权访问这条记录,如需访问请联系该记录的负责人或管理员。', - record_change_not_allowed: '您无权将这条记录保存为当前填写的内容,请修改后重试,或联系管理员。', - record_write_denied: '您无权修改或删除这条记录,如需修改请联系该记录的负责人或管理员。', - approval_recall_not_submitter: '只有提交人可以撤回这条审批请求,如需撤回请联系提交人或管理员。', + permission_denied: '您没有执行此操作的权限,如需访问请联系管理员。', + record_access_denied: '您无权访问这条记录,如需访问请联系该记录的负责人或管理员。', + record_change_not_allowed: '您无权将这条记录保存为当前填写的内容,请修改后重试,或联系管理员。', + record_write_denied: '您无权修改或删除这条记录,如需修改请联系该记录的负责人或管理员。', + approval_recall_not_submitter: '只有提交人可以撤回这条审批请求,如需撤回请联系提交人或管理员。', delete_restricted: - '该{{object}}正被 {{count}} 条{{dependentObject}}记录通过「{{field}}」引用,请先删除或改派这些记录。', + '该{{object}}正被 {{count}} 条{{dependentObject}}记录通过「{{field}}」引用,请先删除或改派这些记录。', delete_restricted_required: - '该{{object}}正被 {{count}} 条{{dependentObject}}记录通过「{{field}}」引用,且该字段为必填、无法清空,请先删除或改派这些记录。', + '该{{object}}正被 {{count}} 条{{dependentObject}}记录通过「{{field}}」引用,且该字段为必填、无法清空,请先删除或改派这些记录。', delete_restricted_opaque: - '该{{object}}正被{{dependentObject}}记录通过「{{field}}」引用,请先删除或改派这些记录。', + '该{{object}}正被{{dependentObject}}记录通过「{{field}}」引用,请先删除或改派这些记录。', delete_restricted_required_opaque: - '该{{object}}正被{{dependentObject}}记录通过「{{field}}」引用,且该字段为必填、无法清空,请先删除或改派这些记录。', + '该{{object}}正被{{dependentObject}}记录通过「{{field}}」引用,且该字段为必填、无法清空,请先删除或改派这些记录。', }, 'ja-JP': { permission_denied: 'この操作を実行する権限がありません。アクセスが必要な場合は管理者にお問い合わせください。',