Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ business/custom objects, aligning with industry best practices (e.g., ServiceNow
- [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)
- [x] **Driver Specifications** — Memory, PostgreSQL, MongoDB driver schemas + SQL/NoSQL abstractions
- [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`)
- [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
- [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
- [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)
- [x] **AI Protocol** — Agent, Agent Action, Conversation, Cost, MCP, Model Registry, NLQ, Orchestration, Predictive, RAG Pipeline, Runtime Ops, Feedback Loop, DevOps Agent, Plugin Development
- [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)
Expand Down Expand Up @@ -477,7 +477,7 @@ business/custom objects, aligning with industry best practices (e.g., ServiceNow

| Contract | Priority | Package | Notes |
|:---|:---:|:---|:---|
| `II18nService` | **P1** | `@objectstack/service-i18n` | Map-backed translation with locale resolution |
| `II18nService` | **P1** | `@objectstack/service-i18n` | Map-backed translation with locale resolution; object-first bundle & diff detection |
| `IRealtimeService` | **P1** | `@objectstack/service-realtime` | WebSocket/SSE push (replaces Studio setTimeout hack) |
| `IFeedService` | **P1** | `@objectstack/service-feed` | ✅ Feed/Chatter with comments, reactions, subscriptions |
| `ISearchService` | **P1** | `@objectstack/service-search` | In-memory search first, then Meilisearch driver |
Expand Down
46 changes: 46 additions & 0 deletions content/docs/protocol/objectos/i18n-standard.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,52 @@ Fallback to: en (system default) ✓

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

### Object-First Convention (Recommended)

ObjectOS uses an **object-first** convention where all translatable metadata
for an object is aggregated under `o.{object_name}`. Global (non-object-bound)
translations remain in dedicated top-level groups. This aligns with Salesforce DX
and Dynamics conventions, enabling efficient translation workbench editing
and automated coverage detection.

```typescript
// AppTranslationBundle for a single locale (e.g. zh-CN)
const zh: AppTranslationBundle = {
// ── Object-first translations ─────────────────────────────────
o: {
account: {
label: '客户',
pluralLabel: '客户',
description: '客户管理对象',
fields: {
name: { label: '客户名称', help: '公司法定名称' },
industry: { label: '行业', options: { tech: '科技', finance: '金融' } },
},
_options: { status: { active: '活跃', inactive: '停用' } },
_views: { all_accounts: { label: '全部客户' } },
_sections: { basic_info: { label: '基本信息' } },
_actions: { convert: { label: '转换', confirmMessage: '确认转换?' } },
},
},

// ── Global translations ───────────────────────────────────────
_globalOptions: { currency: { usd: '美元', eur: '欧元' } },
app: { crm: { label: '客户关系管理' } },
nav: { home: '首页', settings: '设置' },
dashboard: { sales_overview: { label: '销售概览' } },
reports: { pipeline_report: { label: '管道报表' } },
pages: { landing: { title: '欢迎' } },
messages: { 'common.save': '保存' },
validationMessages: { 'discount_limit': '折扣不能超过40%' },
};
```

**Key benefits:**
- ✅ All translatable content for one object in one place
- ✅ CLI can generate translation skeletons per object
- ✅ Workbench can show per-object coverage and diffs
- ✅ No redundant category/fieldOptions/reports nodes

### Directory Structure

```
Expand Down
78 changes: 78 additions & 0 deletions packages/spec/src/contracts/i18n-service.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, it, expect } from 'vitest';
import type { II18nService } from './i18n-service';
import type { AppTranslationBundle, TranslationCoverageResult } from '../system/translation.zod';

describe('I18n Service Contract', () => {
it('should allow a minimal II18nService implementation with required methods', () => {
Expand Down Expand Up @@ -85,4 +86,81 @@ describe('I18n Service Contract', () => {
service.setDefaultLocale!('zh-CN');
expect(service.getDefaultLocale!()).toBe('zh-CN');
});

it('should allow implementation with getAppBundle and loadAppBundle', () => {
const bundles = new Map<string, AppTranslationBundle>();

const service: II18nService = {
t: () => '',
getTranslations: () => ({}),
loadTranslations: () => {},
getLocales: () => Array.from(bundles.keys()),
getAppBundle: (locale) => bundles.get(locale),
loadAppBundle: (locale, bundle) => { bundles.set(locale, bundle); },
};

const zhBundle: AppTranslationBundle = {
o: {
account: {
label: '客户',
fields: { name: { label: '客户名称' } },
_views: { all_accounts: { label: '全部客户' } },
},
},
messages: { 'common.save': '保存' },
};

service.loadAppBundle!('zh-CN', zhBundle);
const loaded = service.getAppBundle!('zh-CN');
expect(loaded).toBeDefined();
expect(loaded?.o?.account.label).toBe('客户');
expect(loaded?.o?.account._views?.all_accounts.label).toBe('全部客户');
expect(loaded?.messages?.['common.save']).toBe('保存');
});

it('should allow implementation with getCoverage', () => {
const service: II18nService = {
t: () => '',
getTranslations: () => ({}),
loadTranslations: () => {},
getLocales: () => ['en', 'zh-CN'],
getCoverage: (locale, objectName?) => {
const result: TranslationCoverageResult = {
locale,
objectName,
totalKeys: 50,
translatedKeys: 45,
missingKeys: 5,
redundantKeys: 0,
staleKeys: 0,
coveragePercent: 90,
items: [
{ key: 'o.account.fields.website.label', status: 'missing', objectName: 'account', locale },
],
};
return result;
},
};

const coverage = service.getCoverage!('zh-CN', 'account');
expect(coverage.locale).toBe('zh-CN');
expect(coverage.objectName).toBe('account');
expect(coverage.coveragePercent).toBe(90);
expect(coverage.missingKeys).toBe(5);
expect(coverage.items).toHaveLength(1);
expect(coverage.items[0].status).toBe('missing');
});

it('should keep backward compatibility — new methods are optional', () => {
const minimalService: II18nService = {
t: (_key, _locale) => '',
getTranslations: (_locale) => ({}),
loadTranslations: (_locale, _translations) => {},
getLocales: () => [],
};

expect(minimalService.getAppBundle).toBeUndefined();
expect(minimalService.loadAppBundle).toBeUndefined();
expect(minimalService.getCoverage).toBeUndefined();
});
});
37 changes: 36 additions & 1 deletion packages/spec/src/contracts/i18n-service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

import type { AppTranslationBundle, TranslationCoverageResult } from '../system/translation.zod';

/**
* II18nService - Internationalization Service Contract
*
Expand All @@ -16,7 +18,7 @@
export interface II18nService {
/**
* Translate a message key for a given locale
* @param key - Translation key (e.g. 'objects.account.label')
* @param key - Translation key (e.g. 'o.account.label')
* @param locale - BCP-47 locale code (e.g. 'en-US', 'zh-CN')
* @param params - Optional interpolation parameters
* @returns Translated string, or the key itself if not found
Expand Down Expand Up @@ -54,4 +56,37 @@ export interface II18nService {
* @param locale - BCP-47 locale code
*/
setDefaultLocale?(locale: string): void;

// ── Object-first aggregation & diff detection ──────────────────────

/**
* Get object-first translation bundle for a locale.
*
* Returns all translations aggregated under `o.{objectName}` with
* global groups (app, nav, dashboard, etc.) at the top level.
*
* @param locale - BCP-47 locale code
* @returns Object-first AppTranslationBundle, or undefined if no data
*/
getAppBundle?(locale: string): AppTranslationBundle | undefined;

/**
* Load an object-first translation bundle for a locale.
*
* @param locale - BCP-47 locale code
* @param bundle - Object-first AppTranslationBundle
*/
loadAppBundle?(locale: string, bundle: AppTranslationBundle): void;

/**
* Get translation coverage for a locale, optionally scoped to a single object.
*
* Compares the supplied (or currently loaded) translation bundle against
* the source metadata to detect missing, redundant, and stale entries.
Comment on lines +84 to +85

Copilot AI Mar 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getCoverage doc says it compares a “supplied (or currently loaded) translation bundle” against metadata, but the method signature only accepts (locale, objectName?) and provides no way to pass a bundle. Please either adjust the JSDoc to match the API, or change the signature to accept an explicit bundle/source so implementations are unambiguous.

Suggested change
* Compares the supplied (or currently loaded) translation bundle against
* the source metadata to detect missing, redundant, and stale entries.
* Compares the currently loaded translations (or object-first app bundle) for
* the locale against the source metadata to detect missing, redundant, and
* stale entries.

Copilot uses AI. Check for mistakes.
*
* @param locale - BCP-47 locale code
* @param objectName - Optional object name to scope the check
* @returns Coverage result with per-key diff items
*/
getCoverage?(locale: string, objectName?: string): TranslationCoverageResult;
}
Loading