Commit 6819dcb
fix(example-todo): re-key the three locale bundles' 19 dotted
Fixes #18566
Clause-②: no
`examples/app-todo` authored its `messages` ids dot-separated
(`'common.save'`). That spelling is unreachable rather than merely
unconventional: `t()` resolves a key by `key.split('.')` and walking
segment by segment, while `TranslationData.messages` is a FLAT
string-to-string record. `t('messages.common.save', locale)` therefore
looks for a nested `common` object, finds none, and returns the key
string.
## The fork: RE-KEY. Every string is kept.
The card named two shapes and deliberately refused to pick. This PR
**re-keys**. All 19 ids in each of the three locales become
single-segment (`commonSave`), the treatment is identical in `en.ts`,
`zh-CN.ts` and `ja-JP.ts`, and **no translated string is deleted** — all
57 values survive and are now reachable.
### Why not delete
The delete arm rests on the file's own comment at `en.ts:111-112`, read
as establishing that these messages are redundant. **That comment is
about a different key.** In full:
```ts
// `validationMessages` retired in spec 17.0.0 (#4667) — no resolver ever read
// it, so the zh-CN / ja-JP strings here were never rendered and the `en` ones
// merely duplicated the rule's own text. The live home for these messages is
// `validations[].message` on the task object.
```
It is a tombstone for the retired `validationMessages` group, and "these
messages" are validation-RULE messages. The `messages` block holds
generic UI strings — Save, Cancel, Delete, Edit, Create, Search, Filter,
Sort, Refresh, Export, Back, Confirm, three success strings, two confirm
prompts, two error strings. `validations[].message` is not their home
and never was, so the file does not say what the delete arm needs it to
say. Nothing else in the tree says it either: no `validations[]` entry
on the Task object carries any of these nineteen strings.
Three further reasons point the same way:
- the single-segment spelling is what
`content/docs/protocol/kernel/i18n-standard.mdx:493-500` prescribes;
- it is what both `TranslationDataSchema` and `TranslationItemSchema`
docblocks demonstrate after #18190 (`"messages": { "commonSave": "Save"
}`);
- it is what
`packages/plugins/plugin-audit/src/translations/messages.ts` already
does for its own bundle, for the stated reason.
And this is a **reference example**. Deleting the block would leave
`messages` undemonstrated in the one app an author copies from — which
is how the trap arrived here in the first place. Re-keying turns 57 dead
strings into 57 working ones *and* leaves the correct shape on display.
## Count reconciliation — the card's 19/57 is RIGHT
The card says 19 dotted ids per locale, 57 total. A PM reading with a
cruder instrument said 18 per locale, 54 total, and asked for this to be
settled.
**Measured here: 19 per locale, 57 total. The card is right; the 18/54
reading is wrong.**
Instrument: a brace-matched extraction of the top-level `messages`
object literal (scan from `messages: {` to its matching close brace by
brace depth), then a per-line key scan accepting single-quoted,
double-quoted and bare keys. Deliberately not an `awk`/`grep` line
range, which is what can clip a block's final entry. Output on
`origin/main`, before the change:
```
en.ts: messages keys total=19 dotted=19 plain=0 last key = "error.load_failed"
zh-CN.ts: messages keys total=19 dotted=19 plain=0 last key = "error.load_failed"
ja-JP.ts: messages keys total=19 dotted=19 plain=0 last key = "error.load_failed"
GRAND TOTAL dotted messages keys across 3 file(s) = 57
```
**Where the missing one went.** It is not a clipped range. Exactly one
id per locale has an underscore in its second segment —
`error.load_failed` — so a character class that omits `_` while
anchoring on the closing quote-colon drops exactly that one key and
nothing else. Measured both ways on the same files:
```
grep -cE "^\s*'[a-z_]+\.[a-z_]+':" -> 19 / 19 / 19 (class includes _)
grep -cE "^\s*'[a-z]+\.[a-z]+':" -> 18 / 18 / 18 (class omits _)
dropped line: en.ts:107 'error.load_failed': 'Failed to load data',
```
## Premise checks
| premise | verdict on this tree |
|---|---|
| Both `t()` implementations resolve by `split('.')` and walk segment by
segment | **Holds.** `packages/core/src/fallbacks/memory-i18n.ts`
`resolveKey` and
`packages/services/service-i18n/src/file-i18n-adapter.ts` `resolveKey`
are the same four lines of logic. Not diverged. |
| `git grep -nE 'messages\.common\.' origin/main -- examples packages`
returns zero reads | **Holds** — exit 1, no matches. Control on the same
instrument: `messages` resolves 2x in `en.ts`. Repo-wide, the only
occurrences of these ids outside the bundles are the #18190 changeset
and the i18n standard, both citing them as the anti-pattern. |
| #18190 does not reach `examples/**` | **Holds** — this change is
independent of it. |
| `examples/app-todo/src/translations/` is quiet | **Holds** — newest
commit touching that subtree is `dda969cd`; nothing in the recent window
on `examples/app-todo/` touches it. |
## The repair is demonstrated, not asserted
`examples/app-todo/src/translations/message-id-resolution.test.ts` loads
the **real bundle** into the **real provider** and resolves every id
through the public `t()` contract — 121 cases:
- every authored id in all three locales is single-segment (a standing
guard, so the trap cannot come back silently);
- `t('messages.ID', locale)` returns the authored string, for all 19 ids
x 3 locales;
- positive control on the walk itself: `t('objects.todo_task.label',
'en')` is `Task`, `'zh-CN'` is the Chinese label;
- negative control: the three retired dotted spellings driven through
the same call return the key string, which is the exact symptom the
shipped ids had.
**Reverse verification** (run from the committed state, so the restore
point really exists). `scripts/ablation-replace.mjs` put `commonSave`
back to `'common.save'` in `en.ts` and ran the suite:
```
ablation-replace: anchor "commonSave: 'Save'," x1 -> x0
ablation-replace: replace "'common.save': 'Save'," x0 -> x1
ablation-replace: blob 6c09a9d -> fc9ef2abfa59 # the mutation is proven ON DISK
x `common.save` is single-segment
x memory-i18n (core fallback) > en > t("messages.common.save") resolves
Tests 2 failed | 119 passed (121)
ablation-replace: blob after restore 6c09a9d == blob at HEAD 6c09a9d
ablation-replace: git diff HEAD empty
```
Direction observed: **turns red**, as predicted, on both the guard and
the resolution assertion. The negative-control suite stayed green under
the mutation, also as predicted — a flat key named `common.save` is
unreachable whichever spelling introduces it.
## Verification
Run on `19dda8c9` (this branch's head after merging `origin/main`).
| what | result |
|---|---|
| `pnpm --filter '@objectstack/example-todo^...' build` | exit 0 |
| `pnpm --filter @objectstack/example-todo test` | exit 0 — 5 files, 227
tests (121 of them new) |
| `pnpm --filter @objectstack/example-todo typecheck` | exit 0; `tsc
--listFiles` confirms the new test file is in the program |
| `pnpm --filter @objectstack/spec check:generated` | exit 0 — 16/16
artifacts current after the `main` merge |
| `node scripts/pm/dispatch-gates.mjs --commands` (no paths) | 37
families derived; **all 37 run, all exit 0**; `--ran` reconciles 37
derived / 37 run / 0 NOT-MEASURED / 0 UNRUN |
| `pnpm lint` (repo-wide, `eslint . --no-inline-config`) | exit 0 |
Two gates needed a second run for reasons outside this diff, and both
then passed: `check:dual-build-cjs-loads` first reported `PREREQUISITE
NOT MET` (packages outside this app's dependency closure had no `dist/`
in this worktree), and `check:dts-closure` named `plugin-hono-server`
and `service-cluster-redis` as missing declaration files that their own
builds had emitted minutes earlier — the `#15042` shape the gate's own
failure text describes. A rebuild of the named packages restored 153/153
declarations across 61 packages.
## Deviations, declared
1. **The suite drives one provider, not two.** Importing
`FileI18nAdapter` from `@objectstack/service-i18n` would add a seventh
entry to this package's shrink-only unaliased-artifact ledger, and
`check:test-source-alias` reds on it by name. Its prescribed remedy is
an anchored alias in `examples/app-todo/vitest.config.ts` — outside this
change's declared file surface, which is
`examples/app-todo/src/translations/` plus `.changeset/`. The proof is
unaffected in substance: both implementations resolve keys with
identical code, verified by reading both. The test docblock records
this.
2. **No changeset; `skip-changeset` is owed and this PR could not apply
it.** `@objectstack/example-todo` is `"private": true` with no `files[]`
— nothing published moves, which is exactly the `skip-changeset` case,
and no changeset in this repo has ever named an example package (their
CHANGELOG entries are dependency bumps only). Applying labels was
outside this run's declared write budget, so `Check Changeset` will sit
red until a seat applies `skip-changeset`. That is the only expected
red.
3. **No maintainer-summary section.** This diff touches no governed
surface, so it is neither the facts layer nor the rules layer that
clause addresses.
## Acceptance notes
Observed while working here, out of scope, filed nowhere:
- `packages/lint/src/lint-liveness-properties.test.ts` uses `{ messages:
{ 'common.save': ... } }` in four fixtures. Noted, not filed: they are
fixtures for a different property's liveness, not an authoring doorway,
and they are not drift anybody has to chase today. Named successor: the
deferred half 2 of #18190 — narrowing `TranslationItemSchema.messages`
to single-segment keys — cannot land without re-spelling them, so that
card carries them.
- `check:dts-closure` / declarations vanishing from `dist/` after a
successful build, in this worktree, twice (`packages/mcp`, then
`plugin-hono-server` + `service-cluster-redis`). Noted, not filed: the
gate's own text names this as `#15042` reproducing, so the card exists.
Successor: whoever works `#15042`.
- #18584 (the `{currentTask.days_overdue}` interpolation defect in this
same app) is **not addressed here** and remains open; it is held serial
behind this change by design.
## What this deliberately does NOT do
`TranslationItemSchema` is untouched, and `packages/spec` has zero
changes. Restricting `messages` to single-segment keys is the deferred
half 2 of #18190 and rides its own card.
---
_Generated by [Claude
Code](https://claude.ai/code/session_019hBqDVrwbijUCoK9qsss2E)_
---------
Co-authored-by: Claude <noreply@anthropic.com>messages ids single-segment, so all 57 strings resolve (#19149)1 parent 87f776b commit 6819dcb
4 files changed
Lines changed: 182 additions & 57 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
85 | 85 | | |
86 | 86 | | |
87 | 87 | | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
88 | 99 | | |
89 | | - | |
90 | | - | |
91 | | - | |
92 | | - | |
93 | | - | |
94 | | - | |
95 | | - | |
96 | | - | |
97 | | - | |
98 | | - | |
99 | | - | |
100 | | - | |
101 | | - | |
102 | | - | |
103 | | - | |
104 | | - | |
105 | | - | |
106 | | - | |
107 | | - | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
108 | 119 | | |
109 | 120 | | |
110 | 121 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
90 | 90 | | |
91 | 91 | | |
92 | 92 | | |
| 93 | + | |
| 94 | + | |
93 | 95 | | |
94 | | - | |
95 | | - | |
96 | | - | |
97 | | - | |
98 | | - | |
99 | | - | |
100 | | - | |
101 | | - | |
102 | | - | |
103 | | - | |
104 | | - | |
105 | | - | |
106 | | - | |
107 | | - | |
108 | | - | |
109 | | - | |
110 | | - | |
111 | | - | |
112 | | - | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
113 | 115 | | |
114 | 116 | | |
Lines changed: 110 additions & 0 deletions
| 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 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
98 | 98 | | |
99 | 99 | | |
100 | 100 | | |
| 101 | + | |
| 102 | + | |
101 | 103 | | |
102 | | - | |
103 | | - | |
104 | | - | |
105 | | - | |
106 | | - | |
107 | | - | |
108 | | - | |
109 | | - | |
110 | | - | |
111 | | - | |
112 | | - | |
113 | | - | |
114 | | - | |
115 | | - | |
116 | | - | |
117 | | - | |
118 | | - | |
119 | | - | |
120 | | - | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
121 | 123 | | |
122 | 124 | | |
0 commit comments