fix(example-todo): re-key the three locale bundles' 19 dotted messages ids single-segment, so all 57 strings resolve - #19149
Queued
huangyiirene wants to merge 4 commits into
Queued
huangyiirene wants to merge 4 commits into
huangyiirene wants to merge 4 commits into
Conversation
… single-segment, so they resolve
`t()` resolves a key by `key.split('.')` and walking segment by segment --
identically in `packages/core/src/fallbacks/memory-i18n.ts` and
`packages/services/service-i18n/src/file-i18n-adapter.ts` -- while
`TranslationData.messages` is a FLAT `Record<string, string>`. An id that
merely *contains* a dot is therefore one key NAMED `common.save`, and
`t('messages.common.save', locale)` looks for a nested `common` object, finds
none, and returns the key string.
`examples/app-todo` authored exactly that: 19 ids per locale across `en`,
`zh-CN` and `ja-JP` -- 57 translated strings that resolved to nothing, in the
reference app an author copies from. They are re-keyed to the single-segment
spelling the standard prescribes (`commonSave`), so the strings are kept and
the example demonstrates the reachable shape rather than the trap.
`message-id-resolution.test.ts` demonstrates it rather than asserting it: the
real bundle is loaded into BOTH real providers and all 57 ids are resolved
through the public `t()` contract, with the old dotted spelling driven through
the same call as the control.
Claude-Session: https://claude.ai/code/session_019hBqDVrwbijUCoK9qsss2E
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019hBqDVrwbijUCoK9qsss2E Co-authored-by: Claude <noreply@anthropic.com>
…ack alone
`check:test-source-alias` reds on a NEW unaliased artifact import: importing
`FileI18nAdapter` from `@objectstack/service-i18n` would add a seventh entry to
this package's shrink-only unaliased-artifact ledger, and the gate's own remedy
is an anchored alias in `examples/app-todo/vitest.config.ts` -- outside this
change's declared file surface.
The proof is unaffected in substance: both shipped implementations resolve a key
with the same code (`key.split('.')` walked segment by segment), so what the
suite establishes about these ids holds for both. The docblock now says which
provider is driven and why the other is not.
Claude-Session: https://claude.ai/code/session_019hBqDVrwbijUCoK9qsss2E
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019hBqDVrwbijUCoK9qsss2E Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Sep 18, 2026
huangyiirene
marked this pull request as ready for review
September 19, 2026 00:04
huangyiirene
added this pull request to the merge queue
Sep 19, 2026
Any commits made after this event will not be merged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #18566
Clause-②: no
examples/app-todoauthored itsmessagesids dot-separated ('common.save'). That spelling is unreachable rather than merely unconventional:t()resolves a key bykey.split('.')and walking segment by segment, whileTranslationData.messagesis a FLAT string-to-string record.t('messages.common.save', locale)therefore looks for a nestedcommonobject, 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 inen.ts,zh-CN.tsandja-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:It is a tombstone for the retired
validationMessagesgroup, and "these messages" are validation-RULE messages. Themessagesblock 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[].messageis 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: novalidations[]entry on the Task object carries any of these nineteen strings.Three further reasons point the same way:
content/docs/protocol/kernel/i18n-standard.mdx:493-500prescribes;TranslationDataSchemaandTranslationItemSchemadocblocks demonstrate after class (c): a dottedmessagesid is authorable, schema-valid and resolves to nothing — and TranslationItemSchema's own @example teaches it, at two sites #18190 ("messages": { "commonSave": "Save" });packages/plugins/plugin-audit/src/translations/messages.tsalready does for its own bundle, for the stated reason.And this is a reference example. Deleting the block would leave
messagesundemonstrated 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
messagesobject literal (scan frommessages: {to its matching close brace by brace depth), then a per-line key scan accepting single-quoted, double-quoted and bare keys. Deliberately not anawk/grepline range, which is what can clip a block's final entry. Output onorigin/main, before the change: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:Premise checks
t()implementations resolve bysplit('.')and walk segment by segmentpackages/core/src/fallbacks/memory-i18n.tsresolveKeyandpackages/services/service-i18n/src/file-i18n-adapter.tsresolveKeyare the same four lines of logic. Not diverged.git grep -nE 'messages\.common\.' origin/main -- examples packagesreturns zero readsmessagesresolves 2x inen.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.examples/**examples/app-todo/src/translations/is quietdda969cd; nothing in the recent window onexamples/app-todo/touches it.The repair is demonstrated, not asserted
examples/app-todo/src/translations/message-id-resolution.test.tsloads the real bundle into the real provider and resolves every id through the publict()contract — 121 cases:t('messages.ID', locale)returns the authored string, for all 19 ids x 3 locales;t('objects.todo_task.label', 'en')isTask,'zh-CN'is the Chinese label;Reverse verification (run from the committed state, so the restore point really exists).
scripts/ablation-replace.mjsputcommonSaveback to'common.save'inen.tsand ran the suite: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.saveis unreachable whichever spelling introduces it.Verification
Run on
19dda8c9(this branch's head after mergingorigin/main).pnpm --filter '@objectstack/example-todo^...' buildpnpm --filter @objectstack/example-todo testpnpm --filter @objectstack/example-todo typechecktsc --listFilesconfirms the new test file is in the programpnpm --filter @objectstack/spec check:generatedmainmergenode scripts/pm/dispatch-gates.mjs --commands(no paths)--ranreconciles 37 derived / 37 run / 0 NOT-MEASURED / 0 UNRUNpnpm lint(repo-wide,eslint . --no-inline-config)Two gates needed a second run for reasons outside this diff, and both then passed:
check:dual-build-cjs-loadsfirst reportedPREREQUISITE NOT MET(packages outside this app's dependency closure had nodist/in this worktree), andcheck:dts-closurenamedplugin-hono-serverandservice-cluster-redisas missing declaration files that their own builds had emitted minutes earlier — the#15042shape the gate's own failure text describes. A rebuild of the named packages restored 153/153 declarations across 61 packages.Deviations, declared
FileI18nAdapterfrom@objectstack/service-i18nwould add a seventh entry to this package's shrink-only unaliased-artifact ledger, andcheck:test-source-aliasreds on it by name. Its prescribed remedy is an anchored alias inexamples/app-todo/vitest.config.ts— outside this change's declared file surface, which isexamples/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.skip-changesetis owed and this PR could not apply it.@objectstack/example-todois"private": truewith nofiles[]— nothing published moves, which is exactly theskip-changesetcase, 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, soCheck Changesetwill sit red until a seat appliesskip-changeset. That is the only expected red.Acceptance notes
Observed while working here, out of scope, filed nowhere:
packages/lint/src/lint-liveness-properties.test.tsuses{ 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 class (c): a dottedmessagesid is authorable, schema-valid and resolves to nothing — and TranslationItemSchema's own @example teaches it, at two sites #18190 — narrowingTranslationItemSchema.messagesto single-segment keys — cannot land without re-spelling them, so that card carries them.check:dts-closure/ declarations vanishing fromdist/after a successful build, in this worktree, twice (packages/mcp, thenplugin-hono-server+service-cluster-redis). Noted, not filed: the gate's own text names this as#15042reproducing, so the card exists. Successor: whoever works#15042.{currentTask.days_overdue},而todo_task没有这个字段 —— 每一封都渲染成「Due …,(空)day(s) overdue.」 #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
TranslationItemSchemais untouched, andpackages/spechas zero changes. Restrictingmessagesto single-segment keys is the deferred half 2 of #18190 and rides its own card.Generated by Claude Code