Skip to content

Commit 76e4c2c

Browse files
feat(spec): translate a settings map specifier's column headers (#31484)
<!-- claude-projects-attribution: {"github_login":"marchtian"} --> _Requested by **田騏** · [project thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSiR1iqus8kMX7vh9PVAPJir9)_ Fixes #31453 Clause-②: yes (widening) Metadata-protocol: none ## 维护者速读 - **改了什么**:平台设置翻译包里,每个设置项可以翻译 map 表格的表头了:`keyColumn`(键列)和 `columns.列名`(各值列),各含 `label`(表头)与 `help`(悬停说明)。数据生命周期三张表的 5 个表头已写进 10 个非英文语言包,控制台表格读取这些翻译。 - **为什么改**:用中文等非英文界面打开「数据生命周期」时,保留期覆盖、行数配额、按类别默认配额三张表的表头一直是英文,翻译包里没有地方能放它们。 - **风险与回滚**:只放宽平台翻译包的解析(两个新的可选键,形状严格),存储、保存校验和 manifest 都不变;应用自带的翻译包仍然整组拒绝 `settings`。直接 revert 即可回滚。 - **先看**:`packages/spec/src/system/translation.zod.ts`(`keys` 条目新增 `keyColumn` / `columns` 与 `settingsMapColumnTranslation`);`ui/apps/console/src/pages/settings/SettingsMapField.tsx`(表头读翻译);`packages/services/service-settings/src/translations/settings-translation-coverage.test.ts`(表头纳入逐语言覆盖校验)。 - **两分钟验证**:`pnpm --dir ui exec vitest run apps/console/src/pages/settings/__tests__/SettingsView.mapRows-31391.test.tsx -t 31453`,预期 2 条通过:中文下表头显示「对象 / 行保留时长」,没翻译的列保留英文。 - **需要你做的**:无,席位落地。 ## What changes - **Spec** (`packages/spec/src/system/translation.zod.ts`). The platform bundle's `settings.NAMESPACE.keys.KEY` entry gains two optional groups, spelled as the `map` specifier spells its headers: - `keyColumn`: the key column, `{ label, help }`; - `columns`: a record keyed by the column `name`, each `{ label, help }`. `label` translates the specifier column's `label`; `help` translates its `description`, the spelling the setting entry already uses for a specifier's `description`. Both are built with the same `strictObject({ surface, history: TRANSLATION_HISTORY, aliases })` helper as their siblings. On the setting entry, `keyHeader` is answered `keyColumn` and `headers` / `columnLabels` are answered `columns`. On a column, `title` / `header` / `heading` are answered `label`, `description` / `helpText` / `hint` are answered `help`, and a specifier column pasted in whole gets a prescription for `name` (it is the record key, `columns.NAME`) and `kind` (not translatable), never a rename. A bundle written before this change parses unchanged. `ServedTranslationDataSchema` spreads the same shape, so the served document carries the groups with no other change. `SpecifierSchema`'s `keyColumn` / `columns` TSDoc names the translation address, as `confirmOffText` does. - **Bundles** (`@objectstack/service-settings`). zh-CN, zh-Hant, ja-JP, ko, es-ES, pt-BR, fr, de, ru and ar translate the five headers of `lifecycle.retention_overrides`, `lifecycle.quotas` and `lifecycle.quota_defaults` (`Object`, `Keep rows for`, `Expire rows after`, `Max rows`, `Lifecycle class`). The coverage test holds every `map` header to the same per-locale coverage as the setting's label, and refuses header copy on a setting that is not a map or for a column the map does not declare. - **Console overlay** (cross-lane `ui/` files, named in the claim's file surface). `keys.KEY.label` is overlaid in the console, by `useSettingsLabel`, not on the server; the new leaves are read there too: `mapKeyColumnLabel` / `mapKeyColumnHelp` / `mapColumnLabel` / `mapColumnHelp`, handed to `SettingsMapField` as `headerText`. A miss keeps the manifest's text in the page language (`pickLocalized`), as every other settings string does. `help` is the header's hover text; the key column now shows its declared `description` there too, as the value columns already did, so both declared leaves are read. Served Data Lifecycle page in zh-CN, `retention_overrides` headers (measured by the new console test against the shipped zh-CN strings): | | key column | `maxAge` | `expireAfter` | |---|---|---|---| | before | Object | Keep rows for | Expire rows after | | after | 对象 | 行保留时长 | 行过期时长 | ## Tests - `packages/spec/src/system/translation.test.ts`, `a map setting's column headers`: the leaves parse on the platform face, the platform bundle and the served document; the four entry-level aliases name their targets; an undeclared leaf on a column and on the key column is refused at its path with the alias named; a bare string is refused as `invalid_type` at its path; a pasted specifier column gets the `columns.NAME` prescription and no `name` rename. - `settings-translation-coverage.test.ts`: both new directions. Mutation check: renaming zh-CN's `expireAfter` entry to `expireAt` reds both `missing for lifecycle: key:retention_overrides.columns.expireAfter` and `undeclared settings copy: lifecycle.key:retention_overrides.columns.expireAt`. - `SettingsView.mapRows-31391.test.tsx`, new top-level `describe` in the existing harness: zh-CN headers and hover text through the real `SettingsView` render path, an untranslated column keeps its literal, the translated header still names the row's cells and key picker, and a one-column map reads its own key. Reverse check: with `headerText` unwired, both cases red (`[ 'Object', 'Keep rows for', … ]`). ## Acceptance notes - **Shape vs the order.** The order proposed `keyColumn` as a plain string because the specifier's key column "carries only `label`". Measured: `SpecifierMapKeyColumnSchema` declares `label` and `description`, and `SpecifierMapColumnSchema` declares `label`, `description`, `kind` and `name`. `description` is user-visible text (the value columns render it as the header's `title`). So both groups are `{ label, help }` objects, which also leaves no string-to-object change owed later. - **Changeset level.** `Metadata-protocol: none` grades every entry `patch` (`scripts/check-changeset-no-major.mjs`), so `@objectstack/spec` is `patch`, not the `minor` the order suggested. `none` holds because `settings` is platform-only: both application doors refuse it by name, and `authorable-surface/` does not move. - **ui exhaustive copies.** This is a nested leaf under `settings`, not a new top-level group. `SpecTranslationData` in `@object-ui/i18n` forwards `settings` opaquely, and nothing in `ui/` enumerates settings-key leaves except `useSettingsLabel`, which this PR extends. `@object-ui/console` type-check is green. - **Not added.** No `resolveSettings*` helper in `i18n-resolver.ts`: that family has no in-repo caller for settings keys, and the `confirmOffText` precedent added none. - **Serial.** #31451 (draws these tables) is merged. No open PR on these files was found. - **Review.** An independent review of the spec diff and the code implementing it: round 1 PASS with should-fix (the `name` alias, a detached TSDoc block, one loose test assertion, wording in four locales), all fixed; delta re-review PASS. ## Verification <details><summary>dispatch-gates: 97 derived, 97 run, 0 NOT-MEASURED</summary> All 97 derived commands exit 0 on this head's tree after `git merge origin/main`. Nine first exited 3 (prerequisite: a stale spec `dist/`, unbuilt packages, the teardown fixture commit missing from the shallow clone) and exit 0 after `turbo run build --filter=./packages/**` and fetching the fixture commit. `check:test-typecheck` needs a larger heap locally (`NODE_OPTIONS=--max-old-space-size=12288`); the debt file was not regenerated. Also: `pnpm --filter @objectstack/spec check:generated` exit 0, `check:doc-authoring` exit 0, eslint on every touched file exit 0, `@object-ui/console` type-check exit 0, spec `translation.test.ts` + `src/shared` 1224 pass, service-settings translations 135 pass, console map/label suites 22 pass. </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent a61ddc9 commit 76e4c2c

