objectstack-i18n

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Internationalization — ObjectStack I18n Protocol

国际化——ObjectStack I18n协议

Expert instructions for designing internationalization (i18n) and localization (l10n) strategies using the ObjectStack specification. This skill covers translation bundle structures, locale configuration, object-first translation patterns, coverage detection, and integration with the I18nService.

使用ObjectStack规范设计国际化(i18n)和本地化(l10n)策略的专业指南。本指南涵盖翻译包结构、区域配置、对象优先翻译模式、覆盖率检测以及与I18nService的集成。

When to Use This Skill

何时使用本技能

  • You are configuring i18n for a new ObjectStack project.
  • You need to create translation bundles for multiple locales.
  • You are designing object-first translation structures (per-object translation files).
  • You need to detect missing translations (
    os i18n check
    coverage analysis).
  • You are extending the service contract with AI translation suggestions (TMS / machine-translation integrations).
  • You are implementing locale-specific formatting (dates, numbers, currency). Related: workspace regional defaults (
    timezone
    ,
    locale
    ,
    currency
    ) live in the tenant-scoped
    localization
    settings, are resolved onto each request's
    ExecutionContext
    , and are exposed at
    GET /api/v1/auth/me/localization
    ; a currency field falls back to
    localization.currency
    when it omits its own (ADR-0053).
  • You need to understand translation file organization strategies (bundled, per_locale, per_namespace).

  • 你正在为新的ObjectStack项目配置i18n
  • 你需要为多区域创建翻译包
  • 你正在设计对象优先的翻译结构(每个对象对应一个翻译文件)。
  • 你需要检测缺失的翻译
    os i18n check
    覆盖率分析)。
  • 你正在通过AI翻译建议扩展服务协议(翻译管理系统TMS/机器翻译集成)。
  • 你正在实现特定区域的格式(日期、数字、货币)。 相关:工作区区域默认设置(
    timezone
    locale
    currency
    )存储在租户范围的
    localization
    配置中,会解析到每个请求的
    ExecutionContext
    ,并通过
    GET /api/v1/auth/me/localization
    暴露;当货币字段未指定自身值时,会回退到
    localization.currency
    (ADR-0053)。
  • 你需要了解翻译文件组织策略(捆绑式、按区域划分、按命名空间划分)。

Core Concepts

核心概念

Translation Architecture Overview

翻译架构概述

  1. Runtime format —
    objects.*
    (
    TranslationData
    )
    : each locale is authored as one
    TranslationData
    value. All translatable content for an object (label, fields, options, views, sections, actions) is grouped under
    objects.{object_name}
    , with global groups (
    apps
    ,
    messages
    ,
    globalActions
    ,
    dashboards
    ,
    settings
    ,
    metadataForms
    ) at the top level.
  2. Bundle registration: per-locale files are assembled with
    defineTranslationBundle({ en, 'zh-CN': … })
    into a
    TranslationBundle
    (locale code →
    TranslationData
    ) and registered via
    defineStack({ translations: [...] })
    . This is the format the runtime resolvers,
    os i18n extract
    ,
    os i18n check
    , and the example apps all use.
  3. Coverage detection:
    os i18n check
    compares registered bundles against source metadata to report missing keys per locale.
  4. Runtime authoring —
    TranslationItem
    : a
    translation
    metadata item authored in the Studio / metadata API carries the same
    objects.*
    groups plus the
    locale
    it translates. There is only one shape; see "Authoring at Runtime" below.

  1. 运行时格式——
    objects.*
    TranslationData
    :每个区域对应一个
    TranslationData
    值。对象的所有可翻译内容(标签、字段、选项、视图、章节、操作)都归类在
    objects.{object_name}
    下,全局分组(
    apps
    messages
    globalActions
    dashboards
    settings
    metadataForms
    )位于顶层。
  2. 包注册:按区域划分的文件通过
    defineTranslationBundle({ en, 'zh-CN': … })
    组装成
    TranslationBundle
    (区域代码→
    TranslationData
    ),并通过
    defineStack({ translations: [...] })
    注册。这是运行时解析器、
    os i18n extract
    os i18n check
    以及示例应用所使用的格式。
  3. 覆盖率检测
    os i18n check
    将已注册的包与源元数据进行比较,报告每个区域缺失的键。
  4. 运行时编写——
    TranslationItem
    :在Studio/元数据API中编写的
    translation
    元数据项包含与上述相同的
    objects.*
    分组,再加上它所对应的
    locale
    。格式唯一;详见下方“运行时编写”部分。

Translation Configuration

翻译配置

Stack-Level I18n Config

栈级I18n配置

Configure i18n settings in your
objectstack.config.ts
:
<!-- os:check -->
typescript
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)
});
PropertyTypeRequired / DefaultDescription
defaultLocale
string
requiredDefault BCP-47 locale code
supportedLocales
string[]
requiredAll supported locales
fallbackLocale
string
optionalFallback when translation missing
BCP-47 Locale Codes: Use standard locale tags (e.g.,
en-US
,
zh-CN
,
pt-BR
,
en-GB
).

