Skip to content

Commit f41ddbd

Browse files
i18n(plugin-sharing): ship the assignment rule object in all eleven platform languages (#30856)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9D7v5H7BaFmL7sUCoN7tSxhm)_ domain:server#1 · os-elon-musk · session_016WV4cya5ej5GQV86RPbRPK Part of #30512 — a follow-up to package 8 (`@objectstack/plugin-sharing`, #30767): the assignment rule object `sys_assignment_rule`, which #30760 added to this package after #30767 landed. It folds in the translations #30066 owed for that object (landing note 6677585327), so no separate card is needed. The card stays open: settings and platform-objects follow, and the last PR of the series makes the bundle check read the platform locale table. Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么:** 新对象「分配规则」(`sys_assignment_rule`) 的名称、说明、字段名与帮助、两组选项(分配给:队列 / 用户;选择成员:轮流 / 负载最少)和「按对象」视图名,补上英文以外全部 10 种语言(简中、日、西 + 繁中、阿、法、巴葡、俄、德、韩),每种 30 条。 - **为什么改:** #30760 加了这个对象,但没进本包的抽取配置,所以没有任何翻译;用任何非英文语言的管理员打开「设置 → 分配规则」都看到英文。本 PR 把它加进抽取配置,从此 `check:i18n` 也覆盖它。 - **风险与回滚:** 只改抽取配置的对象列表、与之同步的归属测试清单(一行)和翻译数据,不动逻辑、schema、接口;其它 5 个对象的译文一字未改。选项「队列」「用户」沿用各语言在队列、共享对象上的同一个词,「收件人类型叫法一致」的测试照常通过。回滚即撤 PR。 - **先看:** `scripts/i18n-extract.config.ts`(对象列表多了 `SysAssignmentRule`)和 `src/translations/bundle-ownership.test.ts`(同步加一行),再抽看任一语言 `TAG.objects.generated.ts` 的 `sys_assignment_rule` 段。 - **两分钟验证:** `node scripts/check-i18n-bundles.mjs --filter=sharing` 输出 `plugins/plugin-sharing in sync`;`pnpm --filter @objectstack/plugin-sharing test` 全绿。 - **需要你做的:** 无,席位落地。 ## What changes - `scripts/i18n-extract.config.ts` lists `SysAssignmentRule` beside the five objects it already extracts, so the repo's own `os i18n extract --fill=default --source-hashes` writes its 30 leaves into `en.objects.generated.ts` and every locale bundle, and `check:i18n` covers them from this PR on. - The 30 leaves (label, plural label, description, the field labels and help, the `recipient_type` options `queue` / `user`, the `strategy` options `round_robin` / `least_loaded`, and the `by_object` view) are translated in `zh-CN`, `ja-JP`, `es-ES`, `zh-Hant`, `ar`, `fr`, `pt-BR`, `ru`, `de` and `ko`. - Each locale reuses its existing vocabulary: the `queue` / `user` options read exactly as on `sys_sharing_rule` and `sys_queue_member`, "Organization", "Name", "Display Label", "Description", "Object", "Active" and "By Object" read as on `sys_sharing_rule`, and the object's name follows the word each locale's `sys_queue` help already uses for assignment rules (for `zh-CN` and `ja-JP` also the Setup menu entry #30760 added). - The `load` help names the `least_loaded` option by its translated label in each locale, and both expression examples (`record.priority == "high"`, `record.status != "closed"`) stay byte-identical. ## Tests - `bundle-ownership.test.ts` (ADR-0029 D8) lists the objects the extract config owns and is red when the two lists differ; `sys_assignment_rule` is added to it, the one line this config change requires (without it the package suite fails: "bundle carries objects this package's extract config does not own"). - `recipient-vocabulary-consistency.test.ts` already scans every object of every locale, so the new object is held to "one option key, one rendering per locale" with no edit. - No other test changes. ## Acceptance notes - **Quality bar (#17508 / PR #19345):** every leaf written for the reader; placeholders, code spans and identifiers byte-identical, checked mechanically per leaf against `en` (key set, `{{holes}}`, `{holes}`, backtick spans, angle tokens: all ten locales OK, zero diffs; 181 leaves each). Arabic is RTL prose with identifiers and the expression examples left in Latin script. `zh-Hant` was checked against OpenCC's Traditional (Taiwan) conversion: no Simplified-only character in the 30 new strings. - **Leaves equal to English, on purpose, each recorded by the extractor as a source copy:** `fr` `Description`, `Condition`; `de` `Name`. All are the native words. - No cross-locale coincidence: `check:i18n-stale-fill` passes with the baseline untouched. - No string opens with `os ` followed by a lowercase word (`check-cli-command-ids` passes), and the whole "Lint · docs & code guards" lane was run locally before opening. ## Verification <details><summary>Package suite, typecheck, build (verify lock)</summary> ``` turbo run build --filter=!@objectstack/docs: 118/118 tasks successful, exit 0 (at 59684ad1, the patch before the ownership line) pnpm --filter @objectstack/plugin-sharing test && typecheck (at 8f3e8b49, the same patch on main c17719d) Test Files 67 passed (67) Tests 1436 passed (1436) check:test-typecheck: OK (tsconfig.test.json) os-verify-lock: VERDICT command-exit 0 first run at 59684ad1, without the bundle-ownership.test.ts line: 1 failed (the ownership guard naming sys_assignment_rule), 1435 passed; VERDICT command-exit 1 ``` </details> <details><summary>i18n gates, the derived families and the docs &amp; code guards lane</summary> ``` node scripts/check-i18n-bundles.mjs --filter=sharing (at 753d30d, this head) check-i18n-bundles: OK (1 package(s) — all bundles in sync, no undeclared authoring keys). EXIT 0 node scripts/check-i18n-stale-fill.mjs (at 753d30d) check-i18n-stale-fill: OK (11 bundle set(s) — no new stale fills, 1 baselined). EXIT 0 dispatch-gates --commands (at 8f3e8b49): 61 families, each run and recorded with its exit code: 61 x exit 0 dispatch-gates --ran: 61 derived, 61 run, 0 NOT-MEASURED pnpm lint (repo-wide, at 8f3e8b49): exit 0 CI lane "Lint · docs & code guards", every command run locally (at 8f3e8b49) 56 commands (lane-verdict excepted), 56 x exit 0, check-cli-command-ids included re-derived at 753d30d: 62 families, the 61 above plus `node scripts/ci/snapshot-next.mjs --self-test` (main added it to lint.yml's CI-engine job); run here: exit 0. `pnpm check:undeclared-dep-imports` (its script changed on main) re-run here: exit 0 changed lines 744 (+742 / -2), 16 files: under the 3000-line human-merge threshold ``` </details> The branch is one commit on `main` `ae82d057` (after #30830 landed). Its diff has the same patch id (`0760465a`) as `8f3e8b49`, where the suite, the derived families, `pnpm lint` and the docs & code guards lane ran on main `c17719dd`; none of the main commits in between touches `plugin-sharing`. Both i18n checks and the two families that changed on main were re-run on this head. Dogfood: not run — content-only package PR, no `!` in the title and no `(narrowing)` arm. --- _Generated by [Claude Code](https://claude.ai/code/session_016WV4cya5ej5GQV86RPbRPK)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 042efb7 commit f41ddbd

16 files changed

Lines changed: 742 additions & 2 deletions
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/plugin-sharing': patch
3+
---
4+
5+
i18n(plugin-sharing): the assignment rule object ships in all eleven platform languages
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
`sys_assignment_rule`, the object an organization authors its own assignment rules in, now carries its label, description, field labels and help, option labels and its "By Object" view name in `zh-CN`, `ja-JP`, `es-ES`, `zh-Hant`, `ar`, `fr`, `pt-BR`, `ru`, `de` and `ko`, so an administrator in any platform language reads Setup → Assignment Rules in their own language instead of English. The object is now listed in the package's extract config, so the existing `check:i18n` bundle check covers it. Its recipient options read the same words as the queue and sharing objects in each language. Part of #30512.

‎packages/plugins/plugin-sharing/scripts/i18n-extract.config.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
*/
4444

4545
import { defineStack, type ObjectStackDefinition } from '@objectstack/spec';
46-
import { SysRecordShare, SysSharingRule, SysShareLink, SysQueue, SysQueueMember } from '../src/objects/index.js';
46+
import { SysRecordShare, SysSharingRule, SysShareLink, SysQueue, SysQueueMember, SysAssignmentRule } from '../src/objects/index.js';
4747
import { enObjects } from '../src/translations/en.objects.generated.js';
4848
import { zhCNObjects } from '../src/translations/zh-CN.objects.generated.js';
4949
import { jaJPObjects } from '../src/translations/ja-JP.objects.generated.js';
@@ -73,7 +73,7 @@ import { sharingPageTranslations } from '../src/translations/pages.js';
7373
* precision: the annotated type is the function's own return type.
7474
*/
7575
const config: ObjectStackDefinition = defineStack({
76-
objects: [SysRecordShare, SysSharingRule, SysShareLink, SysQueue, SysQueueMember] as any,
76+
objects: [SysRecordShare, SysSharingRule, SysShareLink, SysQueue, SysQueueMember, SysAssignmentRule] as any,
7777
pages: [SysQueueDetailPage],
7878
translations: [
7979
{ en: { objects: enObjects, pages: sharingPageTranslations.en } },

‎packages/plugins/plugin-sharing/src/translations/ar.objects.generated.ts‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,5 +343,71 @@ export const arObjects: NonNullable<TranslationData['objects']> = {
343343
label: "حسب قائمة الانتظار"
344344
}
345345
}
346+
},
347+
sys_assignment_rule: {
348+
label: "قاعدة الإسناد",
349+
pluralLabel: "قواعد الإسناد",
350+
description: "ترسل كل سجل جديد يطابق شرطًا إلى قائمة انتظار أو إلى شخص.",
351+
fields: {
352+
organization_id: {
353+
label: "المؤسسة",
354+
help: "المؤسسة التي تنتمي إليها هذه القاعدة."
355+
},
356+
name: {
357+
label: "الاسم",
358+
help: "يعرّف القاعدة. أحرف لاتينية صغيرة وأرقام وشرطات سفلية فقط، ولا يجوز أن يكون اسم قاعدة يوفرها تطبيق مثبّت."
359+
},
360+
label: {
361+
label: "التسمية المعروضة"
362+
},
363+
description: {
364+
label: "الوصف"
365+
},
366+
object_name: {
367+
label: "الكائن",
368+
help: "الكائن الذي ترسل هذه القاعدة سجلاته الجديدة. لا يمكن الإسناد إلا في الكائنات التي لسجلاتها مالك."
369+
},
370+
criteria: {
371+
label: "الشرط",
372+
help: "السجلات الجديدة التي تأخذها هذه القاعدة؛ اتركه فارغًا لتأخذ كل سجل جديد لم تأخذه قاعدة سابقة. يُكتب تعبيرًا على السجل، مثل record.priority == \"high\"."
373+
},
374+
sort_order: {
375+
label: "الترتيب",
376+
help: "عندما يمكن لأكثر من قاعدة من قواعدك أن تأخذ السجل نفسه، تُجرَّب القاعدة ذات الترتيب الأدنى أولًا. تُجرَّب قواعدك دائمًا قبل القواعد التي توفرها التطبيقات المثبّتة."
377+
},
378+
recipient_type: {
379+
label: "الإسناد إلى",
380+
help: "أرسل السجل إلى قائمة انتظار يمكن لأعضائها أخذه، أو مباشرة إلى شخص واحد.",
381+
options: {
382+
queue: "قائمة الانتظار",
383+
user: "المستخدم"
384+
}
385+
},
386+
recipient_id: {
387+
label: "قائمة الانتظار أو المستخدم",
388+
help: "قائمة الانتظار أو الشخص الذي يذهب إليه السجل. يجب أن يكون الشخص عضوًا نشطًا في هذه المؤسسة."
389+
},
390+
strategy: {
391+
label: "اختيار عضو",
392+
help: "لقائمة انتظار: أعطِ السجل مباشرة لعضو واحد، بالتناوب أو باختيار من لديه أقل عمل مفتوح. اتركه فارغًا لإبقاء السجل في قائمة الانتظار حتى يأخذه عضو.",
393+
options: {
394+
round_robin: "بالتناوب",
395+
least_loaded: "الأقل عبئًا"
396+
}
397+
},
398+
load: {
399+
label: "العمل المفتوح",
400+
help: "مطلوب مع «الأقل عبئًا»: السجلات التي تُحتسب عملًا مفتوحًا للعضو، ويُكتب تعبيرًا على السجل، مثل record.status != \"closed\"."
401+
},
402+
active: {
403+
label: "نشِط",
404+
help: "أوقفه لتتوقف هذه القاعدة عن إسناد السجلات الجديدة دون حذفها."
405+
}
406+
},
407+
_views: {
408+
by_object: {
409+
label: "حسب الكائن"
410+
}
411+
}
346412
}
347413
};

‎packages/plugins/plugin-sharing/src/translations/bundle-ownership.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const OWNED_OBJECTS = new Set([
2222
'sys_share_link',
2323
'sys_queue',
2424
'sys_queue_member',
25+
'sys_assignment_rule',
2526
]);
2627

2728
describe('objects translation bundle ownership (ADR-0029 D8)', () => {

‎packages/plugins/plugin-sharing/src/translations/de.objects.generated.ts‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,5 +343,71 @@ export const deObjects: NonNullable<TranslationData['objects']> = {
343343
label: "Nach Warteschlange"
344344
}
345345
}
346+
},
347+
sys_assignment_rule: {
348+
label: "Zuweisungsregel",
349+
pluralLabel: "Zuweisungsregeln",
350+
description: "Sendet jeden neuen Datensatz, der eine Bedingung erfüllt, an eine Warteschlange oder an eine Person.",
351+
fields: {
352+
organization_id: {
353+
label: "Organisation",
354+
help: "Die Organisation, zu der diese Regel gehört."
355+
},
356+
name: {
357+
label: "Name",
358+
help: "Kennzeichnet die Regel. Nur Kleinbuchstaben, Ziffern und Unterstriche, und nicht der Name einer Regel, die eine installierte App bereitstellt."
359+
},
360+
label: {
361+
label: "Anzeigebezeichnung"
362+
},
363+
description: {
364+
label: "Beschreibung"
365+
},
366+
object_name: {
367+
label: "Objekt",
368+
help: "Das Objekt, dessen neue Datensätze diese Regel weiterleitet. Zuweisen lassen sich nur Objekte, deren Datensätze einen Besitzer haben."
369+
},
370+
criteria: {
371+
label: "Bedingung",
372+
help: "Welche neuen Datensätze diese Regel übernimmt; leer lassen, um jeden neuen Datensatz zu übernehmen, den keine frühere Regel übernommen hat. Geschrieben als Ausdruck über den Datensatz, zum Beispiel record.priority == \"high\"."
373+
},
374+
sort_order: {
375+
label: "Reihenfolge",
376+
help: "Wenn mehrere Ihrer Regeln denselben Datensatz übernehmen könnten, wird die mit der niedrigsten Reihenfolge zuerst versucht. Ihre Regeln werden immer vor den Regeln installierter Apps versucht."
377+
},
378+
recipient_type: {
379+
label: "Zuweisen an",
380+
help: "Sendet den Datensatz an eine Warteschlange, aus der ihre Mitglieder ihn übernehmen können, oder direkt an eine Person.",
381+
options: {
382+
queue: "Warteschlange",
383+
user: "Benutzer"
384+
}
385+
},
386+
recipient_id: {
387+
label: "Warteschlange oder Benutzer",
388+
help: "Die Warteschlange oder die Person, an die der Datensatz geht. Eine Person muss ein aktives Mitglied dieser Organisation sein."
389+
},
390+
strategy: {
391+
label: "Mitglied auswählen",
392+
help: "Für eine Warteschlange: gibt den Datensatz direkt einem Mitglied, reihum oder dem Mitglied mit der wenigsten offenen Arbeit. Leer lassen, damit der Datensatz in der Warteschlange bleibt, bis ein Mitglied ihn übernimmt.",
393+
options: {
394+
round_robin: "Reihum",
395+
least_loaded: "Geringste Auslastung"
396+
}
397+
},
398+
load: {
399+
label: "Offene Arbeit",
400+
help: "Erforderlich bei „Geringste Auslastung“: welche Datensätze als offene Arbeit eines Mitglieds zählen, geschrieben als Ausdruck über den Datensatz, zum Beispiel record.status != \"closed\"."
401+
},
402+
active: {
403+
label: "Aktiv",
404+
help: "Ausschalten, damit diese Regel keine neuen Datensätze mehr zuweist, ohne sie zu löschen."
405+
}
406+
},
407+
_views: {
408+
by_object: {
409+
label: "Nach Objekt"
410+
}
411+
}
346412
}
347413
};