20 files changed

Lines changed: 350 additions & 11 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/service-settings': patch
4+
'@object-ui/console': patch
5+
---
6+
7+
feat: a settings `map` specifier's column headers can be translated, and Data Lifecycle's three map tables ship translated headers
8+
9+
Clause-②: yes (widening)
10+
Metadata-protocol: none
11+
12+
- `PlatformTranslationDataSchema` (`@objectstack/spec/system`): the platform bundle's `settings.<namespace>.keys.<key>` entry gains two optional groups, spelled as the `map` specifier spells its headers. `keyColumn` translates the key column and `columns.<name>` each value column, keyed by the column's `name`. Each carries `label` (the header) and `help` (the column's `description`, shown as the header's hover text), and is strict like its siblings: an undeclared leaf is refused, with `description` → `help` and `title` → `label` named, and `headers` / `columnLabels` → `columns`, `keyHeader` → `keyColumn` named on the setting entry. A bundle written before this change parses exactly as it did. `settings` stays platform-only, so no application bundle can write these groups.
13+
- `@objectstack/service-settings`: every shipped non-English bundle (zh-CN, zh-Hant, ja-JP, ko, es-ES, pt-BR, fr, de, ru, ar) translates the five headers of `lifecycle.retention_overrides`, `lifecycle.quotas` and `lifecycle.quota_defaults` (`Object`, `Keep rows for`, `Expire rows after`, `Max rows`, `Lifecycle class`). The settings translation coverage test now holds each `map` header to the same per-locale coverage as the setting's label, and refuses header copy for a setting that is not a map or a column the map does not declare.
14+
- `@object-ui/console`: the Settings page's map table reads the two groups. A header with no translation keeps the manifest's text, as every other settings string does. The key column now shows its declared `description` as hover text, as the value columns already did.
15+
- Stored values, the save door and the manifest shape do not change.

