Skip to content

Commit ca37605

Browse files
i18n(service-messaging): ship the notification objects and digest heading in zh-Hant, ar, fr and pt-BR (第 5 包的 A 半) (#30638)
<!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgBxjikPH2bUin9o95gbAxsx)_ domain:server#2 · os-bill · session_01Utp6Cai5CRdAs4u9gTipWU Refs #30512 — **第 5 包的 A 半** (package 5 of 11, `@objectstack/service-messaging`, first half: `zh-Hant`, `ar`, `fr`, `pt-BR`). The B half (`ru`, `de`, `ko`) follows as its own PR; the card stays open until the last PR of the series flips the bundle check to the platform locale table. Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么:** 消息/通知七个对象(收件箱、回执、投递、偏好、模板、HTTP 投递、用户设备)的名称、字段、选项、帮助和视图名,以及摘要标题「您有 {{count}} 条通知」,补上繁中、阿语、法语、巴葡 4 种。这是第 5 包的 A 半;俄、德、韩在 B 半。 - **为什么改:** 平台语言表(#29428 裁决 F)要求 11 种语言;本包整包超过 3000 行,按席位裁决按语言拆成 A/B 两半,各自单独可绿。 - **风险与回滚:** 只加翻译数据和注册行,`messages.ts` 的语言类型只加宽到这 4 种(B 半再加 3 种);不动逻辑、schema、接口,原有 4 种一字未改。三处测试改动都是**前提变了、不是放宽**(见下文)。回滚即撤 PR。 - **先看:** `src/translations/index.ts`、`messages.ts`(阿语摘要标题用计数式写法,避开复数变化),再看 `keyed-notification-text.integration.test.ts`。 - **两分钟验证:** `node scripts/check-i18n-bundles.mjs --filter=messaging` 输出 in sync;`pnpm --filter @objectstack/service-messaging test` 全绿。 - **需要你做的:** 无,席位落地。 ## What changes - Four new locales for the messaging objects under the platform locale table's tags: each a `TAG.objects.generated.ts` with all 203 object leaves plus its `TAG.source-hashes.generated.ts`, written by the repo's own `os i18n extract --fill=default --source-hashes`, served through `withSourceFallback`, `zh-CN` ahead of `zh-Hant` in `index.ts`. - `messages.ts`: the digest heading in the four locales; `MessagingLocale` widens to eight tags (the B half widens it to eleven), so each half is green on its own. - The extract command lists the four tags and its merge baseline imports the four bundles, so `check:i18n` covers them from this PR on. ## Test changes — the premise moved, nothing loosened - `keyed-notification-text.integration.test.ts` used `fr-FR` as its recipient with no pack. With `fr` shipping, that premise is false for French: the no-pack recipient and deployment default become `it-IT` (Italian is not a platform-table language) with the same assertions, including the single warning, and a new case pins a `fr-FR` recipient reading the `fr` heading (`Vous avez 2 notifications`). - `admin-copy-vocabulary.test.ts` and `setup-list-empty-states.test.ts` list the bundles they hold to their wording rules; the four new bundles join that list, so the rules now also cover them. ## Acceptance notes - **Quality bar (#17508 / PR #19345):** every leaf written for the reader; placeholders, code spans and identifiers byte-identical, checked per leaf against `en`. Arabic is RTL prose with identifiers in Latin script. `t()` selects no plural form, so the Arabic digest heading is a tally (`عدد الإشعارات لديك: {{count}}`), correct for every count; the comment in `messages.ts` says why. - **Leaves equal to English, on purpose:** product and protocol names `APNs`, `FCM`, `Android`, `iOS`, and one help leaf that is a list of status codes, in every locale; beyond those, native words only: `fr` `Notifications`, `Partition`, `Source`, `Version`, `Web`; `pt-BR` `Status`, `Web`. `pt-BR` writes the payload as `Carga útil` rather than the English loanword. - No cross-locale coincidence: `check:i18n-stale-fill` passes with the baseline untouched. ## Verification <details><summary>Package suite, typecheck, build (verify lock, at the rebased head)</summary> ``` turbo build --filter=@objectstack/service-messaging...: 16 tasks successful, exit 0 pnpm --filter @objectstack/service-messaging test && typecheck Test Files 66 passed (66) Tests 845 passed (845) os-verify-lock: VERDICT command-exit 0 ``` </details> <details><summary>i18n gates and size</summary> ``` node scripts/check-i18n-bundles.mjs --filter=messaging in sync EXIT 0 node scripts/check-i18n-stale-fill.mjs OK (no new stale fills, 1 baselined) EXIT 0 eslint --no-inline-config on the changed .ts files: exit 0 changed lines 2195 (+2178 / -17), 15 files: under the 3000-line threshold ``` </details> Derived gate families (dispatch-gates --commands, reconciled with --ran): 62 run, 0 not measured, all exit 0; recorded in the os-dev-report on #30512. Dogfood: not run — content-only package PR, no `!` in the title and no `(narrowing)` arm. --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Utp6Cai5CRdAs4u9gTipWU --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2d6402c commit ca37605

