Repository navigation
Commit 76e4c2c
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
- .changeset
- docs/audits/2026-07-unknown-key-strictness-ledger.counts
- packages
- services/service-settings/src/translations
- spec/src/system
- ui/apps/console/src/pages/settings
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
22 | | - | |
| 22 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
700 | 700 | | |
701 | 701 | | |
702 | 702 | | |
| 703 | + | |
| 704 | + | |
703 | 705 | | |
704 | 706 | | |
705 | 707 | | |
706 | 708 | | |
| 709 | + | |
| 710 | + | |
707 | 711 | | |
708 | 712 | | |
709 | 713 | | |
710 | 714 | | |
| 715 | + | |
| 716 | + | |
711 | 717 | | |
712 | 718 | | |
713 | 719 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
745 | 745 | | |
746 | 746 | | |
747 | 747 | | |
| 748 | + | |
| 749 | + | |
748 | 750 | | |
749 | 751 | | |
750 | 752 | | |
751 | 753 | | |
| 754 | + | |
| 755 | + | |
752 | 756 | | |
753 | 757 | | |
754 | 758 | | |
755 | 759 | | |
| 760 | + | |
| 761 | + | |
756 | 762 | | |
757 | 763 | | |
758 | 764 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
597 | 597 | | |
598 | 598 | | |
599 | 599 | | |
| 600 | + | |
| 601 | + | |
600 | 602 | | |
601 | 603 | | |
602 | 604 | | |
603 | 605 | | |
| 606 | + | |
| 607 | + | |
604 | 608 | | |
605 | 609 | | |
606 | 610 | | |
607 | 611 | | |
| 612 | + | |
| 613 | + | |
608 | 614 | | |
609 | 615 | | |
610 | 616 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
754 | 754 | | |
755 | 755 | | |
756 | 756 | | |
| 757 | + | |
| 758 | + | |
757 | 759 | | |
758 | 760 | | |
759 | 761 | | |
760 | 762 | | |
| 763 | + | |
| 764 | + | |
761 | 765 | | |
762 | 766 | | |
763 | 767 | | |
764 | 768 | | |
| 769 | + | |
| 770 | + | |
765 | 771 | | |
766 | 772 | | |
767 | 773 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
594 | 594 | | |
595 | 595 | | |
596 | 596 | | |
| 597 | + | |
| 598 | + | |
597 | 599 | | |
598 | 600 | | |
599 | 601 | | |
600 | 602 | | |
| 603 | + | |
| 604 | + | |
601 | 605 | | |
602 | 606 | | |
603 | 607 | | |
604 | 608 | | |
| 609 | + | |
| 610 | + | |
605 | 611 | | |
606 | 612 | | |
607 | 613 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
560 | 560 | | |
561 | 561 | | |
562 | 562 | | |
| 563 | + | |
| 564 | + | |
563 | 565 | | |
564 | 566 | | |
565 | 567 | | |
566 | 568 | | |
| 569 | + | |
| 570 | + | |
567 | 571 | | |
568 | 572 | | |
569 | 573 | | |
570 | 574 | | |
| 575 | + | |
| 576 | + | |
571 | 577 | | |
572 | 578 | | |
573 | 579 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
738 | 738 | | |
739 | 739 | | |
740 | 740 | | |
| 741 | + | |
| 742 | + | |
741 | 743 | | |
742 | 744 | | |
743 | 745 | | |
744 | 746 | | |
| 747 | + | |
| 748 | + | |
745 | 749 | | |
746 | 750 | | |
747 | 751 | | |
748 | 752 | | |
| 753 | + | |
| 754 | + | |
749 | 755 | | |
750 | 756 | | |
751 | 757 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
748 | 748 | | |
749 | 749 | | |
750 | 750 | | |
| 751 | + | |
| 752 | + | |
751 | 753 | | |
752 | 754 | | |
753 | 755 | | |
754 | 756 | | |
| 757 | + | |
| 758 | + | |
755 | 759 | | |
756 | 760 | | |
757 | 761 | | |
758 | 762 | | |
| 763 | + | |
| 764 | + | |
759 | 765 | | |
760 | 766 | | |
761 | 767 | | |
| |||
0 commit comments