Skip to content

feat(cli): gate generated translation bundles with os i18n extract --check - #3683

Merged
os-zhuang merged 2 commits into
mainfrom
claude/org-creation-http-500-teams-gocypm
Jul 27, 2026
Merged

feat(cli): gate generated translation bundles with os i18n extract --check#3683
os-zhuang merged 2 commits into
mainfrom
claude/org-creation-http-500-teams-gocypm

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

#3670。那个 PR 清掉了翻译 bundle 里积压的漂移,但没有任何东西阻止它再次积压 —— 本 PR 补上这个 gate。

为什么需要

生成的翻译 bundle 此前没有新鲜度检查。落后于 schema 时不会有任何东西变红,所以只有当某个人恰好重跑一次 extractor 才会被发现。等到真有人跑的时候(#3670),已经同时攒了三类漂移:

类型 实例
schema 已移除、bundle 仍留着翻译 enable.trash / enable.mru(#2377)、agent visibility(#1901)—— schema 现在直接拒绝这些 key
schema 已新增、bundle 完全没有条目 summaryOperations.*、ADR-0105 D8 的 invitation-placement 字段、#3647 的 better-auth 1.7 列
提交成空字符串 sys_migration 在 ja-JP / es-ES 下的 label —— 渲染成空白,而不是回退到可读文本

这和 #3624 是同一个形状:产物和来源之间没有 gate,所以缺口只能靠偶然发现。#3647 用"推导出期望面 + 构建失败"关掉了 better-auth 列的那一个;本 PR 对翻译做同样的事。

实现

--check 渲染的内容与真实 extract 完全一致 —— 两个分支迭代的是同一份 rendered set,所以 check 不可能和 writer 产生分歧。它不写文件,而是与 --out 里已提交的内容比对,列出每个过期/缺失的文件,并打印出该跑的重新生成命令:

✗ out of date: packages/platform-objects/src/apps/translations/en.objects.generated.ts

✗ Translation bundles have drifted from the schema. Regenerate and commit:
  os i18n extract packages/... --locales=zh-CN,ja-JP,es-ES --fill=default --out=packages/...

merge 模式运行(和任何一次普通 extract 一样),所以它永远不会要求任何人重新翻译:一个最新的 bundle 重新 extract 出来是逐字节相同的。

flag 放在 CLI 而不是仓库脚本里 —— 任何 ship 生成 bundle 的消费者(cloud、各 app)都能用同样的方式加 gate。本仓库里是 pnpm check:i18n,接在 lint.yml 里其他 post-build consumer gate 旁边(它通过 extract config 读取已构建的 spec dist,所以必须在 build 之后)。

文档

os i18n check 此前在文档和 skill 里都是以"那个 CI gate"的身份出现的 —— 但它是覆盖率 gate,回答"字符串翻了没有"。对于提交生成 bundle 的人来说,另一半始终没有防护:改个 label、加个对象、删个 spec key,覆盖率仍然 100%,而 bundle 已经悄悄过期。

所以在两处都把它们作为一对来写(content/docs/ui/translations.mdxskills/objectstack-i18n/SKILL.md —— 后者会通过 npx skills add 分发给第三方),并在 AGENTS.md 的 Documentation Guardrails 里加了一行标明这些文件是生成的。

验证

场景 结果
当前 main(bundle 是新鲜的) 8 bundle(s) are in sync
删掉一个 schema 里存在的 key(sys_team.member_count) ✅ 报 out of date + exit 1
注入一个 schema 已不存在的死 key ✅ 报 out of date + exit 1
@objectstack/cli ✅ 637 passed (61 files)
check:skill-refs ✅ 9 generated files in sync

三类漂移的两个方向都能抓到。

一条已解决的说明。 本 PR 正文早先记录了 @objectstack/cli 有 16 个本地测试失败(data-model-rules.test.ts / lint-namespace-prefix.test.ts),并存疑是否与本 PR 有关。已排除:干净重建依赖后 637/637 全过,那 16 个失败纯粹是我本地 dist 过期的产物。当时我先确认了"带改动与不带改动的失败数完全相同(都是 16)",再做的重建复核 —— 记在这里以免留下悬念。

Refs #3670, #3624, #3647, #2377, #1901

…-check`

The generated translation bundles had no freshness gate. Nothing failed when
they fell behind the schema, so they only ever got fixed when someone happened
to re-run the extractor by hand — and by the time anyone did (#3670), three
distinct drifts had accumulated at once:

  - translations left behind for keys the schema had REMOVED (`enable.trash` /
    `enable.mru`, #2377; agent `visibility`, #1901) — keys the schema now
    rejects outright;
  - keys the schema had GAINED with no entry in any locale
    (`summaryOperations.*`, the ADR-0105 D8 invitation-placement fields, and
    the better-auth 1.7 columns from #3647);
  - `sys_migration` committed with EMPTY STRINGS for its labels in ja-JP and
    es-ES, which renders blank rather than falling back to anything readable.

This is the same shape as #3624: an artifact and its source with no gate
between them, so the gap is only found by accident. #3647 closed that one for
better-auth's columns by deriving the expected surface and failing the build;
this does the same for translations.

`--check` renders exactly what a real extract would write — both branches
iterate one shared rendered set, so the check cannot drift from the writer —
then compares against `--out` instead of writing, listing every stale or
missing file and printing the regenerate command. It runs in merge mode like
any other extract, so it never asks for re-translation: an up-to-date bundle
re-extracts byte-identically.

The flag lives on the CLI rather than in a repo script, so any consumer that
ships generated bundles can gate them the same way. Here it is `pnpm
check:i18n`, wired into lint.yml beside the other post-build consumer gates
(it reads the built spec dist through the extract config).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UYLC8TfjzHGwatNxZKdX7H
@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 27, 2026 2:18pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation ci/cd dependencies Pull requests that update a dependency file tooling size/m labels Jul 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/platform-objects.

19 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/skills-reference.mdx (via packages/cli)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli)
  • content/docs/automation/hook-bodies.mdx (via packages/cli)
  • content/docs/deployment/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli)
  • content/docs/plugins/packages.mdx (via @objectstack/cli, @objectstack/platform-objects)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • content/docs/releases/implementation-status.mdx (via @objectstack/cli)
  • content/docs/releases/v16.mdx (via @objectstack/cli)
  • content/docs/ui/setup-app.mdx (via @objectstack/platform-objects)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

The docs and the i18n skill both presented `os i18n check` as *the* CI gate.
It is the coverage gate — it answers 'are the strings translated?'. For anyone
committing generated bundles that leaves the other half unguarded: renaming a
label, adding an object, or removing a spec key keeps coverage at 100% while
the bundles quietly go stale.

Documents the two as a pair, in both surfaces, since the skill is what ships to
third parties via `npx skills add`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UYLC8TfjzHGwatNxZKdX7H
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/m tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants