Skip to content

Commit 47e03a7

Browse files
authored
Merge pull request #846 from objectstack-ai/copilot/add-object-first-internationalization
2 parents 00b1843 + e7aa1b4 commit 47e03a7

6 files changed

Lines changed: 767 additions & 3 deletions

File tree

ROADMAP.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ business/custom objects, aligning with industry best practices (e.g., ServiceNow
341341
- [x] **Data Protocol** — Object, Field (35+ types), Query, Filter, Validation, Hook, Datasource, Dataset, Analytics, Document, Storage Name Mapping (`tableName`/`columnName`), Feed & Activity Timeline (FeedItem, Comment, Mention, Reaction, FieldChange), Record Subscription (notification channels)
342342
- [x] **Driver Specifications** — Memory, PostgreSQL, MongoDB driver schemas + SQL/NoSQL abstractions
343343
- [x] **UI Protocol** — View (List/Form/Kanban/Calendar/Gantt), App, Dashboard, Report, Action, Page (16 types), Chart, Widget, Theme, Animation, DnD, Touch, Keyboard, Responsive, Offline, Notification, i18n, Content Elements, Enhanced Activity Timeline (`RecordActivityProps` unified timeline, `RecordChatterProps` sidebar/drawer), Unified Navigation Protocol (`NavigationItem` as single source of truth with 7 types: object/dashboard/page/url/report/action/group; `NavigationArea` for business domain partitioning; `order`/`badge`/`requiredPermissions` on all nav items), Airtable Interface Parity (`UserActionsConfig`, `AppearanceConfig`, `ViewTab`, `AddRecordConfig`, `InterfacePageConfig`, `showRecordCount`, `allowPrinting`)
344-
- [x] **System Protocol** — Manifest, Auth Config, Cache, Logging, Metrics, Tracing, Audit, Encryption, Masking, Migration, Tenant, Translation, Search Engine, HTTP Server, Worker, Job, Object Storage, Notification, Message Queue, Registry Config, Collaboration, Compliance, Change Management, Disaster Recovery, License, Security Context, Core Services, SystemObjectName/SystemFieldName Constants, StorageNameMapping Utilities
344+
- [x] **System Protocol** — Manifest, Auth Config, Cache, Logging, Metrics, Tracing, Audit, Encryption, Masking, Migration, Tenant, Translation (object-first `AppTranslationBundle` + diff/coverage detection), Search Engine, HTTP Server, Worker, Job, Object Storage, Notification, Message Queue, Registry Config, Collaboration, Compliance, Change Management, Disaster Recovery, License, Security Context, Core Services, SystemObjectName/SystemFieldName Constants, StorageNameMapping Utilities
345345
- [x] **Automation Protocol** — Flow (autolaunched/screen/schedule), Workflow, State Machine, Trigger Registry, Approval, ETL, Sync, Webhook, BPMN Semantics (parallel/join gateways, boundary events, wait events, default sequence flows), Node Executor Plugin Protocol (wait pause/resume, executor descriptors), BPMN XML Interop (import/export options, element mappings, diagnostics)
346346
- [x] **AI Protocol** — Agent, Agent Action, Conversation, Cost, MCP, Model Registry, NLQ, Orchestration, Predictive, RAG Pipeline, Runtime Ops, Feedback Loop, DevOps Agent, Plugin Development
347347
- [x] **API Protocol** — Protocol (104 schemas), Endpoint, Contract, Router, Dispatcher, REST Server, GraphQL, OData, WebSocket, Realtime, Batch, Versioning, HTTP Cache, Documentation, Discovery, Registry, Errors, Auth, Auth Endpoints, Metadata, Analytics, Query Adapter, Storage, Plugin REST API, Feed API (Feed CRUD, Reactions, Subscription), Automation API (CRUD + Toggle + Runs)
@@ -477,7 +477,7 @@ business/custom objects, aligning with industry best practices (e.g., ServiceNow
477477

478478
| Contract | Priority | Package | Notes |
479479
|:---|:---:|:---|:---|
480-
| `II18nService` | **P1** | `@objectstack/service-i18n` | Map-backed translation with locale resolution |
480+
| `II18nService` | **P1** | `@objectstack/service-i18n` | Map-backed translation with locale resolution; object-first bundle & diff detection |
481481
| `IRealtimeService` | **P1** | `@objectstack/service-realtime` | WebSocket/SSE push (replaces Studio setTimeout hack) |
482482
| `IFeedService` | **P1** | `@objectstack/service-feed` | ✅ Feed/Chatter with comments, reactions, subscriptions |
483483
| `ISearchService` | **P1** | `@objectstack/service-search` | In-memory search first, then Meilisearch driver |

content/docs/protocol/objectos/i18n-standard.mdx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,52 @@ Fallback to: en (system default) ✓
130130

131131
Translations are stored in **JSON files** organized by locale and namespace.
132132

133+
### Object-First Convention (Recommended)
134+
135+
ObjectOS uses an **object-first** convention where all translatable metadata
136+
for an object is aggregated under `o.{object_name}`. Global (non-object-bound)
137+
translations remain in dedicated top-level groups. This aligns with Salesforce DX
138+
and Dynamics conventions, enabling efficient translation workbench editing
139+
and automated coverage detection.
140+
141+
```typescript
142+
// AppTranslationBundle for a single locale (e.g. zh-CN)
143+
const zh: AppTranslationBundle = {
144+
// ── Object-first translations ─────────────────────────────────
145+
o: {
146+
account: {
147+
label: '客户',
148+
pluralLabel: '客户',
149+
description: '客户管理对象',
150+
fields: {
151+
name: { label: '客户名称', help: '公司法定名称' },
152+
industry: { label: '行业', options: { tech: '科技', finance: '金融' } },
153+
},
154+
_options: { status: { active: '活跃', inactive: '停用' } },
155+
_views: { all_accounts: { label: '全部客户' } },
156+
_sections: { basic_info: { label: '基本信息' } },
157+
_actions: { convert: { label: '转换', confirmMessage: '确认转换?' } },
158+
},
159+
},
160+
161+
// ── Global translations ───────────────────────────────────────
162+
_globalOptions: { currency: { usd: '美元', eur: '欧元' } },
163+
app: { crm: { label: '客户关系管理' } },
164+
nav: { home: '首页', settings: '设置' },
165+
dashboard: { sales_overview: { label: '销售概览' } },
166+
reports: { pipeline_report: { label: '管道报表' } },
167+
pages: { landing: { title: '欢迎' } },
168+
messages: { 'common.save': '保存' },
169+
validationMessages: { 'discount_limit': '折扣不能超过40%' },
170+
};
171+
```
172+
173+
**Key benefits:**
174+
- ✅ All translatable content for one object in one place
175+
- ✅ CLI can generate translation skeletons per object
176+
- ✅ Workbench can show per-object coverage and diffs
177+
- ✅ No redundant category/fieldOptions/reports nodes
178+
133179
### Directory Structure
134180

135181
```

packages/spec/src/contracts/i18n-service.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, it, expect } from 'vitest';
22
import type { II18nService } from './i18n-service';
3+
import type { AppTranslationBundle, TranslationCoverageResult } from '../system/translation.zod';
34

45
describe('I18n Service Contract', () => {
56
it('should allow a minimal II18nService implementation with required methods', () => {
@@ -85,4 +86,81 @@ describe('I18n Service Contract', () => {
8586
service.setDefaultLocale!('zh-CN');
8687
expect(service.getDefaultLocale!()).toBe('zh-CN');
8788
});
89+
90+
it('should allow implementation with getAppBundle and loadAppBundle', () => {
91+
const bundles = new Map<string, AppTranslationBundle>();
92+
93+
const service: II18nService = {
94+
t: () => '',
95+
getTranslations: () => ({}),
96+
loadTranslations: () => {},
97+
getLocales: () => Array.from(bundles.keys()),
98+
getAppBundle: (locale) => bundles.get(locale),
99+
loadAppBundle: (locale, bundle) => { bundles.set(locale, bundle); },
100+
};
101+
102+
const zhBundle: AppTranslationBundle = {
103+
o: {
104+
account: {
105+
label: '客户',
106+
fields: { name: { label: '客户名称' } },
107+
_views: { all_accounts: { label: '全部客户' } },
108+
},
109+
},
110+
messages: { 'common.save': '保存' },
111+
};
112+
113+
service.loadAppBundle!('zh-CN', zhBundle);
114+
const loaded = service.getAppBundle!('zh-CN');
115+
expect(loaded).toBeDefined();
116+
expect(loaded?.o?.account.label).toBe('客户');
117+
expect(loaded?.o?.account._views?.all_accounts.label).toBe('全部客户');
118+
expect(loaded?.messages?.['common.save']).toBe('保存');
119+
});
120+
121+
it('should allow implementation with getCoverage', () => {
122+
const service: II18nService = {
123+
t: () => '',
124+
getTranslations: () => ({}),
125+
loadTranslations: () => {},
126+
getLocales: () => ['en', 'zh-CN'],
127+
getCoverage: (locale, objectName?) => {
128+
const result: TranslationCoverageResult = {
129+
locale,
130+
objectName,
131+
totalKeys: 50,
132+
translatedKeys: 45,
133+
missingKeys: 5,
134+
redundantKeys: 0,
135+
staleKeys: 0,
136+
coveragePercent: 90,
137+
items: [
138+
{ key: 'o.account.fields.website.label', status: 'missing', objectName: 'account', locale },
139+
],
140+
};
141+
return result;
142+
},
143+
};
144+
145+
const coverage = service.getCoverage!('zh-CN', 'account');
146+
expect(coverage.locale).toBe('zh-CN');
147+
expect(coverage.objectName).toBe('account');
148+
expect(coverage.coveragePercent).toBe(90);
149+
expect(coverage.missingKeys).toBe(5);
150+
expect(coverage.items).toHaveLength(1);
151+
expect(coverage.items[0].status).toBe('missing');
152+
});
153+
154+
it('should keep backward compatibility — new methods are optional', () => {
155+
const minimalService: II18nService = {
156+
t: (_key, _locale) => '',
157+
getTranslations: (_locale) => ({}),
158+
loadTranslations: (_locale, _translations) => {},
159+
getLocales: () => [],
160+
};
161+
162+
expect(minimalService.getAppBundle).toBeUndefined();
163+
expect(minimalService.loadAppBundle).toBeUndefined();
164+
expect(minimalService.getCoverage).toBeUndefined();
165+
});
88166
});

packages/spec/src/contracts/i18n-service.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

3+
import type { AppTranslationBundle, TranslationCoverageResult } from '../system/translation.zod';
4+
35
/**
46
* II18nService - Internationalization Service Contract
57
*
@@ -16,7 +18,7 @@
1618
export interface II18nService {
1719
/**
1820
* Translate a message key for a given locale
19-
* @param key - Translation key (e.g. 'objects.account.label')
21+
* @param key - Translation key (e.g. 'o.account.label')
2022
* @param locale - BCP-47 locale code (e.g. 'en-US', 'zh-CN')
2123
* @param params - Optional interpolation parameters
2224
* @returns Translated string, or the key itself if not found
@@ -54,4 +56,37 @@ export interface II18nService {
5456
* @param locale - BCP-47 locale code
5557
*/
5658
setDefaultLocale?(locale: string): void;
59+
60+
// ── Object-first aggregation & diff detection ──────────────────────
61+
62+
/**
63+
* Get object-first translation bundle for a locale.
64+
*
65+
* Returns all translations aggregated under `o.{objectName}` with
66+
* global groups (app, nav, dashboard, etc.) at the top level.
67+
*
68+
* @param locale - BCP-47 locale code
69+
* @returns Object-first AppTranslationBundle, or undefined if no data
70+
*/
71+
getAppBundle?(locale: string): AppTranslationBundle | undefined;
72+
73+
/**
74+
* Load an object-first translation bundle for a locale.
75+
*
76+
* @param locale - BCP-47 locale code
77+
* @param bundle - Object-first AppTranslationBundle
78+
*/
79+
loadAppBundle?(locale: string, bundle: AppTranslationBundle): void;
80+
81+
/**
82+
* Get translation coverage for a locale, optionally scoped to a single object.
83+
*
84+
* Compares the supplied (or currently loaded) translation bundle against
85+
* the source metadata to detect missing, redundant, and stale entries.
86+
*
87+
* @param locale - BCP-47 locale code
88+
* @param objectName - Optional object name to scope the check
89+
* @returns Coverage result with per-key diff items
90+
*/
91+
getCoverage?(locale: string, objectName?: string): TranslationCoverageResult;
5792
}

0 commit comments

Comments
 (0)