Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .changeset/18190-translation-messages-example-single-segment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'@objectstack/spec': patch
---

docs(translation): the two `TranslationData` / `TranslationItem` `@example` blocks stop teaching a `messages` id that cannot resolve (#18190)

`messages` is declared a flat `Record<string, string>` (`translation.zod.ts` — `messages: z.record(z.string(), z.string())`), while `t()` resolves a key by walking its dot path segment by segment. Both implementations do this, identically:

- `packages/core/src/fallbacks/memory-i18n.ts` — `resolveKey()`, `key.split('.')`, walked by `t()`;
- `packages/services/service-i18n/src/file-i18n-adapter.ts` — a second `resolveKey()` with the same body, walked by `t()` through `resolveFromLocale()`.

So an id that merely *contains* a dot is one flat key named `common.save`, and `t('messages.common.save', …)` looks for a nested `common` object, finds a string or nothing at the first hop, and returns the key itself. Both docblock `@example` blocks on this schema demonstrated exactly that id — the doorway an author (or an authoring agent) copies from.

- The JSON example on `TranslationDataSchema` and the TypeScript example on `TranslationItemSchema` now author `commonSave`, the single-segment spelling `content/docs/protocol/kernel/i18n-standard.mdx` already prescribes and `packages/plugins/plugin-audit/src/translations/messages.ts` already applies to its own bundle.
- Both docblocks now state the rule, so the counter-example is named as one rather than demonstrated.

⚠️ **The schema still accepts a dotted id** — nothing is narrowed here and no key is retired. Whether the door should refuse a dotted `messages` key narrows a published accept set and rides its own card; this change is the doorway half only.

For authors: a `messages` id containing a dot never resolved, so re-spelling one single-segment (`'common.save'` → `commonSave`, looked up as `messages.commonSave`) turns a key that was returning itself into one that translates. No key that resolved before stops resolving.
15 changes: 13 additions & 2 deletions packages/spec/src/system/translation.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -574,9 +574,16 @@ const TRANSLATION_KEY_GUIDANCE: Record<LegacyObjectFirstKey | 'validationMessage
* {
* "objects": { "account": { "label": "Account" } },
* "apps": { "crm": { "label": "CRM" } },
* "messages": { "common.save": "Save" }
* "messages": { "commonSave": "Save" }
* }
* ```
*
* `messages` ids are single-segment. `t()` resolves a key by walking its dot
* path into this structure, and `messages` is a flat `Record<string, string>`,
* so an id that itself contains a dot (`'common.save'`) is looked up as a
* nested `common` object and resolves to nothing; `messages.commonSave`
* resolves. Both implementations walk identically — see
* `content/docs/protocol/kernel/i18n-standard.mdx`.
*/
/**
* The translation groups, as a shape rather than a schema.
Expand Down Expand Up @@ -1414,6 +1421,10 @@ export type TranslationConfig = z.input<typeof TranslationConfigSchema>;
* sync skips an item whose locale it cannot resolve, and a skip is invisible
* to whoever — or whatever — authored it.
*
* `messages` ids are single-segment, for the reason spelled out on
* {@link TranslationDataSchema}: `t()` walks the dot path, so an id containing
* a dot resolves to nothing.
*
* @example
* ```typescript
* const zhCN = defineTranslation({
Expand All @@ -1427,7 +1438,7 @@ export type TranslationConfig = z.input<typeof TranslationConfigSchema>;
* },
* },
* apps: { crm: { label: '客户关系管理' } },
* messages: { 'common.save': '保存' },
* messages: { commonSave: '保存' },
* });
* ```
*/
Expand Down
Loading