Skip to content

Commit 33a1af0

Browse files
committed
fix(spec): a translation item is named in snake_case, the locale tag goes in locale
`TranslationItemSchema.name` called the locale tag (`zh-CN`) the conventional item name, which the save door's item-name grammar refuses. The describe, its comment, the generated reference row and the sync's skip warning now say snake_case (`zh_cn`) with the BCP-47 tag in `locale`. The door and the read-side fallback keep their behaviour; a door pin covers both names. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzhKYT8L13Bgj8icT3i
1 parent 1685a06 commit 33a1af0

5 files changed

Lines changed: 45 additions & 8 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): `TranslationItemSchema.name` no longer tells authors to name a translation item after its locale tag
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The `name` describe said a translation item was "conventionally the locale code (`zh-CN`)", but the metadata save door's item-name grammar refuses `zh-CN` (uppercase and a hyphen), so an author or agent following the describe got a 400 `INVALID_REQUEST` on the first save. The describe now says a translation item is named in snake_case like every metadata item (e.g. `zh_cn`), with the BCP-47 tag in `locale`. The save door and the runtime sync's read-side name fallback for rows stored before `locale` became required are unchanged; the sync's skip warning now points authors at `locale` only.

‎content/docs/references/system/translation.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -737,7 +737,7 @@ One locale of translations — the `translation` metadata type
737737
| **metadataForms** | `Record<string, { label?: string; description?: string; sections?: Record<string, object>; fields?: Record<string, object> }>` | optional | Translations for metadata-type configuration forms keyed by metadata type |
738738
| **settingsCommon** | `{ sourceLabels?: object }` | optional | Cross-namespace Settings UI strings |
739739
| **locale** | `string` | ✅ | BCP-47 locale this item translates (e.g. "zh-CN") |
740-
| **name** | `string` | optional | Item name — conventionally the locale code (`zh-CN`); the runtime sync falls back to it when `locale` is absent |
740+
| **name** | `string` | optional | Item name — snake_case like every metadata item (e.g. `zh_cn`); the BCP-47 tag goes in `locale` |
741741
| **label** | `string` | optional | Human-readable label shown in metadata lists |
742742
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
743743
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |

‎packages/core/src/fallbacks/authored-translation-sync.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ export async function readAuthoredTranslationLayer(
192192
if (!locale) {
193193
logger?.warn?.(
194194
`[i18n] authored translation '${row?.name}' has no resolvable locale `
195-
+ "(set the top-level 'locale', or name the item after its BCP-47 locale) — skipped",
195+
+ "(set the top-level 'locale' to its BCP-47 tag, e.g. 'zh-CN') — skipped",
196196
);
197197
continue;
198198
}

‎packages/metadata-protocol/src/protocol.item-name-grammar.test.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,3 +300,27 @@ describe('#12194 — the refusal is scoped to the doors that MINT or PROMOTE', (
300300
).rejects.toMatchObject({ code: 'INVALID_REQUEST', status: 400 });
301301
});
302302
});
303+
304+
describe('#26128 — a translation item is named in snake_case, the BCP-47 tag goes in `locale`', () => {
305+
// `TranslationItemSchema.name` once called the locale tag (`zh-CN`) the
306+
// conventional item name, while this door refused it. The describe moved;
307+
// the door did not. Both halves are pinned here so they cannot drift again.
308+
const TRANSLATION_BODY = { locale: 'zh-CN', label: '简体中文' };
309+
310+
it('refuses a tag-shaped item name (`zh-CN`) with the grammar refusal and persists nothing', async () => {
311+
const { protocol, rows } = makeProtocol();
312+
await expect(
313+
protocol.saveMetaItem({ type: 'translation', name: 'zh-CN', item: { ...TRANSLATION_BODY, name: 'zh-CN' } }),
314+
).rejects.toMatchObject({ code: 'INVALID_REQUEST', status: 400, message: expect.stringMatching(GRAMMAR_MESSAGE) });
315+
expect(metaRows(rows)).toHaveLength(0);
316+
});
317+
318+
it('accepts the snake_case name (`zh_cn`) with `locale: \'zh-CN\'`', async () => {
319+
const { protocol, rows } = makeProtocol();
320+
const result = await protocol.saveMetaItem({ type: 'translation', name: 'zh_cn', item: { ...TRANSLATION_BODY, name: 'zh_cn' } });
321+
expect(result.success).toBe(true);
322+
expect(metaRows(rows)).toHaveLength(1);
323+
expect(metaRows(rows)[0]!.name).toBe('zh_cn');
324+
expect(JSON.parse(metaRows(rows)[0]!.metadata).locale).toBe('zh-CN');
325+
});
326+
});

‎packages/spec/src/system/translation.zod.ts‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2220,12 +2220,15 @@ export const TranslationItemSchema = lazySchema(() => strictObject({
22202220
// Undeclared, they were stripped on every parse, so the metadata door quietly
22212221
// discarded an item's own name on the way through.
22222222
//
2223-
// Not snake_case-constrained the way `job.name` is: translation items are
2224-
// conventionally named after their locale (`zh-CN`), and the runtime sync
2225-
// reads `row.name` as a locale fallback for exactly that reason. Optional
2226-
// because `locale`, not `name`, is this type's required identity — that call
2227-
// was deliberate (a skipped item is invisible) and is not being reopened.
2228-
name: z.string().optional().describe('Item name — conventionally the locale code (`zh-CN`); the runtime sync falls back to it when `locale` is absent'),
2223+
// Named like every other metadata item: snake_case (`zh_cn`), with the BCP-47
2224+
// tag in `locale`. The save door's item-name grammar
2225+
// (`METADATA_ITEM_NAME_PATTERN`) refuses a tag-shaped name such as `zh-CN`,
2226+
// so this shape does not re-check it. The runtime sync still reads a
2227+
// tag-shaped `row.name` as a locale fallback, but only for rows stored before
2228+
// `locale` became required. Optional because `locale`, not `name`, is this
2229+
// type's required identity — that call was deliberate (a skipped item is
2230+
// invisible) and is not being reopened.
2231+
name: z.string().optional().describe('Item name — snake_case like every metadata item (e.g. `zh_cn`); the BCP-47 tag goes in `locale`'),
22292232
label: z.string().optional().describe('Human-readable label shown in metadata lists'),
22302233

22312234
// ADR-0010 — runtime protection envelope (internal — set by the loader).

0 commit comments

Comments
 (0)