objectstack.config.ts
中配置i18n设置:
<!-- os:check -->
typescript
import { defineStack } from '@objectstack/spec';

export default defineStack({
  i18n: {
    defaultLocale: 'en',
    supportedLocales: ['en', 'zh-CN', 'ja-JP', 'es-ES'],
    fallbackLocale: 'en',
  },
  // translations: [MyTranslations],  ← 在此处注册你的包(见下文)
});
属性类型必填/默认值描述
defaultLocale
string
必填默认BCP-47区域代码
supportedLocales
string[]
必填所有支持的区域
fallbackLocale
string
可选翻译缺失时的回退区域
BCP-47区域代码:使用标准区域标签(例如
en-US
zh-CN
pt-BR
en-GB
)。

File Organization Strategies

文件组织策略

1. Bundled (Single File)

1. 捆绑式(单文件)

All locales in one file. Best for small projects with few objects.
src/translations/
  crm.translation.ts        # { en: {...}, "zh-CN": {...} }
When to use: Fewer than 5 objects, 2-3 locales, < 200 translation keys total.
所有区域放在一个文件中。最适合对象较少的小型项目。
src/translations/
  crm.translation.ts        # { en: {...}, "zh-CN": {...} }
适用场景:少于5个对象,2-3个区域,翻译键总数少于200个。

2. Per-Locale (Recommended)

2. 按区域划分(推荐)

One file per locale containing all namespaces. Recommended when a single locale file stays under ~500 lines.
src/translations/
  en.ts                     # TranslationData for English
  zh-CN.ts                  # TranslationData for Chinese
  ja-JP.ts                  # TranslationData for Japanese
When to use: Medium projects (5-20 objects), 3-5 locales, organized by language.
每个区域对应一个包含所有命名空间的文件。当单个区域文件行数少于约500行时推荐使用。
src/translations/
  en.ts                     # 英文的TranslationData
  zh-CN.ts                  # 中文的TranslationData
  ja-JP.ts                  # 日文的TranslationData
适用场景:中型项目(5-20个对象),3-5个区域,按语言组织。

3. Per-Namespace (Enterprise)

3. 按命名空间划分(企业级)

One file per namespace (object) per locale. Aligns with Salesforce DX and ServiceNow conventions.
i18n/
  en/
    account.json            # ObjectTranslationData
    contact.json
    common.json             # messages + app labels
  zh-CN/
    account.json
    contact.json
    common.json
When to use: Large projects (20+ objects), 5+ locales, team collaboration, CI/CD pipelines.
These are authoring conventions: your import graph assembles whichever layout you choose into the
TranslationBundle
values you register on the stack.
FileI18nAdapter
's
localesDir
loads only flat top-level
{locale}.json
files (subdirectories are skipped) — a per-namespace tree must be assembled by your own imports or build step.

每个命名空间(对象)每个区域对应一个文件。与Salesforce DX和ServiceNow的约定一致。
i18n/
  en/
    account.json            # ObjectTranslationData
    contact.json
    common.json             # messages + 应用标签
  zh-CN/
    account.json
    contact.json
    common.json
适用场景:大型项目(20个以上对象),5个以上区域,团队协作,CI/CD流水线。
这些是编写约定:你的导入图会将你选择的任何布局组装成注册到栈中的
TranslationBundle
值。
FileI18nAdapter
localesDir
仅加载顶层的扁平
{locale}.json
文件(子目录会被跳过)——按命名空间划分的目录结构必须通过你自己的导入或构建步骤来组装。

Authoring Translation Bundles (
objects.*
)