‎docs/audits/2026-07-unknown-key-strictness-ledger.counts/system.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ hand-patch a number here** — fix the code or the verdict and regenerate.
1919

2020
| Dir | Sites |
2121
|---|---|
22-
| `system/` | 399 |
22+
| `system/` | 400 |

‎packages/services/service-settings/src/translations/ar.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,14 +700,20 @@ export const ar: PlatformTranslationData = {
700700
retention_overrides: {
701701
label: 'تجاوزات الاحتفاظ',
702702
help: 'تجاوزات النافذة لكل كائن. تستخدم المدد h/d/w/y. تُضبط لكل مؤسسة، فتحتفظ المؤسسة الخاضعة للتنظيم بسنوات بينما تحتفظ مؤسسة التطوير بأيام. يُتجاهَل التجاوز الأقصر من الحد الأدنى الذي تحتاجه ميزة في المنصة (مثل نافذة كشف التكرار في قائمة انتظار المهام) ويُسجَّل خطأً، وتظل النافذة الافتراضية سارية.',
703+
keyColumn: { label: 'الكائن' },
704+
columns: { maxAge: { label: 'الاحتفاظ بالصفوف لمدة' }, expireAfter: { label: 'انتهاء صلاحية الصفوف بعد' } },
703705
},
704706
quotas: {
705707
label: 'حصص الصفوف',
706708
help: 'الحد الأقصى لعدد الصفوف لكل كائن. يؤدي التجاوز إلى تنبيه حوكمة — ولا تحذف الحصص أبدًا ما يتجاوز السياسة المعلنة.',
709+
keyColumn: { label: 'الكائن' },
710+
columns: { max_rows: { label: 'الحد الأقصى للصفوف' } },
707711
},
708712
quota_defaults: {
709713
label: 'الحصص الافتراضية حسب الفئة',
710714
help: 'حصص احتياطية حسب فئة دورة الحياة (telemetry, transient, event, audit).',
715+
keyColumn: { label: 'فئة دورة الحياة' },
716+
columns: { max_rows: { label: 'الحد الأقصى للصفوف' } },
711717
},
712718
growth_alert_rows: {
713719
label: 'حد تنبيه النمو (صفوف لكل جولة)',

‎packages/services/service-settings/src/translations/de.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -745,14 +745,20 @@ export const de: PlatformTranslationData = {
745745
retention_overrides: {
746746
label: 'Aufbewahrungs-Überschreibungen',
747747
help: 'Fensterüberschreibungen pro Objekt. Dauern verwenden h/d/w/y. Pro Organisation gesetzt, sodass eine regulierte Organisation Jahre aufbewahren kann, eine Entwicklungsorganisation nur Tage. Eine Überschreibung, die kürzer ist als das Minimum, das eine Plattformfunktion braucht (etwa das Duplikaterkennungsfenster der Job-Warteschlange), wird ignoriert und als Fehler protokolliert; das Standardfenster gilt weiter.',
748+
keyColumn: { label: 'Objekt' },
749+
columns: { maxAge: { label: 'Zeilen aufbewahren für' }, expireAfter: { label: 'Zeilen ablaufen lassen nach' } },
748750
},
749751
quotas: {
750752
label: 'Zeilenkontingente',
751753
help: 'Maximale Zeilenanzahl pro Objekt. Eine Überschreitung löst eine Governance-Warnung aus — Kontingente löschen nie über die deklarierte Richtlinie hinaus.',
754+
keyColumn: { label: 'Objekt' },
755+
columns: { max_rows: { label: 'Max. Zeilen' } },
752756
},
753757
quota_defaults: {
754758
label: 'Standardkontingente pro Klasse',
755759
help: 'Ersatzkontingente nach Lebenszyklusklasse (telemetry, transient, event, audit).',
760+
keyColumn: { label: 'Lebenszyklusklasse' },
761+
columns: { max_rows: { label: 'Max. Zeilen' } },
756762
},
757763
growth_alert_rows: {
758764
label: 'Wachstums-Warnschwelle (Zeilen pro Durchlauf)',

‎packages/services/service-settings/src/translations/es-ES.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,14 +597,20 @@ export const esES: PlatformTranslationData = {
597597
+ 'Se define por organización, de modo que una organización regulada puede conservar años y una de desarrollo, días. '
598598
+ 'Una sustitución más corta que el mínimo que necesita una función de la plataforma (por ejemplo, la ventana de detección de duplicados de la cola de trabajos) '
599599
+ 'se ignora y se registra como error, y se sigue aplicando la ventana predeterminada.',
600+
keyColumn: { label: 'Objeto' },
601+
columns: { maxAge: { label: 'Conservar filas durante' }, expireAfter: { label: 'Caducar filas tras' } },
600602
},
601603
quotas: {
602604
label: 'Cuotas de filas',
603605
help: 'Número máximo de filas por objeto. Superarlo genera una alerta de gobernanza: las cuotas nunca eliminan más allá de la política declarada.',
606+
keyColumn: { label: 'Objeto' },
607+
columns: { max_rows: { label: 'Máx. de filas' } },
604608
},
605609
quota_defaults: {
606610
label: 'Cuotas predeterminadas por clase',
607611
help: 'Cuotas de respaldo por clase de ciclo de vida (telemetry, transient, event, audit).',
612+
keyColumn: { label: 'Clase de ciclo de vida' },
613+
columns: { max_rows: { label: 'Máx. de filas' } },
608614
},
609615
growth_alert_rows: {
610616
label: 'Umbral de alerta de crecimiento (filas por barrido)',

‎packages/services/service-settings/src/translations/fr.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,14 +754,20 @@ export const fr: PlatformTranslationData = {
754754
retention_overrides: {
755755
label: 'Surcharges de rétention',
756756
help: 'Surcharges de fenêtre par objet. Les durées utilisent h/d/w/y. Définies par organisation : une organisation réglementée peut conserver des années, une organisation de développement quelques jours. Une surcharge plus courte que le minimum requis par une fonction de la plateforme (par exemple la fenêtre de détection des doublons de la file de tâches) est ignorée et journalisée comme erreur, et la fenêtre par défaut continue de s’appliquer.',
757+
keyColumn: { label: 'Objet' },
758+
columns: { maxAge: { label: 'Conserver les lignes pendant' }, expireAfter: { label: 'Faire expirer les lignes après' } },
757759
},
758760
quotas: {
759761
label: 'Quotas de lignes',
760762
help: 'Nombre maximal de lignes par objet. Un dépassement déclenche une alerte de gouvernance — les quotas ne suppriment jamais au-delà de la politique déclarée.',
763+
keyColumn: { label: 'Objet' },
764+
columns: { max_rows: { label: 'Lignes max.' } },
761765
},
762766
quota_defaults: {
763767
label: 'Quotas par défaut par classe',
764768
help: 'Quotas de repli par classe de cycle de vie (telemetry, transient, event, audit).',
769+
keyColumn: { label: 'Classe de cycle de vie' },
770+
columns: { max_rows: { label: 'Lignes max.' } },
765771
},
766772
growth_alert_rows: {
767773
label: 'Seuil d’alerte de croissance (lignes par passage)',

‎packages/services/service-settings/src/translations/ja-JP.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,14 +594,20 @@ export const jaJP: PlatformTranslationData = {
594594
help: 'オブジェクトごとの保持ウィンドウの上書き。期間は h/d/w/y で指定します。'
595595
+ '組織ごとに設定するため、規制対象の組織は数年、開発用の組織は数日だけ保持できます。'
596596
+ 'プラットフォーム機能が必要とする最小期間(例:ジョブキューの重複検出ウィンドウ)より短い上書きは無視されてエラーとして記録され、既定のウィンドウが引き続き適用されます。',
597+
keyColumn: { label: 'オブジェクト' },
598+
columns: { maxAge: { label: '行の保持期間' }, expireAfter: { label: '行の失効までの期間' } },
597599
},
598600
quotas: {
599601
label: '行数クォータ',
600602
help: 'オブジェクトごとの最大行数。超過するとガバナンスアラートが発生します。クォータが宣言済みのポリシーを超えて削除することはありません。',
603+
keyColumn: { label: 'オブジェクト' },
604+
columns: { max_rows: { label: '最大行数' } },
601605
},
602606
quota_defaults: {
603607
label: 'クラス別の既定クォータ',
604608
help: 'ライフサイクルクラスごとのフォールバッククォータ(telemetry、transient、event、audit)。',
609+
keyColumn: { label: 'ライフサイクルクラス' },
610+
columns: { max_rows: { label: '最大行数' } },
605611
},
606612
growth_alert_rows: {
607613
label: '増加アラートのしきい値(クリーンアップ 1 回あたりの行数)',

‎packages/services/service-settings/src/translations/ko.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,14 +560,20 @@ export const ko: PlatformTranslationData = {
560560
retention_overrides: {
561561
label: '보존 기간 재정의',
562562
help: '객체별 보존 기간 재정의입니다. 기간 단위는 h/d/w/y입니다. 조직별로 설정하므로 규제 대상 조직은 수년, 개발용 조직은 며칠만 보관할 수 있습니다. 플랫폼 기능에 필요한 최소 기간(예: 작업 대기열의 중복 감지 기간)보다 짧은 재정의는 무시되고 오류로 기록되며, 기본 기간이 계속 적용됩니다.',
563+
keyColumn: { label: '객체' },
564+
columns: { maxAge: { label: '행 보존 기간' }, expireAfter: { label: '행 만료 기간' } },
563565
},
564566
quotas: {
565567
label: '행 할당량',
566568
help: '객체별 최대 행 수입니다. 초과하면 거버넌스 알림이 발생하며, 할당량은 선언된 정책을 넘어 데이터를 삭제하지 않습니다.',
569+
keyColumn: { label: '객체' },
570+
columns: { max_rows: { label: '최대 행 수' } },
567571
},
568572
quota_defaults: {
569573
label: '클래스별 기본 할당량',
570574
help: '수명 주기 클래스별 대체 할당량입니다 (telemetry, transient, event, audit).',
575+
keyColumn: { label: '수명 주기 클래스' },
576+
columns: { max_rows: { label: '최대 행 수' } },
571577
},
572578
growth_alert_rows: {
573579
label: '증가 알림 임계값(정리당 행 수)',

‎packages/services/service-settings/src/translations/pt-BR.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,14 +738,20 @@ export const ptBR: PlatformTranslationData = {
738738
retention_overrides: {
739739
label: 'Substituições de retenção',
740740
help: 'Substituições de janela por objeto. As durações usam h/d/w/y. Definidas por organização, para que uma organização regulada guarde anos e uma de desenvolvimento guarde dias. Uma substituição mais curta que o mínimo exigido por um recurso da plataforma (por exemplo, a janela de detecção de duplicatas da fila de tarefas) é ignorada e registrada como erro, e a janela padrão continua valendo.',
741+
keyColumn: { label: 'Objeto' },
742+
columns: { maxAge: { label: 'Manter linhas por' }, expireAfter: { label: 'Expirar linhas após' } },
741743
},
742744
quotas: {
743745
label: 'Cotas de linhas',
744746
help: 'Quantidade máxima de linhas por objeto. Ultrapassar gera um alerta de governança — as cotas nunca excluem além da política declarada.',
747+
keyColumn: { label: 'Objeto' },
748+
columns: { max_rows: { label: 'Máx. de linhas' } },
745749
},
746750
quota_defaults: {
747751
label: 'Cotas padrão por classe',
748752
help: 'Cotas de reserva por classe de ciclo de vida (telemetry, transient, event, audit).',
753+
keyColumn: { label: 'Classe de ciclo de vida' },
754+
columns: { max_rows: { label: 'Máx. de linhas' } },
749755
},
750756
growth_alert_rows: {
751757
label: 'Limite de alerta de crescimento (linhas por varredura)',

‎packages/services/service-settings/src/translations/ru.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,14 +748,20 @@ export const ru: PlatformTranslationData = {
748748
retention_overrides: {
749749
label: 'Переопределения хранения',
750750
help: 'Переопределения срока для объектов. Длительности задаются в h/d/w/y. Задаются для каждой организации, так что регулируемая организация может хранить годы, а организация для разработки — дни. Переопределение короче минимума, нужного функции платформы (например, окна обнаружения дубликатов очереди заданий), игнорируется и записывается как ошибка, а срок по умолчанию продолжает действовать.',
751+
keyColumn: { label: 'Объект' },
752+
columns: { maxAge: { label: 'Хранить строки в течение' }, expireAfter: { label: 'Срок истечения строк' } },
751753
},
752754
quotas: {
753755
label: 'Квоты строк',
754756
help: 'Максимум строк для объекта. Превышение вызывает оповещение управления — квоты никогда не удаляют сверх объявленной политики.',
757+
keyColumn: { label: 'Объект' },
758+
columns: { max_rows: { label: 'Макс. строк' } },
755759
},
756760
quota_defaults: {
757761
label: 'Квоты по умолчанию для классов',
758762
help: 'Резервные квоты по классу жизненного цикла (telemetry, transient, event, audit).',
763+
keyColumn: { label: 'Класс жизненного цикла' },
764+
columns: { max_rows: { label: 'Макс. строк' } },
759765
},
760766
growth_alert_rows: {
761767
label: 'Порог оповещения о росте (строк за проход)',

0 commit comments

Comments
 (0)