15 files changed

Lines changed: 2178 additions & 17 deletions
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/service-messaging': patch
3+
---
4+
5+
i18n(service-messaging): the notification objects and the digest heading ship in Traditional Chinese, Arabic, French and Brazilian Portuguese
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The messaging objects (`sys_inbox_message`, `sys_notification_receipt`, `sys_notification_delivery`, `sys_notification_preference`, `sys_notification_template`, `sys_http_delivery`, `sys_user_device`) and the digest heading "You have {{count}} notifications" now carry `zh-Hant`, `ar`, `fr` and `pt-BR` beside the existing `en`, `zh-CN`, `ja-JP` and `es-ES`, so a recipient in one of those languages reads the notification screens and the digest heading in their own language instead of English. `t()` selects no plural form, so the Arabic heading is worded as a tally (`عدد الإشعارات لديك: {{count}}`), correct for every count. A recipient whose language is `fr-FR` now reads the `fr` pack. Each new locale is registered under its platform-table tag with its source-hash companion and its messages, and the package's extract command lists the tags, so the existing `check:i18n` bundle check covers them. First half of package 5 of #30512; `ru`, `de` and `ko` follow in the second half.

‎packages/services/service-messaging/scripts/i18n-extract.config.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* `--merge` preserves every hand-translated string.
88
*
99
* os i18n extract packages/services/service-messaging/scripts/i18n-extract.config.ts \
10-
* --locales=zh-CN,ja-JP,es-ES --fill=default --objects-only --no-metadata-forms \
10+
* --locales=zh-CN,ja-JP,es-ES,zh-Hant,ar,fr,pt-BR --fill=default --objects-only --no-metadata-forms \
1111
* --source-hashes \
1212
* --out=packages/services/service-messaging/src/translations
1313
*
@@ -49,6 +49,10 @@ import { enObjects } from '../src/translations/en.objects.generated.js';
4949
import { zhCNObjects } from '../src/translations/zh-CN.objects.generated.js';
5050
import { jaJPObjects } from '../src/translations/ja-JP.objects.generated.js';
5151
import { esESObjects } from '../src/translations/es-ES.objects.generated.js';
52+
import { zhHantObjects } from '../src/translations/zh-Hant.objects.generated.js';
53+
import { arObjects } from '../src/translations/ar.objects.generated.js';
54+
import { frObjects } from '../src/translations/fr.objects.generated.js';
55+
import { ptBRObjects } from '../src/translations/pt-BR.objects.generated.js';
5256