编写翻译包(
objects.*

The canonical authoring path: one
TranslationData
per locale, assembled with
defineTranslationBundle
and registered on the stack. This mirrors the shipped example apps (
src/translations/{en,zh-CN}.ts
+
index.ts
):
<!-- os:check -->
typescript
// 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
validates the bundle at authoring time via
.parse()
— prefer it over a bare
: TranslationBundle
literal.

标准编写流程:每个区域对应一个
TranslationData
,通过
defineTranslationBundle
组装并注册到栈中。这与示例应用的实现方式一致(
src/translations/{en,zh-CN}.ts
+
index.ts
):
<!-- os:check -->
typescript
// src/translations/en.ts — 每个区域对应一个TranslationData
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 — 相同结构,翻译后的值
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 — 将多个区域组装成一个包…
export const TodoTranslations = defineTranslationBundle({
  en,
  'zh-CN': zhCN,
});

// objectstack.config.ts — …并注册到栈中
export default defineStack({
  i18n: { defaultLocale: 'en', supportedLocales: ['en', 'zh-CN'] },
  translations: [TodoTranslations],
});
defineTranslationBundle
会通过
.parse()
在编写时验证包——优先使用它而不是直接使用
: TranslationBundle
字面量。

Object-Level Translation Structure

对象级翻译结构

All translatable content for a single object is aggregated under
objects.{object_name}
with these sub-keys:
Sub-keyHolds
label
/
pluralLabel
/
description
Object-level text (
label
is required)
fields.{field_name}
label
,
help
,
placeholder
,
options
(option value → label) per field
_views.{view_name}
label
,
description
,
emptyState.title
/
emptyState.message
_actions.{action_name}
label
,
confirmText
,
successMessage
,
params.{param_name}
,
resultDialog
_sections.{section_name}
Form section / tab
label
,
description
Top-level groups alongside
objects
:
apps
(label, description, navigation),
messages
,
globalActions
(object-less actions),
dashboards
,
settings
,
metadataForms
,
settingsCommon
.
Validation messages are not a translation group.
validationMessages
was removed in spec 17.0.0 (#4667) — nothing ever read it, so a translated rule message was stored and never shown. Author the message on the rule itself (
object.validations[].message
), which the engine returns on every rejected write.
For the exact Zod shape (and any field that may have been added since), read
node_modules/@objectstack/spec/src/system/translation.zod.ts
TranslationDataSchema
,
ObjectTranslationDataSchema
, and
FieldTranslationSchema
.

单个对象的所有可翻译内容都聚合在
objects.{object_name}
下,包含以下子键:
子键包含内容
label
/
pluralLabel
/
description
对象级文本(
label
为必填项)
fields.{field_name}
每个字段的
label
help
placeholder
options
(选项值→标签)
_views.{view_name}
label
description
emptyState.title
/
emptyState.message
_actions.{action_name}
label
confirmText
successMessage
params.{param_name}
resultDialog
_sections.{section_name}
表单章节/标签页的
label
description
objects
同级的顶层分组:
apps
(标签、描述、导航)、
messages
globalActions
(无对象关联的操作)、
dashboards
settings
metadataForms
settingsCommon
验证消息不属于翻译分组
validationMessages
在规范17.0.0中被移除(#4667)——从未有组件读取它,因此翻译后的规则消息会被存储但不会显示。请直接在规则本身中编写消息(
object.validations[].message
),引擎会在每次写入被拒绝时返回该消息。
如需了解精确的Zod结构(以及可能新增的字段),请查看
node_modules/@objectstack/spec/src/system/translation.zod.ts
中的
TranslationDataSchema
ObjectTranslationDataSchema
FieldTranslationSchema

Naming Conventions

命名约定

ContextConventionExample
Locale codesBCP-47
en
,
en-US
,
zh-CN
,
pt-BR
Object keys in
objects.*
snake_case
objects.project_task
,
objects.support_case
Field keys
snake_case
fields.first_name
,
fields.due_date
Option valueslowercase
options.status.in_progress
Message keysdot-separated
common.save
,
validation.required
Critical: Object names and field keys in translation bundles must match the
snake_case
names defined in your Object and Field schemas.
Option sub-keys are the option's stored
value
, never its display label: for
options: [{ value: 'direct_mail', label: 'Direct Mail' }]
write
options: { direct_mail: '直邮' }
— both
'Direct Mail'
and
'direct-mail'
parse, ship, and resolve to nothing.
os validate
/
os lint
/
os compile
check this direction and report it as warnings (
translation-target-unknown
,
translation-option-key-unknown
): a key naming an object, field, view, action, param, section, app, nav item, dashboard or widget that does not exist is listed alongside the names that do. A bundle keyed to something since renamed still parses — the label just renders silently in its source locale while every neighbouring label resolves.

上下文约定示例
区域代码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
关键注意事项:翻译包中的对象名称和字段键必须与对象和字段 schema 中定义的
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
):如果某个键对应的对象、字段、视图、操作、参数、章节、应用、导航项、仪表板或小部件不存在,会列出该键及已存在的名称。如果某个包的键指向已重命名的对象,仍然会被解析——但对应的标签会以源区域语言静默显示,而相邻的标签都能正常解析。

Authoring at Runtime: the
translation
Item

运行时编写:
translation

Translations do not have to ship as files. A
translation
metadata item
— created in the Studio, through the metadata API, or by an agent — is one locale's worth of the same
objects.*
groups documented above, plus the
locale
it translates. There is exactly one shape; nothing converts between formats.
<!-- os:check -->
typescript
import { defineTranslation } from '@objectstack/spec/system';

