| name | objectstack-i18n | ||||||||
|---|---|---|---|---|---|---|---|---|---|
| description | Author ObjectStack translation bundles — object/field labels, view text, app navigation strings, automation messages — and configure locale fallback, coverage reporting, and the per-locale source layout. Use when the user is adding `*.translation.ts` files, wiring a new locale, or resolving missing-translation warnings. Do not use for general i18n library questions unrelated to ObjectStack bundles. | ||||||||
| license | Apache-2.0 | ||||||||
| compatibility | Requires @objectstack/spec Zod schemas (v4+) | ||||||||
| metadata |
|
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.
- 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 or stale translations (coverage analysis).
- You are integrating AI-powered translation suggestions.
- You are implementing locale-specific formatting (dates, numbers, currency).
Workspace regional defaults — reference
timezone,locale, andcurrency— live in thelocalizationSETTINGS (tenant-scoped), are resolved onto every request'sExecutionContext, and are exposed to the client atGET /api/v1/auth/me/localization.localization.currencyis the fallback a currency field/measure uses when it omits its own (ADR-0053). - You need to understand translation file organization strategies (bundled, per_locale, per_namespace).
ObjectStack follows an object-first translation model inspired by Salesforce and Dynamics 365:
-
Object-First Aggregation: All translatable content for an object (labels, fields, options, views, actions) is grouped under a single namespace:
o.{object_name}. -
Global Groups: Non-object-bound translations (apps, navigation, messages) live at the top level.
-
Locale Files: Each locale has its own complete translation bundle (e.g.,
en.json,zh-CN.json). -
Coverage Detection: The system can compare translation bundles against source metadata to identify missing, redundant, or stale entries.
Configure i18n settings in your objectstack.config.ts:
import { defineStack } from '@objectstack/spec';
export default defineStack({
i18n: {
defaultLocale: 'en',
supportedLocales: ['en', 'zh-CN', 'ja-JP', 'es-ES'],
fallbackLocale: 'en',
fileOrganization: 'per_locale',
messageFormat: 'simple', // or 'icu' for complex plurals
lazyLoad: false,
cache: true,
},
});| Property | Type | Default | Description |
|---|---|---|---|
defaultLocale |
string |
'en' |
Default BCP-47 locale code |
supportedLocales |
string[] |
['en'] |
All supported locales |
fallbackLocale |
string |
same as defaultLocale |
Fallback when translation missing |
fileOrganization |
'bundled' | 'per_locale' | 'per_namespace' |
'per_locale' |
How translation files are organized |
messageFormat |
'simple' | 'icu' |
'simple' |
Interpolation format (ICU for plurals/gender) |
lazyLoad |
boolean |
false |
Load translations on demand |
cache |
boolean |
true |
Cache loaded translations in memory |
BCP-47 Locale Codes: Use standard locale tags (e.g.,
en-US,zh-CN,pt-BR,en-GB).
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.
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.
One file per namespace (object) per locale. Recommended for large projects with many objects/languages. Aligns with Salesforce DX and ServiceNow conventions.
i18n/
en/
account.json # ObjectTranslationData
contact.json
project_task.json
common.json # messages + app labels
zh-CN/
account.json
contact.json
project_task.json
common.json
When to use: Large projects (20+ objects), 5+ locales, team collaboration, CI/CD pipelines.
The AppTranslationBundle is the canonical format for a single locale:
const zh: AppTranslationBundle = {
_meta: {
locale: 'zh-CN',
direction: 'ltr',
},
// Object-first translations
o: {
account: {
label: '客户',
pluralLabel: '客户',
description: '客户管理对象',
fields: {
name: { label: '客户名称', help: '公司或组织的法定名称' },
industry: {
label: '行业',
options: { tech: '科技', finance: '金融', retail: '零售' }
},
website: { label: '网站', placeholder: '输入网站地址' },
},
_options: {
status: { active: '活跃', inactive: '停用' },
},
_views: {
all_accounts: { label: '全部客户' },
my_accounts: { label: '我的客户' },
},
_sections: {
basic_info: { label: '基本信息' },
contact_info: { label: '联系方式' },
},
_actions: {
convert_lead: { label: '转换线索', confirmMessage: '确认转换为客户?' },
merge: { label: '合并客户', confirmMessage: '此操作无法撤销,确认合并?' },
},
},
},
// Global picklist options (not object-specific)
_globalOptions: {
currency: { usd: '美元', eur: '欧元', cny: '人民币' },
},
// App-level translations
app: {
crm: { label: '客户关系管理', description: '管理销售流程' },
helpdesk: { label: '服务台', description: '客户支持系统' },
},
// Navigation menu
nav: {
home: '首页',
settings: '设置',
reports: '报表',
admin: '管理',
},
// Dashboard translations
dashboard: {
sales_overview: { label: '销售概览', description: '销售漏斗与目标' },
},
// Report translations
reports: {
pipeline_report: { label: '管道报表' },
},
// Page translations
pages: {
landing: { title: '欢迎', description: '开始使用 ObjectStack' },
},
// UI messages (supports ICU MessageFormat if enabled)
messages: {
'common.save': '保存',
'common.cancel': '取消',
'common.delete': '删除',
'common.confirm': '确认',
'validation.required': '此字段为必填项',
'pagination.showing': '显示 {start} 到 {end},共 {total} 条',
},
// Validation error messages
validationMessages: {
discount_limit: '折扣不能超过40%',
end_date_after_start: '结束日期必须晚于开始日期',
},
// Global notifications
notifications: {
record_created: { title: '创建成功', body: '记录已创建' },
},
// Global error messages
errors: {
'ERR_NETWORK': '网络连接失败',
'ERR_PERMISSION': '权限不足',
},
};All translatable content for a single object is aggregated under
o.{object_name} with these sub-keys:
| Sub-key | Holds |
|---|---|
label / pluralLabel / description / helpText |
Object-level text |
fields.{field_name} |
label, help, placeholder, options per field |
_options.{picklist_name} |
Object-scoped picklist option labels |
_views.{view_name} |
View label / description |
_sections.{section_name} |
Form section / tab labels |
_actions.{action_name} |
Action label + confirmMessage |
_notifications.{key} / _errors.{code} |
Per-object messages |
For the exact Zod shape (and any field that may have been added since), read
node_modules/@objectstack/spec/src/system/translation.zod.ts —
ObjectTranslationNodeSchema and FieldTranslationSchema.
| Context | Convention | Example |
|---|---|---|
| Locale codes | BCP-47 | en, en-US, zh-CN, pt-BR |
Object keys in o.* |
snake_case |
o.project_task, o.support_case |
| Field keys | snake_case |
fields.first_name, fields.due_date |
| Option values | lowercase | options.status.in_progress |
| Message keys | dot-separated | common.save, validation.required |
Critical: Object names and field keys in translation bundles must match the
snake_casenames defined in your Object and Field schemas.
The II18nService.getCoverage() method compares a translation bundle against source metadata to detect:
- Missing — Keys that exist in metadata but not in the translation bundle
- Redundant — Keys in the bundle that have no matching metadata
- Stale — Keys where the source metadata has changed since translation
const coverage = i18nService.getCoverage('zh-CN', 'account');
console.log(coverage);
// {
// locale: 'zh-CN',
// objectName: 'account',
// totalKeys: 120,
// translatedKeys: 105,
// missingKeys: 12,
// redundantKeys: 3,
// staleKeys: 0,
// coveragePercent: 87.5,
// items: [
// { key: 'o.account.fields.website.label', status: 'missing', locale: 'zh-CN' },
// ...
// ],
// breakdown: [
// { group: 'fields', totalKeys: 45, translatedKeys: 40, coveragePercent: 88.9 },
// { group: 'views', totalKeys: 8, translatedKeys: 8, coveragePercent: 100 },
// ],
// }Each item in coverage.items carries key (dot path), status
(missing | redundant | stale), objectName, locale, optional
sourceHash for stale detection, and AI-enrichment fields (aiSuggested,
aiConfidence) when suggestTranslations() has been run. Full Zod shape:
node_modules/@objectstack/spec/src/system/translation.zod.ts —
TranslationDiffItemSchema.
The II18nService.suggestTranslations() method enriches diff items with AI-generated translations:
const missingItems = coverage.items.filter(item => item.status === 'missing');
const suggestions = await i18nService.suggestTranslations('zh-CN', missingItems);
suggestions.forEach(item => {
console.log(`${item.key}: ${item.aiSuggested} (confidence: ${item.aiConfidence})`);
// o.account.fields.website.label: 网站 (confidence: 0.95)
});Best Practice: AI suggestions work best when:
- You provide source locale context (e.g., English labels)
- You include domain-specific glossaries
- You review and approve suggestions before committing
Use {variable} placeholders:
{
"messages": {
"welcome": "Welcome, {userName}!",
"pagination": "Showing {start} to {end} of {total} items"
}
}Usage:
i18n.t('messages.welcome', 'en', { userName: 'Alice' });
// "Welcome, Alice!"For complex pluralization, gender, and select:
// Enable in stack config
i18n: { messageFormat: 'icu' }{
"messages": {
"inbox": "{count, plural, =0 {No messages} one {1 message} other {# messages}}",
"gender": "{gender, select, male {He} female {She} other {They}} replied"
}
}When to use ICU:
- Languages with complex plural rules (Arabic, Slavic languages)
- Gender-aware translations
- Ordinal numbers (1st, 2nd, 3rd)
- Date/time/number formatting
II18nService is the kernel service that loads bundles, resolves keys with
fallback chains, reports coverage, and (optionally) generates AI
suggestions. Methods:
t(key, locale, params?)— resolve a single key with interpolationgetTranslations(locale)— full snapshot for a localeloadTranslations(locale, bundle)— programmatic loadgetLocales()/getDefaultLocale()/setDefaultLocale()getAppBundle(locale)/loadAppBundle(locale, bundle)— object-first formatgetCoverage(locale, objectName?)— diff vs metadatasuggestTranslations(locale, items)— AI-fill missing keys
Full TypeScript signature lives in
node_modules/@objectstack/spec/src/contracts/i18n-service.ts — read it
when you need exact parameter shapes.
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');Use the CLI or API to extract all translatable keys from your metadata:
objectstack i18n extract --locale zh-CN --output i18n/zh-CN.jsonThis generates a skeleton bundle with all required keys.
Fill in the translations manually, or use AI suggestions:
objectstack i18n suggest --locale zh-CN --input i18n/zh-CN.jsonRun coverage analysis to detect missing or stale translations:
objectstack i18n coverage --locale zh-CNCommit translation files to version control. ObjectStack automatically loads them at runtime.
Reference implementation shape:
- Bundle entry:
src/translations/index.ts(orcrm.translation.ts) - Locale files:
src/translations/{en,zh-CN,ja-JP,es-ES}.ts
Use this structure for metadata apps:
| Layer | CRM Pattern |
|---|---|
| Stack config | i18n.fileOrganization = 'per_locale' with explicit locale list |
| Translation assembly | One TranslationBundle that imports per-locale files |
| Locale content | Object-scoped translations (objects.account.fields.*, _views, _actions) + global app/nav/messages |
| Naming integrity | Translation object/field keys exactly match metadata machine names |
For new locales, copy one locale file as a baseline, then run coverage before release.
When multiple plugins contribute translations, use namespaces to avoid collisions:
const crmBundle: AppTranslationBundle = {
namespace: 'crm',
o: {
account: { label: '客户' },
},
};
const helpdeskBundle: AppTranslationBundle = {
namespace: 'helpdesk',
o: {
ticket: { label: '工单' },
},
};Keys are prefixed: crm.o.account.label, helpdesk.o.ticket.label.
const ar: AppTranslationBundle = {
_meta: {
locale: 'ar',
direction: 'rtl',
},
o: {
account: { label: 'حساب' },
},
};UI frameworks can use _meta.direction to apply RTL CSS.
Implement custom II18nService.suggestTranslations() to integrate with:
- Translation Management Systems (TMS) like Phrase, Crowdin, Lokalise
- Machine translation APIs (Google Translate, DeepL)
- Internal translation memory databases
Translation keys must match metadata exactly:
// Metadata
{ name: 'project_task' }
// Translation (WRONG)
{ o: { projectTask: { label: '项目任务' } } }
// Translation (CORRECT)
{ o: { project_task: { label: '项目任务' } } }Always use lowercase machine values for options:
// Metadata
options: [
{ value: 'in_progress', label: 'In Progress' },
]
// Translation (WRONG)
options: { 'In Progress': '进行中' }
// Translation (CORRECT)
options: { in_progress: '进行中' }Stale translations can cause confusion. Always run coverage analysis before releases.
// i18n/zh-CN.ts
import type { AppTranslationBundle } from '@objectstack/spec';
export default {
_meta: {
locale: 'zh-CN',
direction: 'ltr',
},
o: {
account: {
label: '客户',
pluralLabel: '客户',
fields: {
name: { label: '客户名称' },
email: { label: '邮箱', placeholder: '输入邮箱地址' },
status: {
label: '状态',
options: {
active: '活跃',
inactive: '停用',
},
},
},
_views: {
all_accounts: { label: '全部客户' },
},
},
},
app: {
crm: { label: '客户关系管理' },
},
nav: {
home: '首页',
settings: '设置',
},
messages: {
'common.save': '保存',
'common.cancel': '取消',
},
} satisfies AppTranslationBundle;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.
- 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