‎packages/plugins/plugin-sharing/src/translations/de.source-hashes.generated.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919

2020
export const deGeneratedSourceHashes: Readonly<Record<string, string>> = {
21+
"objects.sys_assignment_rule.fields.name.label": "5274417ff1ccaf01",
2122
"objects.sys_queue.fields.name.label": "5274417ff1ccaf01",
2223
"objects.sys_queue_member.fields.recipient_type.options.position": "9a24c02e441eb0b4",
2324
"objects.sys_queue_member.fields.recipient_type.options.team": "42b87165ac826b2b",

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

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,5 +343,71 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
343343
label: "By Queue"
344344
}
345345
}
346+
},
347+
sys_assignment_rule: {
348+
label: "Assignment Rule",
349+
pluralLabel: "Assignment Rules",
350+
description: "Sends each new record that matches a condition to a queue or a person.",
351+
fields: {
352+
organization_id: {
353+
label: "Organization",
354+
help: "The organization this rule belongs to."
355+
},
356+
name: {
357+
label: "Name",
358+
help: "Identifies the rule. Lowercase letters, digits and underscores only, and not the name of a rule an installed app provides."
359+
},
360+
label: {
361+
label: "Display Label"
362+
},
363+
description: {
364+
label: "Description"
365+
},
366+
object_name: {
367+
label: "Object",
368+
help: "The object whose new records this rule sends somewhere. Only objects whose records have an owner can be assigned."
369+
},
370+
criteria: {
371+
label: "Condition",
372+
help: "Which new records this rule takes; leave it empty to take every new record that no earlier rule took. Written as an expression over the record — for example record.priority == \"high\"."
373+
},
374+
sort_order: {
375+
label: "Order",
376+
help: "When several of your rules could take the same record, the one with the lowest order is tried first. Your rules are always tried before the rules installed apps provide."
377+
},
378+
recipient_type: {
379+
label: "Assign To",
380+
help: "Send the record to a queue, where its members can take it, or straight to one person.",
381+
options: {
382+
queue: "Queue",
383+
user: "User"
384+
}
385+
},
386+
recipient_id: {
387+
label: "Queue or User",
388+
help: "The queue, or the person, the record goes to. A person must be an active member of this organization."
389+
},
390+
strategy: {
391+
label: "Pick a Member",
392+
help: "For a queue: give the record straight to one member, taking turns or choosing whoever has the least open work. Leave it empty to leave the record in the queue until a member takes it.",
393+
options: {
394+
round_robin: "Round robin",
395+
least_loaded: "Least loaded"
396+
}
397+
},
398+
load: {
399+
label: "Open Work",
400+
help: "Required for \"Least loaded\": which records count as a member's open work, written as an expression over the record — for example record.status != \"closed\"."
401+
},
402+
active: {
403+
label: "Active",
404+
help: "Turn off to stop this rule from assigning new records, without deleting it."
405+
}
406+
},
407+
_views: {
408+
by_object: {
409+
label: "By Object"
410+
}
411+
}
346412
}
347413
};