export default defineTranslation({
  locale: 'zh-CN',
  objects: {
    account: {
      label: '客户',
      pluralLabel: '客户',
      fields: {
        name: { label: '客户名称', help: '公司或组织的法定名称' },
        industry: { label: '行业', options: { tech: '科技', finance: '金融' } },
        status: { options: { active: '活跃', inactive: '停用' } },
      },
      _views: { all_accounts: { label: '全部客户' } },
      _sections: { basic_info: { label: '基本信息' } },
      _actions: {
        merge: { label: '合并客户', confirmText: '此操作无法撤销,确认合并?' },
      },
    },
  },
  apps: { crm: { label: '客户关系管理', navigation: { home: { label: '首页' } } } },
  messages: { 'common.save': '保存' },
});
Rules that differ from a file bundle:
  • locale
    is required.
    A file bundle names its locales as map keys; an item carries its own. An item whose locale cannot be resolved is skipped by the runtime sync — a silent skip, which is why the field is mandatory rather than inferred from the item name.
  • One locale per item. Author
    zh-CN
    and
    ja-JP
    as two items.
  • Published items are loaded at boot and on every publish (no restart), and layer over the file bundles — an authored value wins over a shipped one for the same key; deleting the item restores the shipped value.
Exact Zod shape:
node_modules/@objectstack/spec/src/system/translation.zod.ts
TranslationItemSchema
.
翻译内容不一定必须以文件形式发布。
translation
元数据项
——在Studio中创建、通过元数据API创建或由Agent创建——包含上述
objects.*
分组的单个区域内容,再加上它所对应的
locale
。格式唯一;无需在不同格式间转换。
<!-- os:check -->
typescript
import { defineTranslation } from '@objectstack/spec/system';

export default defineTranslation({
  locale: 'zh-CN',
  objects: {
    account: {
      label: '客户',
      pluralLabel: '客户',
      fields: {
        name: { label: '客户名称', help: '公司或组织的法定名称' },
        industry: { label: '行业', options: { tech: '科技', finance: '金融' } },
        status: { options: { active: '活跃', inactive: '停用' } },
      },
      _views: { all_accounts: { label: '全部客户' } },
      _sections: { basic_info: { label: '基本信息' } },
      _actions: {
        merge: { label: '合并客户', confirmText: '此操作无法撤销,确认合并?' },
      },
    },
  },
  apps: { crm: { label: '客户关系管理', navigation: { home: { label: '首页' } } } },
  messages: { 'common.save': '保存' },
});
与文件包不同的规则:
  • locale
    为必填项
    。文件包通过映射键命名区域;而元数据项自身携带区域信息。如果区域无法解析,运行时同步会跳过该元数据项——静默跳过,因此该字段为必填项,而非从项名称推断。
  • 每个项对应一个区域
    zh-CN
    ja-JP
    需分别编写为两个项。
  • 已发布的项会在启动时和每次发布后加载(无需重启),并覆盖文件包——对于相同的键,编写的值会优先于打包的值;删除项会恢复打包的值。
精确的Zod结构:
node_modules/@objectstack/spec/src/system/translation.zod.ts
中的
TranslationItemSchema

Retired: the
o.*
dialect

已废弃:
o.*
方言

A second object-first shape keyed on
o.{object_name}
(with
app
,
nav
,
dashboard
,
reports
,
notifications
,
errors
,
_globalOptions
,
_meta
,
namespace
, and
_actions.confirmMessage
) was once documented for Studio-authored translations. No resolver ever read it, so items authored that way saved successfully and rendered nothing. It was removed in #3778 — those keys are now rejected at save time with a message naming the group to use instead. Never author them, in files or at runtime.

曾有一种以
o.{object_name}
为键的对象优先结构(包含
app
nav
dashboard
reports
notifications
errors
_globalOptions
_meta
namespace
_actions.confirmMessage
),用于Studio编写的翻译。从未有解析器读取过该结构,因此以该方式编写的项会成功保存但无法显示。该结构已在#3778中移除——现在保存时会拒绝这些键,并提示应使用的分组。请勿在文件或运行时编写该结构。

Message Interpolation

消息插值

Simple Format (Default)

简单格式(默认)

Both shipped adapters (
FileI18nAdapter
and the in-memory fallback) substitute double-brace
{{variable}}
placeholders only — single braces pass through unchanged. (The schema docstring mentions
{variable}
notation, but that is not what the runtime implements.)
json
{
  "messages": {
    "welcome": "Welcome, {{userName}}!",
    "pagination": "Showing {{start}} to {{end}} of {{total}} items"
  }
}
Usage:
typescript
i18n.t('messages.welcome', 'en', { userName: 'Alice' });
// "Welcome, Alice!"
两个已发布的适配器(
FileI18nAdapter
和内存回退适配器)仅替换双大括号
{{variable}}
占位符——单大括号会原样保留。(schema文档中提到了
{variable}
表示法,但运行时并未实现。)
json
{
  "messages": {
    "welcome": "Welcome, {{userName}}!",
    "pagination": "Showing {{start}} to {{end}} of {{total}} items"
  }
}
用法:
typescript
i18n.t('messages.welcome', 'en', { userName: 'Alice' });
// "Welcome, Alice!"

