关于 objectstack-i18n
用于编写和校验 ObjectStack 多语言翻译的 Skill,涵盖符合架构的翻译包、运行时加载、变量插值,以及通过命令行检查覆盖率和文件新鲜度。
主要功能
- 01配置支持语言与回退语言
- 02编写经架构校验的翻译包
- 03翻译对象及应用元数据
- 04运行时加载并合并翻译
- 05检查覆盖率与生成文件新鲜度
适用场景
- 01为 ObjectStack 堆栈设置语言
- 02翻译对象字段、视图和操作
- 03发布前查找缺失翻译
- 04在 CI 中校验生成的翻译包
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ objectstack-ai/ objectstack --skill objectstack-i18n安装后在 Claude Code 中输入/objectstack-i18n确认已加载。
附带文件references/_index.mdevals/bundles-locales-coverage.json
详细介绍
国际化 — ObjectStack I18n 协议
翻译配置
Stack 级 I18n 配置
在 objectstack.config.ts 中配置 i18n 设置:
import { defineStack } from '@objectstack/spec';
export default defineStack({
i18n: {
defaultLocale: 'en',
supportedLocales: ['en', 'zh-CN', 'ja-JP', 'es-ES'],
fallbackLocale: 'en',
},
// translations: [MyTranslations], ← register your bundles here (see below)
});
| 属性 | 类型 | 必填 / 默认值 | 描述 |
|---|---|---|---|
defaultLocale | string | 必填 | 默认 BCP-47 区域代码 |
supportedLocales | string[] | 必填 | 所有受支持的区域 |
fallbackLocale | string | 可选 | 缺少翻译时使用的回退区域 |
BCP-47 区域代码:使用标准区域标签(例如
en-US、zh-CN、pt-BR、en-GB)。
编写翻译包(objects.*)
规范的编写方式:每种区域对应一个 TranslationData,使用
defineTranslationBundle 组装并注册到 stack 上。这与已提供的
examples/app-todo(src/translations/{en,zh-CN,ja-JP}.ts + index.ts)相同:
// src/translations/en.ts — one TranslationData per locale
import { defineStack, defineTranslationBundle } from '@objectstack/spec';
import type { TranslationData } from '@objectstack/spec/system';
const en: TranslationData = {
objects: {
task: {
label: 'Task',
pluralLabel: 'Tasks',
fields: {
subject: { label: 'Subject', help: 'Brief title of the task' },
status: {
label: 'Status',
options: {
not_started: 'Not Started',
in_progress: 'In Progress',
completed: 'Completed',
},
},
due_date: { label: 'Due Date' },
},
_views: {
all_tasks: {
label: 'All Tasks',
emptyState: { title: 'No tasks yet', message: 'Create your first task' },
},
},
_sections: {
details: { label: 'Details' },
},
_actions: {
complete: {
label: 'Complete',
confirmText: 'Mark this task as completed?',
successMessage: 'Task completed',
},
},
},
},
apps: {
todo_app: { label: 'Todo Manager', description: 'Personal task management' },
},
messages: {
'common.save': 'Save',
'common.cancel': 'Cancel',
'welcome.user': 'Welcome, {{userName}}!',
},
};
// src/translations/zh-CN.ts — same shape, translated values
const zhCN: TranslationData = {
objects: {
task: {
label: '任务',
pluralLabel: '任务',
fields: {
subject: { label: '主题', help: '任务的简要标题' },
status: {
label: '状态',
options: { not_started: '未开始', in_progress: '进行中', completed: '已完成' },
},
due_date: { label: '截止日期' },
},
},
},
apps: {
todo_app: { label: '待办管理', description: '个人任务管理' },
},
messages: {
'common.save': '保存',
'common.cancel': '取消',
'welcome.user': '欢迎,{{userName}}!',
},
};
// src/translations/index.ts — assemble the locales into one bundle…
export const TodoTranslations = defineTranslationBundle({
en,
'zh-CN': zhCN,
});
// objectstack.config.ts — …and register it on the stack
export default defineStack({
i18n: { defaultLocale: 'en', supportedLocales: ['en', 'zh-CN'] },
translations: [TodoTranslations],
});
defineTranslationBundle 会在编写时通过 .parse() 验证翻译包 —
建议优先使用它,而不是直接编写 : TranslationBundle 字面量。
对象级翻译结构
单个对象的所有可翻译内容均汇总在
objects.{object_name} 下,其中包含以下子键:
| 子键 | 内容 |
|---|---|
label / pluralLabel / description | 对象级文本(所有键均为可选) |
fields.{field_name} | 每个字段的 label、help、placeholder、options(选项值 → 标签) |
_views.{view_name} | label、description、emptyState.title / emptyState.message、bulkActions.{def_name}(label、confirmText、confirmLabel、params) |
_actions.{action_name} | label、description、confirmText、successMessage、params.{param_name}(及其下的 .options.{value} — 该参数内联 options 中某个选项存储的 value)、outcomeMessages.{outcome}(操作自身 outcomeMessages 声明的结果)、resultDialog.title / .description / .acknowledge(仅适用于声明了 resultDialog 的操作)、resultDialog.fields.{path}(resultDialog.fields[] 中字段的字面 path,包括点号 — client.secret 是一个键);相同的键也适用于 globalActions.{action_name} |
_sections.{section_name} | 表单分区的 label、description |
_tabs.{tab_name} | 筛选预设标签页的 label(ViewTabSchema.name) |
_validations.{rule_name} | 仅 message — 被拒绝的写入操作返回的句子(取代已弃用的 validationMessages) |
与 objects 并列的顶层组:apps(标签、描述、导航)、
messages、globalActions(不含 objectName 的操作 — 若键名指向一个已绑定操作,则会
报错,错误消息会指出应改为填写的 objects.{owner}._actions.{action_name} 键)、
dashboards、datasets、pages、flows、metadataForms、settingsCommon。
settings 仅限平台使用 — 它以设置清单的命名空间为键,且只有平台会声明该命名空间,因此应用翻译包若包含此键,将被拒绝,并指出该键名。
关于准确的 Zod 结构(以及此后可能新增的任何字段),请查阅
node_modules/@objectstack/spec/src/system/translation.zod.ts —
TranslationDataSchema、ObjectTranslationDataSchema 和 FieldTranslationSchema。
命名约定
| 上下文 | 约定 | 示例 |
|---|---|---|
| 区域代码 | BCP-47 | en、en-US、zh-CN、pt-BR |
objects.* 中的对象键 | snake_case | objects.project_task、objects.support_case |
| 字段键 | snake_case | fields.first_name、fields.due_date |
| 选项值 | 小写 | options.status.in_progress |
| 消息键 | 以点分隔 | common.save、validation.required |
重要: 翻译包中的对象名称和字段键必须与对象和字段架构中定义的
snake_case名称一致。
选项子键必须是选项存储的 value,而不是其显示标签:
对于 options: [{ value: 'direct_mail', label: 'Direct Mail' }],应写成
options: { direct_mail: '直邮' }——'Direct Mail' 和 'direct-mail' 都
无法解析、发布或匹配到任何内容。
os validate / os lint / os compile 会检查这一方向:translation-target-unknown
是一个错误,会导致运行失败;translation-option-key-unknown 是警告。
为不存在的对象、字段、视图、选项卡、操作、参数、分区、验证规则、应用、
导航项、仪表板、微件或流程屏幕命名的键,会与实际存在的名称一同列出。在操作
条目下,还有三种情况属于错误:操作未在 outcomeMessages 中声明的结果;未声明的
resultDialog.fields 路径;以及操作未声明 resultDialog 时出现的
resultDialog title / description / acknowledge——翻译包只能翻译操作声明的
对话框,不能添加对话框。参数的内联选项键与字段的 options 共用选项警告;引用
field 且未声明内联 options 的参数不受检查。以已重命名内容为键的翻译包仍可解析——
该标签只会在其源区域设置下静默显示,而所有相邻标签都能正常解析。
文件组织策略
布局属于编写约定:无论选择哪种布局,你自己的导入图都会将其组装为注册到堆栈上的
TranslationBundle——不会有任何机制替你加载目录树。
| 布局 | 文件 | 适用场景 |
|---|---|---|
| 捆绑式 | 一个 crm.translation.ts,包含所有区域设置 | 少于 5 个对象、2-3 种区域设置 |
| 按区域设置(推荐) | src/translations/{en,zh-CN,ja-JP}.ts + index.ts | 5-20 个对象、3-5 种区域设置 |
| 按命名空间 | i18n/{locale}/{object}.json,由你自己的导入组装 | 20 个以上对象、5 种以上区域设置 |
运行时编写:translation 条目
翻译不一定要作为文件发布。translation 元数据条目可在 Studio 中、通过元数据 API
或由 Agent 创建,其中包含某一种区域设置的内容:即上文所述的同一套
objects.* 分组,以及它所翻译的 locale。locale 为必填项,每个条目对应一种区域设置;
已发布的条目会在启动时及每次发布时加载,并叠加在文件翻译包之上(删除该条目后,
随软件发布的值会恢复)。格式只有一种,不会进行格式转换。使用 defineTranslation
编写;确切的 Zod 结构定义位于
node_modules/@objectstack/spec/src/system/translation.zod.ts 中的
TranslationItemSchema。
消息插值
简单格式(默认)
随软件提供的两个适配器(FileI18nAdapter 和内存回退实现)只会替换
双花括号 {{variable}} 占位符——单花括号会原样保留。(架构文档字符串提到了
{variable} 表示法,但运行时并未如此实现。)
{
"messages": {
"welcome": "Welcome, {{userName}}!",
"pagination": "Showing {{start}} to {{end}} of {{total}} items"
}
}
用法:
i18n.t('messages.welcome', 'en', { userName: 'Alice' });
// "Welcome, Alice!"
不支持 ICU MessageFormat
这里没有 ICU MessageFormat 引擎——插值始终采用简单的
{{variable}} 替换方式(设想中的 messageFormat 配置项已被移除)。请按简单替换方式编写
消息;{count, plural, one {1 message} other {# messages}} 这类 ICU 复数/选择字符串
不会被计算。要实现复数形式,请在调用 t() 前于应用代码中选择相应形式。
翻译覆盖率
os i18n check
覆盖率检查的推荐路径是 CLI:
os i18n check # every locale found in the config
os i18n check --locales=zh-CN # scope to specific locales
os i18n check --strict --threshold=95 # CI gate: locale parity + minimum coverage
它会将已注册的资源包与源元数据进行比较,并报告提取器遍历的每种界面中各语言缺失的键——对象及其子键、全局操作、应用、仪表板、页面和流程屏幕。只有发布了相应翻译的技术栈才会计入平台元数据表单(--include-platform 会照常审计它们)。默认语言中的缺漏会报错;--strict 会将其余缺漏也提升为错误;--show-keys 会列出所有缺失键;os lint --i18n-strict 会一并执行此检查。
os i18n extract --check — 检查新鲜度,而非覆盖率
如果你提交了生成的资源包(由 os i18n extract 生成的 *.generated.ts),覆盖率只是门禁的一半:
os i18n extract <config> --locales=zh-CN,ja-JP --fill=default \
--out=src/translations --check
--check 不会写入任何内容。它会重新渲染实际提取时的结果,如果与 --out 中已提交的内容不同,就会失败,列出每个过期或缺失的文件,并打印重新生成命令。
两道门禁都要用——它们回答的是不同的问题。 os i18n check 检查的是字符串是否已翻译?(覆盖率:人工工作)。extract --check 检查的是生成的资源包是否仍与架构生成的内容一致?(新鲜度:机器输出)。重命名标签或删除规范键后,覆盖率仍可保持 100%,但资源包会过期——平台本身的资源包曾因此翻译了架构中已删除的键。
它采用与常规提取相同的合并模式运行,因此不会要求重新翻译:最新的资源包重新提取后,字节级内容完全一致。必须提供 --out——没有它就无法进行比较。
差异与覆盖率架构
规范为工具建模了覆盖率结果:TranslationCoverageResult(总计数、coveragePercent、按组划分的 breakdown)和 TranslationDiffItem——key(点分隔路径)、status(missing | redundant | stale)、locale、可选的 objectName、用于检测过期项的可选 sourceHash,以及 AI 增强字段(aiSuggested、aiConfidence)。完整的 Zod 结构:
node_modules/@objectstack/spec/src/system/translation.zod.ts —
TranslationCoverageResultSchema、TranslationDiffItemSchema。
这些架构为可选契约方法 getCoverage() 和 suggestTranslations() 提供支持,但已发布的适配器均未实现这些方法——覆盖率工作流应使用 os i18n check / os lint --i18n-strict。
与 II18nService 集成
服务契约
II18nService 是内核服务(名称为 'i18n'),负责加载资源包并通过回退机制解析键:
import type { II18nService } from '@objectstack/spec/contracts';
(契约的 .ts 源文件不属于已发布的软件包——软件包只包含 src/**/*.zod.ts——因此请从 @objectstack/spec/contracts 导入类型,而不要读取 node_modules 中的源文件。)
以下方法由两个已发布的适配器实现:来自 @objectstack/service-i18n 的 FileI18nAdapter,以及在未安装 i18n 插件时由 @objectstack/core 注册的内存回退实现:
t(key, locale, params?)— 点路径解析(例如objects.account.label),支持{{param}}插值和回退语言查找getTranslations(locale)— 获取某种语言的完整快照loadTranslations(locale, data)— 以编程方式加载;会进行深度合并,因此多个插件可以各自提供自己的objects.*部分getLocales()/setSupportedLocales()/getDefaultLocale()/setDefaultLocale()
内存回退实现还会解析语言区域代码(精确匹配 → 不区分大小写匹配 → 基础语言 zh-CN → zh → 变体 zh → zh-CN)。
契约还声明了可选方法 getFieldLabels、getCoverage 和 suggestTranslations,但已发布的实现均未提供这些方法。可将它们视为自定义工作台或 TMS 适配器的扩展点。(getAppBundle / loadAppBundle 已随其返回的 o.* 结构一并移除。)
注册
无需接入此插件:只要堆栈配置包含 translations 或 i18n,os serve 就会自行注册 I18nServicePlugin(内核引导流程参见 objectstack-platform → Runtime Boot Sequence)。它会自行注册 GET /api/v1/i18n/locales、/translations/:locale 和 /labels/:object/:locale。
翻译工作流最佳实践
1. 从元数据提取骨架
根据堆栈配置生成可直接编辑的翻译文件:
os i18n extract --locales=zh-CN --out=./src/translations
这会生成 <locale>.objects.generated.ts TypeScript 模块(不是 JSON),以及一个 <locale>.metadata-forms.generated.ts 配套文件,除非指定 --no-metadata-forms。默认语言环境会根据架构标签填充,其他语言环境则遵循 --fill(empty | default | todo)。其余选项请查看 os i18n extract --help:--filter、--default-locale、--no-merge、--objects-only、--source-hashes、--dry-run、--json,……
⚠️ 默认启用 --objects-only,因此 extract 只会写入 objects / globalActions 子树,而 os i18n check 还要求包含 app、navigation、dashboard、widget、page 和 flow 键。按原样执行此工作流时,覆盖率报告会指出 extract 从未生成的缺漏项。传入 --no-objects-only 以包含这些分组,或手动编写。
2. 翻译
手动填写值。
3. 验证覆盖率
os i18n check --locales=zh-CN
在 CI 中添加 --strict / --threshold=95,以便在语言环境存在缺漏时使检查失败。
4. 注册
堆栈通过 defineStack({ translations: [...] }) 注册其翻译包。自带生成包的包或插件——这是此代码库中最常见的形式——还需要额外一步:生成模块导出的是 objects 子树(NonNullable<TranslationData['objects']>),而不是 TranslationData,因此必须先将其包装成翻译包。
// src/translations/index.ts
import { defineTranslationBundle } from '@objectstack/spec';
import type { TranslationData } from '@objectstack/spec/system';
import { withSourceFallback } from '@objectstack/platform-objects/apps';
import { enObjects } from './en.objects.generated.js';
import { zhCNObjects } from './zh-CN.objects.generated.js';
import { zhCNGeneratedSourceHashes } from './zh-CN.source-hashes.generated.js';
const enSource: TranslationData = { objects: enObjects };
export const StorageTranslations = defineTranslationBundle({
en: enSource,
// 4th argument = the `--source-hashes` companion. Without it a leaf whose SOURCE has
// moved goes on serving the superseded fill under a green `os i18n check` forever.
// The 3rd stays undefined: it judges hand-authored `apps` / `dashboards` / `pages`,
// which a fully generated set does not have.
'zh-CN': withSourceFallback({ objects: zhCNObjects }, enSource, undefined, zhCNGeneratedSourceHashes),
});
拥有自身对象的插件不会在堆栈上注册翻译包,而是在自己的 kernel:ready 钩子中加载该包:i18n.loadTranslations(locale, data) 会进行深度合并,因此每个插件都能贡献自己的 objects.* 部分。
已发布示例
examples/app-crm — 捆绑布局(一个 crm.translation.ts,包含 en + zh-CN)。
examples/app-todo — 按语言环境划分的布局({en,zh-CN,ja-JP}.ts + index.ts)。
常见问题
❌ 已弃用的 o.* 结构
所有内容都读取 objects.*。o.* 方言已移除——它不是“Studio 格式”,也不是第二种格式,而是彻底废弃了。无论通过哪种入口,甚至文件都会拒绝它。
// WRONG — in a file bundle AND in a `translation` item
{ o: { account: { label: '客户' } } }
// CORRECT (TranslationData)
{ objects: { account: { label: '客户' } } }
其同级键也遵循相同规则:app → apps,nav → apps.<app>.navigation.<id>.label,dashboard → dashboards,_globalOptions → objects.<obj>.fields.<field>.options,_meta.locale → 顶层 locale,以及 _actions.confirmMessage → _actions.confirmText。
❌ 忽略覆盖率报告
发布前运行 os i18n check;只有 extract --check 会检查是否过时。
验证你的工作
os i18n check # coverage: which keys are missing, per locale
os validate # the bundle conforms to the protocol schema
os lint --i18n-strict 会将覆盖率缺漏作为严重错误处理。有关构建和脚手架检查,请参阅 objectstack-platform → Verify your work。
参考资料
完整的 Zod 架构列表(含单行说明)请参见 references/_index.md — 链接指向 node_modules/@objectstack/spec/src/。请始终 Read 源码以确认准确的字段结构;不要凭记忆推测属性名称。
另请参阅
- objectstack-data — 了解对象和字段的元数据结构
- objectstack-ui — 了解视图、应用和操作的翻译
- objectstack-automation — 了解工作流和流程消息的翻译