‎packages/plugins/plugin-sharing/src/translations/es-ES.objects.generated.ts‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,5 +343,71 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
343343
label: "By Queue"
344344
}
345345
}
346+
},
347+
sys_assignment_rule: {
348+
label: "Regla de asignación",
349+
pluralLabel: "Reglas de asignación",
350+
description: "Envía cada registro nuevo que cumple una condición a una cola o a una persona.",
351+
fields: {
352+
organization_id: {
353+
label: "Organización",
354+
help: "La organización a la que pertenece esta regla."
355+
},
356+
name: {
357+
label: "Nombre",
358+
help: "Identifica la regla. Solo letras minúsculas, dígitos y guiones bajos, y no puede ser el nombre de una regla que proporcione una aplicación instalada."
359+
},
360+
label: {
361+
label: "Nombre visible"
362+
},
363+
description: {
364+
label: "Descripción"
365+
},
366+
object_name: {
367+
label: "Objeto",
368+
help: "El objeto cuyos registros nuevos envía esta regla. Solo se pueden asignar los objetos cuyos registros tienen propietario."
369+
},
370+
criteria: {
371+
label: "Condición",
372+
help: "Qué registros nuevos toma esta regla; déjela vacía para tomar todos los registros nuevos que ninguna regla anterior haya tomado. Se escribe como una expresión sobre el registro, por ejemplo record.priority == \"high\"."
373+
},
374+
sort_order: {
375+
label: "Orden",
376+
help: "Cuando varias de sus reglas pueden tomar el mismo registro, se prueba primero la de menor orden. Sus reglas siempre se prueban antes que las que proporcionan las aplicaciones instaladas."
377+
},
378+
recipient_type: {
379+
label: "Asignar a",
380+
help: "Envía el registro a una cola, donde sus miembros pueden tomarlo, o directamente a una persona.",
381+
options: {
382+
queue: "Cola",
383+
user: "Usuario"
384+
}
385+
},
386+
recipient_id: {
387+
label: "Cola o usuario",
388+
help: "La cola, o la persona, a la que va el registro. Una persona debe ser un miembro activo de esta organización."
389+
},
390+
strategy: {
391+
label: "Elegir un miembro",
392+
help: "Para una cola: entrega el registro directamente a un miembro, por turnos o eligiendo a quien tenga menos trabajo abierto. Déjelo vacío para que el registro permanezca en la cola hasta que un miembro lo tome.",
393+
options: {
394+
round_robin: "Por turnos",
395+
least_loaded: "Menor carga"
396+
}
397+
},
398+
load: {
399+
label: "Trabajo abierto",
400+
help: "Obligatorio con «Menor carga»: qué registros cuentan como trabajo abierto de un miembro, escrito como una expresión sobre el registro, por ejemplo record.status != \"closed\"."
401+
},
402+
active: {
403+
label: "Activo",
404+
help: "Desactívela para que esta regla deje de asignar registros nuevos, sin eliminarla."
405+
}
406+
},
407+
_views: {
408+
by_object: {
409+
label: "Por objeto"
410+
}
411+
}
346412
}
347413
};

0 commit comments

Comments
 (0)