No ICU MessageFormat

不支持ICU MessageFormat

There is no ICU MessageFormat engine — interpolation is always simple
{{variable}}
substitution (the aspirational
messageFormat
config knob was removed in #3494). Author messages for simple substitution; ICU plural/select strings like
{count, plural, one {1 message} other {# messages}}
will not be evaluated. To pluralize, select the form in application code before calling
t()
.

没有ICU MessageFormat引擎——插值始终为简单的
{{variable}}
替换(预期的
messageFormat
配置项已在#3494中移除)。请编写适合简单替换的消息;ICU复数/选择字符串如
{count, plural, one {1 message} other {# messages}}
不会被解析。如需复数形式,请在调用
t()
前在应用代码中选择对应的形式。

Translation Coverage

翻译覆盖率

os i18n check

os i18n check

The working coverage path is the CLI:
bash
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
It compares registered bundles against source metadata and reports missing object/field/option/view/action keys per locale. Missing keys in the default locale are errors;
--strict
promotes non-default gaps to errors and
--show-keys
lists every missing key.
os lint --i18n-strict
folds the same gate into linting.
主要的覆盖率检查方式是CLI:
bash
os i18n check                          # 检查配置中的所有区域
os i18n check --locales=zh-CN          # 仅检查指定区域
os i18n check --strict --threshold=95  # CI校验:区域一致性 + 最低覆盖率要求
它会将已注册的包与源元数据进行比较,报告每个区域缺失的对象/字段/选项/视图/操作键。默认区域中缺失的键会被视为错误;
--strict
会将非默认区域的缺失提升为错误,
--show-keys
会列出所有缺失的键。
os lint --i18n-strict
会将相同的校验整合到代码检查中。

os i18n extract --check
— freshness, not coverage

os i18n extract --check
— 检查新鲜度,而非覆盖率

If you commit generated bundles (
*.generated.ts
produced by
os i18n extract
), coverage is only half the gate:
bash
os i18n extract <config> --locales=zh-CN,ja-JP --fill=default \
  --out=src/translations --check
--check
writes nothing. It re-renders what a real extract would produce and fails if that differs from what is committed in
--out
, naming each stale or missing file and printing the regenerate command.
Use both gates — they answer different questions.
os i18n check
asks are the strings translated? (coverage: human work).
extract --check
asks are the generated bundles still what the schema produces? (freshness: machine output). Renaming a label, adding an object, or removing a spec key leaves coverage at 100% while the bundles quietly go stale — which is exactly how the platform's own bundles ended up carrying translations for keys the schema had already deleted, plus fields with no entry in any locale.
It runs in the same merge mode as a normal extract, so it never asks for re-translation: an up-to-date bundle re-extracts byte-identically. Requires
--out
— there is nothing to compare against without it.
如果你提交了生成的包(由
os i18n extract
生成的
*.generated.ts
),覆盖率只是校验的一部分:
bash
os i18n extract <config> --locales=zh-CN,ja-JP --fill=default \
  --out=src/translations --check
--check
不会写入任何内容。它会重新生成真实提取会产生的内容,如果与
--out
中提交的内容不同,则会失败,并列出每个过期或缺失的文件,同时打印重新生成的命令。
同时使用两种校验——它们解决不同的问题
os i18n check
检查字符串是否已翻译?(覆盖率:人工工作)。
extract --check
检查生成的包是否仍与schema一致?(新鲜度:机器输出)。重命名标签、添加对象或移除规范键会使覆盖率保持100%,但包会悄悄过期——这正是平台自身的包最终包含schema已删除的键的翻译,以及某些字段在所有区域都没有条目的原因。
它以与正常提取相同的合并模式运行,因此不会要求重新翻译:最新的包重新提取后会与原文件完全一致。需要指定
--out
——没有输出目录则无法进行比较。

Diff & Coverage Schemas

差异与覆盖率Schema

The spec models coverage results for tooling:
TranslationCoverageResult
(totals,
coveragePercent
, per-group
breakdown
) and
TranslationDiffItem
key
(dot path),
status
(
missing | redundant | stale
),
locale
, optional
sourceHash
for stale detection, and AI-enrichment fields (
aiSuggested
,
aiConfidence
). Full Zod shape:
node_modules/@objectstack/spec/src/system/translation.zod.ts
TranslationCoverageResultSchema
,
TranslationDiffItemSchema
.
These schemas back the optional contract methods
getCoverage()
and
suggestTranslations()
, which no shipped adapter implements — point coverage workflows at
os i18n check
/
os lint --i18n-strict
instead.

规范为工具定义了覆盖率结果模型:
TranslationCoverageResult
(总计、
coveragePercent
、按分组的
breakdown
)和
TranslationDiffItem
——
key
(点路径)、
status
missing | redundant | stale
)、
locale
、用于过期检测的可选
sourceHash
,以及AI增强字段(
aiSuggested
aiConfidence
)。完整的Zod结构:
node_modules/@objectstack/spec/src/system/translation.zod.ts
中的
TranslationCoverageResultSchema
TranslationDiffItemSchema
这些schema支持可选的合约方法
getCoverage()
suggestTranslations()
,但没有已发布的适配器实现这些方法——请将覆盖率工作流指向
os i18n check
/
os lint --i18n-strict

AI-Powered Translation Suggestions

AI驱动的翻译建议

II18nService.suggestTranslations(locale, items)
is an optional contract method that enriches diff items with
aiSuggested
/
aiConfidence
. It is contract-only today: no shipped adapter implements it, and there is no CLI command for it. Implement it on a custom adapter to integrate:
  • Translation Management Systems (TMS) like Phrase, Crowdin, Lokalise
  • Machine translation APIs (Google Translate, DeepL)
  • Internal translation memory databases
Best Practice: Review and approve machine suggestions before committing them.

II18nService.suggestTranslations(locale, items)
是一个可选的合约方法,用于为差异项添加
aiSuggested
/
aiConfidence
字段。目前仅为合约定义:没有已发布的适配器实现它,也没有对应的CLI命令。在自定义适配器上实现它以集成:
  • 翻译管理系统(TMS)如Phrase、Crowdin、Lokalise
  • 机器翻译API(Google Translate、DeepL)
  • 内部翻译记忆数据库
最佳实践:在提交前审核并批准机器生成的建议。

Integration with II18nService

与II18nService集成

Service Contract

服务合约

II18nService
is the kernel service (name
'i18n'
) that loads bundles and resolves keys with fallback:
typescript
import type { II18nService } from '@objectstack/spec/contracts';
(The contract's source
.ts
is not part of the published package — only
src/**/*.zod.ts
ships — so import the type from
@objectstack/spec/contracts
rather than reading
node_modules
source.)
Methods implemented by both shipped adapters (
FileI18nAdapter
from
@objectstack/service-i18n
, and the in-memory fallback
@objectstack/core
registers when no i18n plugin is present):
  • t(key, locale, params?)
    — dot-path resolution (e.g.
    objects.account.label
    ) with
    {{param}}
    interpolation and fallback-locale lookup
  • getTranslations(locale)
    — full snapshot for a locale
  • loadTranslations(locale, data)
    — programmatic load; deep-merges, so multiple plugins can each contribute their own
    objects.*
    slice
  • getLocales()
    /
    getDefaultLocale()
    /
    setDefaultLocale()
The in-memory fallback additionally resolves locale codes (exact → case-insensitive → base language
zh-CN
zh
→ variant
zh
zh-CN
).
The contract also declares optional methods —
getCoverage
,
suggestTranslations
— that no shipped implementation provides. Treat them as extension points for a custom workbench or TMS adapter. (
getAppBundle
/
loadAppBundle
were removed in #3778 along with the
o.*
shape they returned.)
II18nService
是核心服务(名称为
'i18n'
),负责加载包并通过回退机制解析键:
typescript
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()
    /
    getDefaultLocale()
    /
    setDefaultLocale()
内存回退适配器还支持区域代码解析(精确匹配→不区分大小写匹配→基础语言
zh-CN
zh
→变体
zh
zh-CN
)。
合约还声明了可选方法——
getCoverage
suggestTranslations
——没有已发布的实现提供这些方法。将它们视为自定义工作台或TMS适配器的扩展点。(
getAppBundle
/
loadAppBundle
已随
o.*
结构一起在#3778中移除。)

Plugin Setup

插件设置

typescript
import { ObjectKernel } from '@objectstack/core';
import { I18nServicePlugin } from '@objectstack/service-i18n';

const kernel = new ObjectKernel();
kernel.use(new I18nServicePlugin({
  defaultLocale: 'en',
  localesDir: './i18n',
  fallbackLocale: 'en',
  registerRoutes: true,  // Auto-register REST endpoints
  basePath: '/api/v1/i18n',
}));

await kernel.bootstrap();

const i18n = kernel.getService<II18nService>('i18n');
localesDir
loads only flat, top-level
{locale}.json
files from the directory (subdirectories are skipped).
registerRoutes: true
(the default) self-registers
GET {basePath}/locales
,
/translations/:locale
, and
/labels/:object/:locale
once an HTTP server is available.

typescript
import { ObjectKernel } from '@objectstack/core';
import { I18nServicePlugin } from '@objectstack/service-i18n';

const kernel = new ObjectKernel();
kernel.use(new I18nServicePlugin({
  defaultLocale: 'en',
  localesDir: './i18n',
  fallbackLocale: 'en',
  registerRoutes: true,  // 自动注册REST端点
  basePath: '/api/v1/i18n',
}));

await kernel.bootstrap();

const i18n = kernel.getService<II18nService>('i18n');
localesDir
仅从目录中加载顶层的扁平
{locale}.json
文件(子目录会被跳过)。
registerRoutes: true
(默认值)会在HTTP服务器可用时自动注册
GET {basePath}/locales
/translations/:locale
/labels/:object/:locale
端点。

Translation Workflow Best Practices

翻译工作流最佳实践

1. Extract Skeletons from Metadata

1. 从元数据提取骨架

Scaffold ready-to-edit translation files from your stack config:
bash
os i18n extract --locales=zh-CN --out=./src/translations
This writes
<locale>.objects.generated.ts
TypeScript modules (not JSON) — the default locale is filled from schema labels, other locales follow
--fill
(
empty | default | todo
). Other flags:
--default-locale
,
--filter
(regex over object/app names or key paths),
--dry-run
,
--json
.
从栈配置中生成可编辑的翻译文件骨架:
bash
os i18n extract --locales=zh-CN --out=./src/translations
这会写入
<locale>.objects.generated.ts
TypeScript模块(而非JSON)——默认区域会填充schema中的标签,其他区域遵循
--fill
选项(
empty | default | todo
)。其他标志:
--default-locale
--filter
(对象/应用名称或键路径的正则表达式)、
--dry-run
--json

2. Translate

2. 翻译

Fill in the values manually. (AI suggestion is a contract-only concept —
suggestTranslations()
has no CLI and no shipped implementation.)
手动填充翻译值。(AI建议仅为合约概念——
suggestTranslations()
没有CLI也没有已发布的实现。)

3. Verify Coverage

3. 验证覆盖率

bash
os i18n check --locales=zh-CN
Add
--strict
/
--threshold=95
in CI to fail on locale gaps.
bash
os i18n check --locales=zh-CN
在CI中添加
--strict
/
--threshold=95
,当区域存在缺失时触发失败。

4. Commit & Register

4. 提交并注册

Commit the translation files, import them into your bundle, and register it via
defineStack({ translations: [...] })
.

提交翻译文件,将它们导入到包中,并通过
defineStack({ translations: [...] })
注册。

CRM I18n Blueprint

CRM I18n蓝图

Reference implementation shape:
  • Bundle entry:
    src/translations/index.ts
    (or
    crm.translation.ts
    )
  • Locale files:
    src/translations/{en,zh-CN,ja-JP,es-ES}.ts
Use this structure for metadata apps:
LayerCRM Pattern
Stack config
i18n
with an explicit locale list; per-locale source files by convention
Translation assemblyOne
defineTranslationBundle
call that imports per-locale files
Locale contentObject-scoped translations (
objects.account.fields.*
,
_views
,
_actions
) + global app/messages
Naming integrityTranslation object/field keys exactly match metadata machine names
For new locales, copy one locale file as a baseline, then run
os i18n check
before release.

参考实现结构:
  • 包入口:
    src/translations/index.ts
    (或
    crm.translation.ts
  • 区域文件:
    src/translations/{en,zh-CN,ja-JP,es-ES}.ts
元数据应用使用以下结构:
层级CRM模式
栈配置
i18n
包含明确的区域列表;按约定使用按区域划分的源文件
翻译组装一个
defineTranslationBundle
调用,导入按区域划分的文件
区域内容对象范围的翻译(
objects.account.fields.*
_views
_actions
) + 全局应用/消息
命名一致性翻译的对象/字段键与元数据的机器名称完全匹配
对于新区域,复制一个现有区域文件作为基线,然后在发布前运行
os i18n check

Common Pitfalls

常见陷阱

❌ The Retired
o.*
Shape

❌ 已废弃的
o.*
结构

Everything reads
objects.*
. The
o.*
dialect was removed in #3778 — it is not a "Studio format", not a secondary format, just gone. Files registered in that shape resolve to nothing; runtime items in that shape are rejected at save time.
typescript
// WRONG — in a file bundle AND in a `translation` item
{ o: { account: { label: '客户' } } }

// CORRECT (TranslationData)
{ objects: { account: { label: '客户' } } }
Same rule for its sibling keys:
app
apps
,
nav
apps.<app>.navigation.<id>.label
,
dashboard
dashboards
,
_globalOptions
objects.<obj>.fields.<field>.options
,
_meta.locale
→ top-level
locale
, and
_actions.confirmMessage
_actions.confirmText
.
所有组件都读取
objects.*
o.*
方言已在#3778中移除——它不是“Studio格式”,也不是次要格式,已完全删除。以该结构注册的文件无法解析;运行时以该结构编写的项会在保存时被拒绝。
typescript
// 错误——在文件包和`translation`项中均不允许
{ o: { account: { label: '客户' } } }

// 正确(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

❌ Mismatched Object Names

❌ 对象名称不匹配

Translation keys must match metadata exactly:
typescript
// Metadata
{ name: 'project_task' }

// Translation (WRONG)
{ objects: { projectTask: { label: '项目任务' } } }

// Translation (CORRECT)
{ objects: { project_task: { label: '项目任务' } } }
翻译键必须与元数据完全匹配:
typescript
// 元数据
{ name: 'project_task' }

// 翻译(错误)
{ objects: { projectTask: { label: '项目任务' } } }

// 翻译(正确)
{ objects: { project_task: { label: '项目任务' } } }

❌ Hardcoded Option Values

❌ 硬编码选项值

Always use lowercase machine values for options:
typescript
// Metadata
options: [
  { value: 'in_progress', label: 'In Progress' },
]

// Translation (WRONG)
options: { 'In Progress': '进行中' }

// Translation (CORRECT)
options: { in_progress: '进行中' }
始终使用小写的机器值作为选项键:
typescript
// 元数据
options: [
  { value: 'in_progress', label: 'In Progress' },
]

// 翻译(错误)
options: { 'In Progress': '进行中' }

// 翻译(正确)
options: { in_progress: '进行中' }

❌ Ignoring Coverage Reports

❌ 忽略覆盖率报告

Stale translations can cause confusion. Always run
os i18n check
before releases.

过期的翻译会导致混淆。发布前务必运行
os i18n check

Quick-Start Template

快速入门模板

One compact per-locale file — assemble locales with
defineTranslationBundle
and register via
defineStack({ translations: [...] })
as shown in "Authoring Translation Bundles" above:
<!-- os:check -->
typescript
// src/translations/zh-CN.ts
import type { TranslationData } from '@objectstack/spec/system';

export const zhCN: TranslationData = {
  objects: {
    account: {
      label: '客户',
      pluralLabel: '客户',
      fields: {
        name: { label: '客户名称' },
        email: { label: '邮箱', placeholder: '输入邮箱地址' },
        status: {
          label: '状态',
          options: {
            active: '活跃',
            inactive: '停用',
          },
        },
      },
      _views: {
        all_accounts: { label: '全部客户' },
      },
    },
  },

  apps: {
    crm: { label: '客户关系管理' },
  },

  messages: {
    'common.save': '保存',
    'common.cancel': '取消',
  },
};

一个简洁的按区域划分的文件——如“编写翻译包”部分所示,使用
defineTranslationBundle
组装区域,并通过
defineStack({ translations: [...] })
注册:
<!-- os:check -->
typescript
// src/translations/zh-CN.ts
import type { TranslationData } from '@objectstack/spec/system';

export const zhCN: TranslationData = {
  objects: {
    account: {
      label: '客户',
      pluralLabel: '客户',
      fields: {
        name: { label: '客户名称' },
        email: { label: '邮箱', placeholder: '输入邮箱地址' },
        status: {
          label: '状态',
          options: {
            active: '活跃',
            inactive: '停用',
          },
        },
      },
      _views: {
        all_accounts: { label: '全部客户' },
      },
    },
  },

  apps: {
    crm: { label: '客户关系管理' },
  },

  messages: {
    'common.save': '保存',
    'common.cancel': '取消',
  },
};

Verify your work

验证你的工作

After editing a
*.translation.ts
bundle:
bash
os i18n check   # translation coverage vs the default locale (missing-key report)
os validate     # the bundle conforms to the protocol schema (no artifact)
编辑
*.translation.ts
包后:
bash
os i18n check   # 翻译覆盖率与默认区域对比(缺失键报告)
os validate     # 包符合协议schema(无错误)

or: os build # the same schema gate, plus emits dist/

或:os build # 相同的schema校验,同时生成dist/


`os i18n check` lists keys missing per locale; `os lint --i18n-strict` turns
coverage gaps into hard errors. In a scaffolded project the schema gate is
`npm run validate`. See objectstack-platform → **Verify your work**.

---

`os i18n check`列出每个区域缺失的键;`os lint --i18n-strict`将覆盖率缺失转为严重错误。在脚手架项目中,schema校验的命令是`npm run validate`。详见objectstack-platform → **验证你的工作**。

---

References

参考资料

See references/_index.md for the full list of Zod schemas (with one-line descriptions) — pointers into
node_modules/@objectstack/spec/src/
. Always
Read
the source for exact field shapes; do not rely on memory of property names.
请查看references/_index.md获取完整的Zod schema列表(含单行描述)——指向
node_modules/@objectstack/spec/src/
中的文件。如需了解精确的字段结构,请始终查看源代码;不要依赖对属性名称的记忆。

See Also

另请参阅

  • objectstack-data — For understanding object and field metadata structure
  • objectstack-ui — For view, app, and action translations
  • objectstack-automation — For workflow and flow message translations
  • objectstack-data — 了解对象和字段元数据结构
  • objectstack-ui — 了解视图、应用和操作的翻译
  • objectstack-automation — 了解工作流和流程消息的翻译