5357
/**
5458
* The annotation is load-bearing, not decoration (#10868). `defineStack`
@@ -79,6 +83,10 @@ const config: ObjectStackDefinition = defineStack({
7983
{ 'zh-CN': { objects: zhCNObjects } },
8084
{ 'ja-JP': { objects: jaJPObjects } },
8185
{ 'es-ES': { objects: esESObjects } },
86+
{ 'zh-Hant': { objects: zhHantObjects } },
87+
{ 'ar': { objects: arObjects } },
88+
{ 'fr': { objects: frObjects } },
89+
{ 'pt-BR': { objects: ptBRObjects } },
8290
],
8391
});
8492

‎packages/services/service-messaging/src/keyed-notification-text.integration.test.ts‎

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,9 @@ const USERS = [
8686
{ id: 'u_en', name: 'Ada', email: 'ada@example.com', phone_number: '+15550000001', locale: 'en' },
8787
{ id: 'u_ja', name: 'Sato', email: 'sato@example.com', phone_number: '+81300000001', locale: 'ja-JP' },
8888
{ id: 'u_none', name: 'Nobody', email: 'nobody@example.com', phone_number: '+15550000002' },
89-
// A language no pack in this deployment holds.
89+
// A language no pack in this deployment holds (Italian is not a platform-table language).
90+
{ id: 'u_it', name: 'Rossi', email: 'rossi@example.com', phone_number: '+390600000001', locale: 'it-IT' },
91+
// A regional tag of a platform-table language: the messaging bundle ships `fr` (#30512).
9092
{ id: 'u_fr', name: 'Dupont', email: 'dupont@example.com', phone_number: '+33100000001', locale: 'fr-FR' },
9193
];
9294

@@ -487,22 +489,34 @@ describe("#25015 — a digest's heading is rendered in its recipient's language"
487489
i18n: catalog({ messagingBundle: true, defaultLocale: 'zh-CN' }),
488490
});
489491

490-
const [digest] = await digestOf(ql, c, 'u_fr', 2);
492+
const [digest] = await digestOf(ql, c, 'u_it', 2);
491493

492494
expect(digest.title).toBe('您有 2 条通知');
493495
});
494496

495-
it('when neither the recipient\'s language nor the deployment default has a pack, the English literal — said once', async () => {
497+
it('a regional recipient language reads its platform-table pack (`fr-FR` reads `fr`, #30512)', async () => {
496498
const ql = await boot();
497499
const c = compose(ql, {
498-
outbox: 'memory', preferenceResolver: digestAll, deploymentDefault: 'fr-FR',
499-
i18n: catalog({ messagingBundle: true, defaultLocale: 'fr-FR' }),
500+
outbox: 'memory', preferenceResolver: digestAll, deploymentDefault: 'zh-CN',
501+
i18n: catalog({ messagingBundle: true, defaultLocale: 'zh-CN' }),
500502
});
501503

502504
const [digest] = await digestOf(ql, c, 'u_fr', 2);
503505

506+
expect(digest.title).toBe('Vous avez 2 notifications');
507+
});
508+
509+
it('when neither the recipient\'s language nor the deployment default has a pack, the English literal — said once', async () => {
510+
const ql = await boot();
511+
const c = compose(ql, {
512+
outbox: 'memory', preferenceResolver: digestAll, deploymentDefault: 'it-IT',
513+
i18n: catalog({ messagingBundle: true, defaultLocale: 'it-IT' }),
514+
});
515+
516+
const [digest] = await digestOf(ql, c, 'u_it', 2);
517+
504518
expect(digest.title).toBe('You have 2 notifications');
505-
expect(c.warnings.filter((w) => w.includes(`'${DIGEST_HEADING_KEY}'`) && w.includes('fr-FR'))).toHaveLength(1);
519+
expect(c.warnings.filter((w) => w.includes(`'${DIGEST_HEADING_KEY}'`) && w.includes('it-IT'))).toHaveLength(1);
506520
});
507521

508522
it("a one-item digest keeps its item's own title, rendered in the recipient's language", async () => {

‎packages/services/service-messaging/src/objects/admin-copy-vocabulary.test.ts‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ import { enObjects } from '../translations/en.objects.generated.js';
2626
import { zhCNObjects } from '../translations/zh-CN.objects.generated.js';
2727
import { jaJPObjects } from '../translations/ja-JP.objects.generated.js';
2828
import { esESObjects } from '../translations/es-ES.objects.generated.js';
29+
import { zhHantObjects } from '../translations/zh-Hant.objects.generated.js';
30+
import { arObjects } from '../translations/ar.objects.generated.js';
31+
import { frObjects } from '../translations/fr.objects.generated.js';
32+
import { ptBRObjects } from '../translations/pt-BR.objects.generated.js';
2933

3034
/** The design-record vocabulary an admin-facing string must not carry. */
3135
const DESIGN_RECORD_VOCABULARY: ReadonlyArray<readonly [string, RegExp]> = [
@@ -90,7 +94,16 @@ function isObjectSchema(v: unknown): v is ObjectLike {
9094
&& !!(v as ObjectLike).fields && typeof (v as ObjectLike).fields === 'object';
9195
}
9296

93-
const OBJECT_BUNDLES = { en: enObjects, 'zh-CN': zhCNObjects, 'ja-JP': jaJPObjects, 'es-ES': esESObjects } as const;
97+
const OBJECT_BUNDLES = {
98+
en: enObjects,
99+
'zh-CN': zhCNObjects,
100+
'ja-JP': jaJPObjects,
101+
'es-ES': esESObjects,
102+
'zh-Hant': zhHantObjects,
103+
'ar': arObjects,
104+
'fr': frObjects,
105+
'pt-BR': ptBRObjects,
106+
} as const;
94107

95108
describe('messaging objects: admin-facing copy carries no design-record vocabulary', () => {
96109
it('the matcher recognises each kind of vocabulary it exists to keep out', () => {

‎packages/services/service-messaging/src/objects/setup-list-empty-states.test.ts‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ import { enObjects } from '../translations/en.objects.generated.js';
2828
import { zhCNObjects } from '../translations/zh-CN.objects.generated.js';
2929
import { jaJPObjects } from '../translations/ja-JP.objects.generated.js';
3030
import { esESObjects } from '../translations/es-ES.objects.generated.js';
31+
import { zhHantObjects } from '../translations/zh-Hant.objects.generated.js';
32+
import { arObjects } from '../translations/ar.objects.generated.js';
33+
import { frObjects } from '../translations/fr.objects.generated.js';
34+
import { ptBRObjects } from '../translations/pt-BR.objects.generated.js';
3135

3236
interface ViewLike {
3337
columns?: unknown;
@@ -45,7 +49,16 @@ interface ObjectLike {
4549

4650
const OBJECTS = [NotificationPreference, NotificationTemplate] as unknown as ObjectLike[];
4751

48-
const BUNDLES = { en: enObjects, 'zh-CN': zhCNObjects, 'ja-JP': jaJPObjects, 'es-ES': esESObjects } as const;
52+
const BUNDLES = {
53+
en: enObjects,
54+
'zh-CN': zhCNObjects,
55+
'ja-JP': jaJPObjects,
56+
'es-ES': esESObjects,
57+
'zh-Hant': zhHantObjects,
58+
'ar': arObjects,
59+
'fr': frObjects,
60+
'pt-BR': ptBRObjects,
61+
} as const;
4962

5063
function views(def: ObjectLike): Array<[string, ViewLike]> {
5164
return Object.entries(def.listViews ?? {});

0 commit comments

Comments
 (0)