Repository navigation
Commit f4ce10c
fix(platform-objects): re-translate the object leaves that contradict their current en source (#20652)
Fixes #20539
Clause-②: no
## What changed
A translated leaf in
`packages/platform-objects/src/apps/translations/{ja-JP,es-ES,zh-CN}.objects.generated.ts`
that a translator wrote by hand keeps its value when its `en` source
changes later. Some of those leaves still said what the old source said.
This PR measures that set first, then re-translates the leaves whose
meaning now **contradicts** the current `en`: **19 leaves** (ja-JP 4,
es-ES 11, zh-CN 4). The leaves whose `en` only gained detail or was
reworded are listed below and left as written.
- Values only. No key is added or dropped, no `en` file is edited, and
no provenance companion or echo-decision ledger changes (measured below;
the tooling requires none).
- `.changeset/20539-stale-authored-object-leaves.md`:
`@objectstack/platform-objects` `patch`.
- No new gate, per triage. No test pins a translation string.
## The measurement (taken on this branch at base `cd901d7a5`, before any
edit)
### Instrument
A diff of `en` since each authored leaf's last edit, as the card names
it, run over git history:
1. **Population.** Every string leaf of each locale's objects bundle at
the ref, minus two classes that are not authored: a leaf byte-equal to
the current `en` leaf (an echo, owned by the echo ledgers), and a leaf
with a row in the locale's `*.source-hashes.generated.ts` (a recorded
byte copy, owned by `check:i18n-stale-fill`). What is left is the
authored set.
2. **Last edit.** Walk every first-parent `main` commit that touched the
locale bundle, oldest first, parse the object literal at each, and
record for each path the commit where its value last changed. Values are
compared parsed, so a reformat is not an edit. For a leaf that landed
through a merge commit, `en` is also read at the branch side.
3. **Verdict.** Compare `en`'s value for the same path at that commit
with `en` today. If they differ, `en` moved after the leaf was last
edited, and the leaf is a candidate. Meaning is then judged by hand for
each candidate.
It reads the `en` **bundle** history rather than the object source
files. Today `en` is the source, byte for byte (`pnpm check:i18n`
green). Before `8af76aebf` the `en` bundle could lag its source, and
that lag only ever adds candidates; it never hides one. The bundle diff
is therefore at least as sensitive as a source diff, and it survives
object files moving between packages. Full history is required: the
checkout was shallow (499 commits) and was unshallowed to 15092 before
the run.
The same logic, condensed, reproduces the full instrument's candidate
sets exactly at `cd901d7a5` and `3fa342501`:
```js
// node instrument.mjs REF (run from the repo root; needs full, unshallowed history)
import { execFileSync as x } from 'node:child_process';
const REF = process.argv[2] || 'HEAD', D = 'packages/platform-objects/src/apps/translations/';
const git = (...a) => x('git', a, { encoding: 'utf8', maxBuffer: 268435456 });
const leaves = (o, pre = '', m = new Map()) => { for (const [k, v] of Object.entries(o)) { const p = pre ? pre + '.' + k : k; if (typeof v === 'string') m.set(p, v); else if (v && typeof v === 'object') leaves(v, p, m); } return m; };
const at = (rev, f) => { const s = git('show', rev + ':' + D + f); return leaves(new Function('return (' + s.slice(s.indexOf('= {', s.indexOf('export const')) + 2, s.lastIndexOf('};') + 1) + ');')()); };
const enNow = at(REF, 'en.objects.generated.ts');
for (const L of ['ja-JP', 'es-ES', 'zh-CN']) {
const f = L + '.objects.generated.ts', last = new Map();
let prev = new Map();
for (const c of git('log', '--first-parent', '--format=%H', REF, '--', D + f).trim().split('\n').reverse()) {
const cur = at(c, f);
for (const [p, v] of cur) if (prev.get(p) !== v) last.set(p, c);
prev = cur;
}
const recorded = git('show', REF + ':' + D + L + '.source-hashes.generated.ts');
let authored = 0; const cands = [];
for (const [p, v] of at(REF, f)) {
if (v === enNow.get(p) || recorded.includes('"objects.' + p + '"')) continue; // echo / recorded byte copy
authored++;
const c = last.get(p), enThen = at(c, 'en.objects.generated.ts').get(p);
if (enThen !== enNow.get(p)) cands.push(p + ' @' + c.slice(0, 9));
}
console.log(L, 'authored=' + authored, 'candidates=' + cands.length);
for (const k of cands) console.log(' ' + k);
}
```
Two widening checks ran beside it:
- **Penultimate edit.** For each non-candidate, the full instrument also
compared `en` at the leaf's previous edit. This catches a sweep that
touched a leaf mechanically after `en` had moved. It flagged 12 / 8 / 12
leaves (ja-JP / es-ES / zh-CN). All of them were re-translated in the
same commit that moved `en`, so none is stale.
- **Retired-term scan.** The since-last-edit instrument cannot see a
leaf that was already wrong at its last edit. For the two renames the
candidates surfaced (ADR-0057 department to business unit, and v5.0
project to environment), every leaf that carries the locale's word for
the retired term (`部門` / `部门` / `departamento` case-insensitive;
`プロジェクト` / `项目` / `proyecto`) was compared with the `en` leaf at the
same path. A leaf is flagged when `en` does not carry the English term
there.
### Known-positive control
`sys_two_factor.fields.backup_codes.help`, line 1095 of all four
bundles. `en` has read "Backup recovery codes, encrypted at rest (a
single opaque ciphertext, not readable JSON)" since `266654d82`. The
three translations were last edited at `7bb92056e`, when `en` read
"JSON-serialized backup recovery codes". The instrument flags it in
**all three** locales, zh-CN included. The retired-term scan's positive
is `sys_metadata.fields.environment_id.label`, which both instruments
flag.
### Population and radius
- **Covered:** the objects bundles of `@objectstack/platform-objects`
for ja-JP, es-ES and zh-CN. There are 1529 leaves per locale. Echoes of
the current `en` number 43 / 54 / 42, recorded byte copies 0 beyond
those, and authored leaves **1486 / 1475 / 1487**. History covered:
every first-parent commit since the bundles were created at `6bacbced2`
(62 / 62 / 63 commits touching each locale file).
- **Not covered:** a leaf that was wrong at its last edit for any reason
other than the two renames above; the platform-objects `metadata-forms`
bundles; the other eight packages' bundle sets. Raw candidate counts for
those are in the acceptance notes.
### Counts per locale
| | ja-JP | es-ES | zh-CN |
|---|---|---|---|
| authored leaves | 1486 | 1475 | 1487 |
| candidates (en moved since last edit), before | 17 | 21 | 17 |
| of which contradicting, before | 4 | 9 | 4 |
| retired-term scan, wrong at authoring, before | 0 | 2 | 0 |
| **re-translated here** | **4** | **11** | **4** |
| candidates after (at `3fa342501`) | 13 | 12 | 13 |
| contradicting after | 0 | 0 | 0 |
| retired-term scan after | 0 | 0 | 0 |
The after-candidate sets are exactly the before sets minus the
re-translated leaves: 0 added, and none of the new values equals its
`en` leaf. The 13 / 12 / 13 that remain are the list below.
### The judgment rule
A leaf **contradicts** the current `en` when a reader who acts on it
would believe something about the current field or object that `en` now
says is false. Three cases:
- `en` now denies what the leaf asserts;
- the object was re-modelled, so the leaf describes a different thing;
- the leaf names the record's entity by a term that a rename retired
from it.
A leaf does **not** contradict when `en` only added detail, narrowed or
reworded without retracting what the leaf says, or changed punctuation.
### Re-translated (19)
| path | locales | why it contradicted |
|---|---|---|
| `sys_two_factor.fields.backup_codes.help` | ja-JP, es-ES, zh-CN | Said
JSON-serialized, where `en` says "a single opaque ciphertext, not
readable JSON" (`266654d82`). |
| `sys_notification.description` | ja-JP, es-ES, zh-CN | Said "per-user
notification inbox entries". ADR-0030 re-modelled the object as one
event row per `emit()`, carrying no recipient; the object header says it
"was previously a per-user inbox". |
| `sys_job_run.description` | ja-JP, es-ES, zh-CN | Said "audit trail".
`ca19ee81f` changed the source to "history" because the object is
job-run history and not the audit trail, which is `sys_audit_log` (the
binding ruling that commit cites). |
| `sys_metadata.fields.environment_id.label` | ja-JP, es-ES, zh-CN |
Said "Project (deprecated)" for the `environment_id` column. The v5.0
rename project to environment has no aliases. |
| `sys_business_unit.fields.manager_user_id.help`,
`.effective_from.help`, `.effective_to.help`,
`sys_business_unit_member.description`,
`sys_business_unit_member.fields.is_primary.help` | es-ES | Named the
business unit a *departamento*, where `en` says business unit (ADR-0057
D2; department is now one `kind` of business unit). `61ed5c7c9` renamed
these in `en`, ja-JP and zh-CN and missed es-ES. |
| `sys_business_unit.fields.manager_user_id.label`,
`sys_business_unit.fields.id.label` | es-ES | Same rename: "Responsable
del departamento" and "ID de departamento" against `en` "Business Unit
Head" and "Business Unit ID". These were wrong when authored
(`e16f2a830`), so the since-last-edit instrument cannot see them; the
retired-term scan found them. |
New values: ja-JP 保存時に暗号化されたバックアップ回復コード(単一の不透明な暗号文であり、読み取り可能な JSON
ではありません) · 通知イベント — emit() 1 回につき 1 行(ADR-0030 レイヤー 2 の入口) ·
バックグラウンドジョブの実行履歴 · 環境(非推奨). zh-CN 备用恢复码,静态存储时加密(单个不透明密文,而非可读的 JSON) ·
通知事件——每次 emit() 产生一行(ADR-0030 第 2 层入口) · 后台任务执行历史 · 环境(已废弃). es-ES uses
*unidad de negocio*, the word the bundle already uses for the object and
its `parent_business_unit_id` / `business_unit_id` labels; the rest is
in the diff.
### Stale but not contradicting (listed, left as written)
| path | locales | what `en` did |
|---|---|---|
| `sys_team.fields.organization_id.help` | all three | Added "Null in
single-tenant; auto-stamped in multi-tenant." |
| `sys_business_unit.fields.organization_id.help` | all three | Added
the same sentence. |
| `sys_business_unit.description` | all three | Reworded and extended
(canonical BU tree, data-partition role, kind examples). "Hierarchical
org tree node" is still true. |
| `sys_business_unit_member.fields.is_primary.help` | ja-JP, zh-CN |
Nothing to fix: the leaf already says business units. It was translated
ahead of `en` at `61ed5c7c9`. |
| `sys_oauth_application.fields.client_secret.help` | all three | Added
the enforcement citation and "shown once". The leaf's "hashed at rest"
is still true. |
| `sys_notification.label` / `.pluralLabel` | all three | Narrowed
"Notification" to "Notification Event". The less specific word is not
false, and nothing else in these bundles carries it: `sys_inbox_message`
is "Inbox Message", in its own package. |
| `sys_notification.fields.actor_id.help` | all three | Reworded "the
notification" to "the event". |
| `sys_email.fields.headers_json.help` | all three | Added "Never
returned on the generic data path…". The leaf does not claim that it is.
|
| `sys_email.fields.error.help` | all three | Widened from transport
errors to transport or pre-delivery validation errors. The leaf is
narrower but not contradicted. This is the closest call. |
| `sys_metadata_history.description` | all three | Reframed as a durable
event log of overlay changes. "Version history" is still true, and the
source never retracted the audit wording: `59261af32` calls the row the
audit row. |
| `sys_metadata_history.fields.metadata.help` | all three | Added "(null
for deletes)". |
| `sys_metadata_history.fields.change_note.help` | all three | Added a
trailing period. |
`sys_notification.description`, the second example the card names, was
measured stale and contradicting in all three locales. It is
re-translated above.
## Dispatch hypotheses, measured
- **H1 held.** The control is live at `f1e921ab8e` and at base
`cd901d7a5`, at line 1095 of all four files, and zh-CN is in the set.
- **H2 falsified.** Neither companion records an authored object leaf.
`*.source-hashes.generated.ts` records only byte copies: 43 / 54 / 42
rows, each an echo of the current `en`. `*.source-hashes.ts` is the hand
table for `apps` / `dashboards` / `pages` only: 130 rows each, 0 under
`objects`. So "recorded hash differs" names nothing here, and the
instrument had to be git history.
- **H3 held: values only.** Measured that no companion changes.
`collectFilledFromHashes` records a leaf only when its value equals the
current source or a recorded digest. At `3fa342501`, `node
scripts/check-i18n-bundles.mjs --write --filter=platform-objects`
rewrote the bundle set byte-identical (`git status` empty). Control:
with ja-JP `sys_job_run.description` set to the `en` string (via
`scripts/ablation-replace.mjs`), `pnpm check:i18n` goes red with
`platform-objects DRIFTED (1)`. Restored after: blob equals HEAD, and
`git diff HEAD` is empty.
- **H4 falsified for this set.** No echo-decision ledger row, and no
test file under `packages/platform-objects`, names any of the 19 paths
(`git grep` exit 1). The old values appear nowhere outside the bundles
(exit 1). Both controls hit: a path pinned in the es-ES ledger, and `Cc`
in the ja-JP ledger. No pin moved.
## Verification (all at `3fa342501`)
- `node scripts/pm/dispatch-gates.mjs --commands` (no paths) derived 55
commands, and all 55 exit 0. `--ran` printed "55 derived famil(ies)
accounted for — 55 run, 0 NOT-MEASURED". `pnpm
check:dual-build-cjs-loads` first refused with exit 3 (nine unrelated
packages had no `dist/`). Those were built and the gate re-ran: exit 0.
- Named by the dispatch: `pnpm check:i18n` ("OK (9 package(s) — all
bundles in sync…)"), `pnpm check:i18n-stale-fill` ("OK (10 bundle set(s)
— no new stale fills, 0 baselined)"), `pnpm check:nul-bytes`, `pnpm
check:published-files` and `pnpm check:cross-package-test-inputs` all
exit 0.
- `pnpm --filter @objectstack/platform-objects test`: 58 files, 942
tests passed. `pnpm --filter @objectstack/platform-objects typecheck`:
exit 0.
- Lint, narrowed to the three edited bundles: `eslint --no-inline-config
--format json` read 3 files, 0 errors, 0 warnings.
`ESLint#isPathIgnored` answers `false` for all three, read from the
repo's own config. The config never enables type-aware linting (no
`parserOptions.project`), so a change to string values in these files
cannot move the verdict on any other file. The full `pnpm lint` run is
left to CI.
## Acceptance notes
- **Beyond the radius** (raw instrument counts at `cd901d7a5`; meaning
judged only where noted):
- platform-objects `metadata-forms`: 9 / 9 / 6;
- plugin-approvals: 17 / 17 / 16. All of these are `en` options moving
from the machine value to a title (`pending` to `Pending`), with no
change of meaning;
- plugin-audit: 3 / 3 / 3;
- plugin-security: 7 / 5 / 7;
- plugin-webhooks: 3 / 3 / 3;
- plugin-sharing and the three services: 0.
Read by hand and reported to the seat as one family finding:
plugin-webhooks `sys_webhook.fields.definition_json.help` in all three
locales says the JSON carries the full headers and auth config, where
`en` says credentials are NOT stored there. plugin-audit
`sys_activity.fields.environment_id.label` still says Project. The
metadata-forms `permission` section still promises custom context
variables, and the `agent` section still promises tools; `en` dropped
both.
- `bu-rename-consistency.test.ts` spells its es-ES term as the
case-sensitive `/Departamento/`. That is why the two lowercase es-ES
labels re-translated here (field labels, which the test's header says it
asserts) passed it. This PR fixes the values. The test's blind spot
remains, and a case-insensitive `/departamento/i` would close it. That
edit is outside this card's file surface and is not made here.
- The history walk needed full history. The shared object store was
unshallowed (`git fetch --unshallow origin main`), which every worktree
on this clone now sees.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent d7631d5 commit f4ce10c
4 files changed
Lines changed: 46 additions & 19 deletions
File tree
- .changeset
- packages/platform-objects/src/apps/translations
| 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 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
Lines changed: 11 additions & 11 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
897 | 897 | | |
898 | 898 | | |
899 | 899 | | |
900 | | - | |
901 | | - | |
| 900 | + | |
| 901 | + | |
902 | 902 | | |
903 | 903 | | |
904 | 904 | | |
905 | 905 | | |
906 | 906 | | |
907 | 907 | | |
908 | 908 | | |
909 | | - | |
| 909 | + | |
910 | 910 | | |
911 | 911 | | |
912 | 912 | | |
913 | | - | |
| 913 | + | |
914 | 914 | | |
915 | 915 | | |
916 | 916 | | |
917 | 917 | | |
918 | 918 | | |
919 | 919 | | |
920 | | - | |
| 920 | + | |
921 | 921 | | |
922 | 922 | | |
923 | 923 | | |
| |||
947 | 947 | | |
948 | 948 | | |
949 | 949 | | |
950 | | - | |
| 950 | + | |
951 | 951 | | |
952 | 952 | | |
953 | 953 | | |
| |||
973 | 973 | | |
974 | 974 | | |
975 | 975 | | |
976 | | - | |
| 976 | + | |
977 | 977 | | |
978 | 978 | | |
979 | 979 | | |
| |||
1092 | 1092 | | |
1093 | 1093 | | |
1094 | 1094 | | |
1095 | | - | |
| 1095 | + | |
1096 | 1096 | | |
1097 | 1097 | | |
1098 | 1098 | | |
| |||
2372 | 2372 | | |
2373 | 2373 | | |
2374 | 2374 | | |
2375 | | - | |
| 2375 | + | |
2376 | 2376 | | |
2377 | 2377 | | |
2378 | 2378 | | |
| |||
2710 | 2710 | | |
2711 | 2711 | | |
2712 | 2712 | | |
2713 | | - | |
| 2713 | + | |
2714 | 2714 | | |
2715 | 2715 | | |
2716 | 2716 | | |
| |||
2999 | 2999 | | |
3000 | 3000 | | |
3001 | 3001 | | |
3002 | | - | |
| 3002 | + | |
3003 | 3003 | | |
3004 | 3004 | | |
3005 | 3005 | | |
| |||
Lines changed: 4 additions & 4 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1092 | 1092 | | |
1093 | 1093 | | |
1094 | 1094 | | |
1095 | | - | |
| 1095 | + | |
1096 | 1096 | | |
1097 | 1097 | | |
1098 | 1098 | | |
| |||
2372 | 2372 | | |
2373 | 2373 | | |
2374 | 2374 | | |
2375 | | - | |
| 2375 | + | |
2376 | 2376 | | |
2377 | 2377 | | |
2378 | 2378 | | |
| |||
2710 | 2710 | | |
2711 | 2711 | | |
2712 | 2712 | | |
2713 | | - | |
| 2713 | + | |
2714 | 2714 | | |
2715 | 2715 | | |
2716 | 2716 | | |
| |||
2999 | 2999 | | |
3000 | 3000 | | |
3001 | 3001 | | |
3002 | | - | |
| 3002 | + | |
3003 | 3003 | | |
3004 | 3004 | | |
3005 | 3005 | | |
| |||
Lines changed: 4 additions & 4 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1092 | 1092 | | |
1093 | 1093 | | |
1094 | 1094 | | |
1095 | | - | |
| 1095 | + | |
1096 | 1096 | | |
1097 | 1097 | | |
1098 | 1098 | | |
| |||
2372 | 2372 | | |
2373 | 2373 | | |
2374 | 2374 | | |
2375 | | - | |
| 2375 | + | |
2376 | 2376 | | |
2377 | 2377 | | |
2378 | 2378 | | |
| |||
2710 | 2710 | | |
2711 | 2711 | | |
2712 | 2712 | | |
2713 | | - | |
| 2713 | + | |
2714 | 2714 | | |
2715 | 2715 | | |
2716 | 2716 | | |
| |||
2999 | 2999 | | |
3000 | 3000 | | |
3001 | 3001 | | |
3002 | | - | |
| 3002 | + | |
3003 | 3003 | | |
3004 | 3004 | | |
3005 | 3005 | | |
| |||
0 commit comments