Skip to content

Commit 95f818d

Browse files
fix(spec): a translation item is named in snake_case, the locale tag goes in locale (#26769)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} --> _Requested by **Yibo Wang** · [project thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBGfQhhK2rkH91cc4DbNzUXk)_ Fixes #26128 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:翻译条目 `name` 的说明改为 snake_case(如 `zh_cn`),语言标签写在 `locale`;不再建议用 `zh-CN` 作条目名。保存门与读侧回退行为不变。 - 为什么改:旧说明叫作者用 `zh-CN` 命名,而保存门的条目名语法拒收它(400 INVALID_REQUEST),照规范写的作者第一次保存必失败。 - 风险与回滚:仅文案与一条门禁测试,无接受集变化;回滚即 revert 本 PR。 - 先看:`packages/spec/src/system/translation.zod.ts`(name 的 describe 与上方注释)、`packages/metadata-protocol/src/protocol.item-name-grammar.test.ts`(新增 #26128 段)、`packages/core/src/fallbacks/authored-translation-sync.ts`(跳过警告文案)。 - 两分钟验证:`pnpm --filter @objectstack/metadata-protocol exec vitest run src/protocol.item-name-grammar.test.ts`,预期 27/27 通过,其中 `zh-CN` 被拒、`zh_cn` + `locale: 'zh-CN'` 保存成功。 - 需要你做的:无,席位落地。 ## What changes - `TranslationItemSchema.name` describe: `Item name — snake_case like every metadata item (e.g. \`zh_cn\`); the BCP-47 tag goes in \`locale\``. The comment above it now says the same and that the tag-shaped `row.name` fallback covers only rows stored before `locale` became required. - `content/docs/references/system/translation.mdx`: the generated `name` row, regenerated with `gen:schema` + `gen:docs`. - `authored-translation-sync.ts`: the no-locale skip warning now says `set the top-level 'locale' to its BCP-47 tag, e.g. 'zh-CN'` instead of suggesting the item be named after the tag. Behaviour unchanged (the existing test matches `no resolvable locale`). - Door pin (`protocol.item-name-grammar.test.ts`): `saveMetaItem` of type `translation` named `zh-CN` is refused with `code: INVALID_REQUEST`, `status: 400` and the grammar message, persisting nothing; `zh_cn` with `locale: 'zh-CN'` saves and keeps the tag in `locale`. - `refuseUngrammaticalMetaItemName` and the read-side fallback are untouched. - Docs, skills and examples grep (`conventionally the locale`, `name the item after`, a `name: 'zh-CN'` translation item): no hits outside the files above. - Changeset: `'@objectstack/spec': patch`, `'@objectstack/core': patch` (the sync warn text). - `packages/objectql/src/protocol-publish-canonical-fold.test.ts` (about line 269): the test-helper comment now says the name is snake_case (`zh_cn`) with the tag in `locale`; its fixture already used `zh_cn`. Added in patch round r1 (claim surface amended 15:13Z). ## Tests Tests ran on `33a1af012`; head `f308adfc4` is that commit plus a merge of `origin/main` (`a485b5a3e`) that touches none of these files. - `metadata-protocol` `protocol.item-name-grammar.test.ts`: 27/27 (25 existing + 2 new). - `spec` `src/system/translation.test.ts`: 182/182. `core` `src/fallbacks`: 67/67. `service-i18n` `authored-translations.test.ts`: 16/16. - `metadata-protocol` full suite: 269 files passed, 3 skipped; 21582 tests passed, 19 skipped. - r1, on `ad3a211fe`: `objectql` `protocol-publish-canonical-fold.test.ts` 10/10. ## Acceptance notes - `packages/spec/src/system/translation.test.ts` (about line 757) parses a schema-level item named `zh-CN`; the schema does not constrain `name`, so this stays valid at the schema and is not a door fixture. Left as is. ## Verification - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 106 derived families, run serially on `33a1af012`. 102 exited 0 on the first pass; 4 exited 3 (prerequisite: built output) and exited 0 after a full `pnpm turbo run build` (`check:skill-examples`, `check:dual-build-cjs-loads`, `check:i18n-walk-parity`, `check:lean-entry-closure`). - `--ran` reconciliation: `106 derived famil(ies) accounted for — 106 run, 0 NOT-MEASURED`. The derived list on head `f308adfc4` is byte-identical to the one on `33a1af012`. - After the `os-regen-merge.sh` merge, on `f308adfc4`: spec build + `gen:schema` + `gen:docs` left the tree clean; `check:generated`, `check:docs`, `check:authorable-surface` exit 0. - r1, head `ad3a211fe` (comment + changeset only): the derived list grew to 107 (`check-engine-split-ratio.mjs --self-test` added). The 23 families reading the changeset, objectql tests or repo-wide text were re-run on `ad3a211fe`, all exit 0; `--ran` over the merged record: `107 derived famil(ies) accounted for — 107 run, 0 NOT-MEASURED`. - Full dogfood: not owed (no `!` in the title, no narrowing arm). --- _Generated by [Claude Code](https://claude.ai/code/session_01WGufzhKYT8L13Bgj8icT3i)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e9e344a commit 95f818d

6 files changed

Lines changed: 50 additions & 10 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/core': patch
4+
---
5+
6+
fix(spec): `TranslationItemSchema.name` no longer tells authors to name a translation item after its locale tag
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
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.
12+
13+
`@objectstack/core`: the authored-translation sync's no-locale skip warning now tells authors to set `locale` to the item's BCP-47 tag (e.g. `zh-CN`), instead of suggesting they name the item after it.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -746,7 +746,7 @@ One locale of translations — the `translation` metadata type
746746
| **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 |
747747
| **settingsCommon** | `{ sourceLabels?: object }` | optional | Cross-namespace Settings UI strings |
748748
| **locale** | `string` | ✅ | BCP-47 locale this item translates (e.g. "zh-CN") |
749-
| **name** | `string` | optional | Item name — conventionally the locale code (`zh-CN`); the runtime sync falls back to it when `locale` is absent |
749+
| **name** | `string` | optional | Item name — snake_case like every metadata item (e.g. `zh_cn`); the BCP-47 tag goes in `locale` |
750750
| **label** | `string` | optional | Human-readable label shown in metadata lists |
751751
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
752752
| **_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/objectql/src/protocol-publish-canonical-fold.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,8 +265,8 @@ async function refusalOf(fn: () => Promise<unknown>): Promise<{ code?: string; s
265265

266266
/**
267267
* A `translation` body. `TranslationItemSchema` requires `locale`; `name` is
268-
* declared and optional, and is deliberately NOT snake_case-constrained for
269-
* this type (translation items are conventionally named after their locale).
268+
* declared and optional; like every metadata item it is named in snake_case
269+
* (`zh_cn`), with the BCP-47 tag in `locale`.
270270
*/
271271
const translationBody = (label: string) => ({ locale: 'zh-CN', name: 'zh_cn', label });
272272

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

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

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

0 commit comments

Comments
 (0)