From 764d8f1725afeb667ee920b917f81cbd0724edde Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 10:51:17 +0000 Subject: [PATCH 1/3] Initial plan From e3943b580584f0a8cbc0c28dae046919e07648fc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 10:58:43 +0000 Subject: [PATCH 2/3] feat(spec): add i18n file organization convention schemas and per-locale example - Add ObjectTranslationDataSchema for per-object translation file validation - Add FieldTranslationSchema for reusable field translation structure - Add TranslationFileOrganizationSchema enum (bundled, per_locale, per_namespace) - Add TranslationConfigSchema for i18n stack configuration - Add TranslationData type export - Add i18n optional property to ObjectStackDefinitionSchema - Refactor app-todo to per-locale file splitting convention - Add 18 new tests for all new schemas Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- examples/app-todo/objectstack.config.ts | 8 + examples/app-todo/src/translations/en.ts | 97 ++++++ examples/app-todo/src/translations/ja-JP.ts | 96 ++++++ .../src/translations/todo.translation.ts | 284 +----------------- examples/app-todo/src/translations/zh-CN.ts | 96 ++++++ packages/spec/src/stack.zod.ts | 3 +- packages/spec/src/system/translation.test.ts | 188 ++++++++++++ packages/spec/src/system/translation.zod.ts | 170 ++++++++++- 8 files changed, 655 insertions(+), 287 deletions(-) create mode 100644 examples/app-todo/src/translations/en.ts create mode 100644 examples/app-todo/src/translations/ja-JP.ts create mode 100644 examples/app-todo/src/translations/zh-CN.ts diff --git a/examples/app-todo/objectstack.config.ts b/examples/app-todo/objectstack.config.ts index 8bd3044385..f64a4c9f70 100644 --- a/examples/app-todo/objectstack.config.ts +++ b/examples/app-todo/objectstack.config.ts @@ -48,6 +48,14 @@ export default defineStack({ flows: Object.values(flows) as any, apps: Object.values(apps), + // I18n Configuration — per-locale file organization + i18n: { + defaultLocale: 'en', + supportedLocales: ['en', 'zh-CN', 'ja-JP'], + fallbackLocale: 'en', + fileOrganization: 'per_locale', + }, + // I18n Translation Bundles (en, zh-CN, ja-JP) translations: Object.values(translations), }); diff --git a/examples/app-todo/src/translations/en.ts b/examples/app-todo/src/translations/en.ts new file mode 100644 index 0000000000..eab5484d11 --- /dev/null +++ b/examples/app-todo/src/translations/en.ts @@ -0,0 +1,97 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import type { TranslationData } from '@objectstack/spec/system'; + +/** + * English (en) — Todo App Translations + * + * Per-locale file: one file per language, following the `per_locale` convention. + * Each file exports a single `TranslationData` object for its locale. + */ +export const en: TranslationData = { + objects: { + task: { + label: 'Task', + pluralLabel: 'Tasks', + fields: { + subject: { label: 'Subject', help: 'Brief title of the task' }, + description: { label: 'Description' }, + status: { + label: 'Status', + options: { + not_started: 'Not Started', + in_progress: 'In Progress', + waiting: 'Waiting', + completed: 'Completed', + deferred: 'Deferred', + }, + }, + priority: { + label: 'Priority', + options: { + low: 'Low', + normal: 'Normal', + high: 'High', + urgent: 'Urgent', + }, + }, + category: { label: 'Category' }, + due_date: { label: 'Due Date' }, + reminder_date: { label: 'Reminder Date/Time' }, + completed_date: { label: 'Completed Date' }, + owner: { label: 'Assigned To' }, + tags: { + label: 'Tags', + options: { + important: 'Important', + quick_win: 'Quick Win', + blocked: 'Blocked', + follow_up: 'Follow Up', + review: 'Review', + }, + }, + is_recurring: { label: 'Recurring Task' }, + recurrence_type: { label: 'Recurrence Type' }, + recurrence_interval: { label: 'Recurrence Interval' }, + is_completed: { label: 'Is Completed' }, + is_overdue: { label: 'Is Overdue' }, + progress_percent: { label: 'Progress (%)' }, + estimated_hours: { label: 'Estimated Hours' }, + actual_hours: { label: 'Actual Hours' }, + notes: { label: 'Notes' }, + category_color: { label: 'Category Color' }, + }, + }, + }, + apps: { + todo_app: { + label: 'Todo Manager', + description: 'Personal task management application', + }, + }, + messages: { + 'common.save': 'Save', + 'common.cancel': 'Cancel', + 'common.delete': 'Delete', + 'common.edit': 'Edit', + 'common.create': 'Create', + 'common.search': 'Search', + 'common.filter': 'Filter', + 'common.sort': 'Sort', + 'common.refresh': 'Refresh', + 'common.export': 'Export', + 'common.back': 'Back', + 'common.confirm': 'Confirm', + 'success.saved': 'Successfully saved', + 'success.deleted': 'Successfully deleted', + 'success.completed': 'Task marked as completed', + 'confirm.delete': 'Are you sure you want to delete this task?', + 'confirm.complete': 'Mark this task as completed?', + 'error.required': 'This field is required', + 'error.load_failed': 'Failed to load data', + }, + validationMessages: { + completed_date_required: 'Completed date is required when status is Completed', + recurrence_fields_required: 'Recurrence type is required for recurring tasks', + }, +}; diff --git a/examples/app-todo/src/translations/ja-JP.ts b/examples/app-todo/src/translations/ja-JP.ts new file mode 100644 index 0000000000..40c540fda8 --- /dev/null +++ b/examples/app-todo/src/translations/ja-JP.ts @@ -0,0 +1,96 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import type { TranslationData } from '@objectstack/spec/system'; + +/** + * 日本語 (ja-JP) — Todo App Translations + * + * Per-locale file: one file per language, following the `per_locale` convention. + */ +export const jaJP: TranslationData = { + objects: { + task: { + label: 'タスク', + pluralLabel: 'タスク', + fields: { + subject: { label: '件名', help: 'タスクの簡単なタイトル' }, + description: { label: '説明' }, + status: { + label: 'ステータス', + options: { + not_started: '未着手', + in_progress: '進行中', + waiting: '待機中', + completed: '完了', + deferred: '延期', + }, + }, + priority: { + label: '優先度', + options: { + low: '低', + normal: '通常', + high: '高', + urgent: '緊急', + }, + }, + category: { label: 'カテゴリ' }, + due_date: { label: '期日' }, + reminder_date: { label: 'リマインダー日時' }, + completed_date: { label: '完了日' }, + owner: { label: '担当者' }, + tags: { + label: 'タグ', + options: { + important: '重要', + quick_win: 'クイックウィン', + blocked: 'ブロック中', + follow_up: 'フォローアップ', + review: 'レビュー', + }, + }, + is_recurring: { label: '繰り返しタスク' }, + recurrence_type: { label: '繰り返しタイプ' }, + recurrence_interval: { label: '繰り返し間隔' }, + is_completed: { label: '完了済み' }, + is_overdue: { label: '期限超過' }, + progress_percent: { label: '進捗率 (%)' }, + estimated_hours: { label: '見積時間' }, + actual_hours: { label: '実績時間' }, + notes: { label: 'メモ' }, + category_color: { label: 'カテゴリ色' }, + }, + }, + }, + apps: { + todo_app: { + label: 'ToDo マネージャー', + description: '個人タスク管理アプリケーション', + }, + }, + messages: { + 'common.save': '保存', + 'common.cancel': 'キャンセル', + 'common.delete': '削除', + 'common.edit': '編集', + 'common.create': '新規作成', + 'common.search': '検索', + 'common.filter': 'フィルター', + 'common.sort': '並べ替え', + 'common.refresh': '更新', + 'common.export': 'エクスポート', + 'common.back': '戻る', + 'common.confirm': '確認', + 'success.saved': '保存しました', + 'success.deleted': '削除しました', + 'success.completed': 'タスクを完了にしました', + 'confirm.delete': 'このタスクを削除してもよろしいですか?', + 'confirm.complete': 'このタスクを完了にしますか?', + 'error.required': 'この項目は必須です', + 'error.load_failed': 'データの読み込みに失敗しました', + }, + validationMessages: { + completed_date_required: 'ステータスが「完了」の場合、完了日は必須です', + recurrence_fields_required: '繰り返しタスクには繰り返しタイプが必要です', + }, +}; diff --git a/examples/app-todo/src/translations/todo.translation.ts b/examples/app-todo/src/translations/todo.translation.ts index 95b488f3a0..8bc885ced2 100644 --- a/examples/app-todo/src/translations/todo.translation.ts +++ b/examples/app-todo/src/translations/todo.translation.ts @@ -1,284 +1,24 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import type { TranslationBundle } from '@objectstack/spec/system'; +import { en } from './en'; +import { zhCN } from './zh-CN'; +import { jaJP } from './ja-JP'; /** * Todo App — Internationalization (i18n) * - * Demonstrates multi-language translations covering: - * - Object & field labels (singular / plural) - * - Select-field option labels - * - App navigation labels - * - UI messages (common, success, confirm, error) - * - Validation error messages + * Demonstrates **per-locale file splitting** convention: + * each language is defined in its own file (`en.ts`, `zh-CN.ts`, `ja-JP.ts`) + * and assembled into a single `TranslationBundle` here. + * + * For large projects with many objects, use `per_namespace` organization + * to further split each locale into per-object files (see i18n-standard docs). * * Supported locales: en (English), zh-CN (Chinese), ja-JP (Japanese) */ export const TodoTranslations: TranslationBundle = { - // ─── English (base) ─────────────────────────────────────────────── - en: { - objects: { - task: { - label: 'Task', - pluralLabel: 'Tasks', - fields: { - subject: { label: 'Subject', help: 'Brief title of the task' }, - description: { label: 'Description' }, - status: { - label: 'Status', - options: { - not_started: 'Not Started', - in_progress: 'In Progress', - waiting: 'Waiting', - completed: 'Completed', - deferred: 'Deferred', - }, - }, - priority: { - label: 'Priority', - options: { - low: 'Low', - normal: 'Normal', - high: 'High', - urgent: 'Urgent', - }, - }, - category: { label: 'Category' }, - due_date: { label: 'Due Date' }, - reminder_date: { label: 'Reminder Date/Time' }, - completed_date: { label: 'Completed Date' }, - owner: { label: 'Assigned To' }, - tags: { - label: 'Tags', - options: { - important: 'Important', - quick_win: 'Quick Win', - blocked: 'Blocked', - follow_up: 'Follow Up', - review: 'Review', - }, - }, - is_recurring: { label: 'Recurring Task' }, - recurrence_type: { label: 'Recurrence Type' }, - recurrence_interval: { label: 'Recurrence Interval' }, - is_completed: { label: 'Is Completed' }, - is_overdue: { label: 'Is Overdue' }, - progress_percent: { label: 'Progress (%)' }, - estimated_hours: { label: 'Estimated Hours' }, - actual_hours: { label: 'Actual Hours' }, - notes: { label: 'Notes' }, - category_color: { label: 'Category Color' }, - }, - }, - }, - apps: { - todo_app: { - label: 'Todo Manager', - description: 'Personal task management application', - }, - }, - messages: { - 'common.save': 'Save', - 'common.cancel': 'Cancel', - 'common.delete': 'Delete', - 'common.edit': 'Edit', - 'common.create': 'Create', - 'common.search': 'Search', - 'common.filter': 'Filter', - 'common.sort': 'Sort', - 'common.refresh': 'Refresh', - 'common.export': 'Export', - 'common.back': 'Back', - 'common.confirm': 'Confirm', - 'success.saved': 'Successfully saved', - 'success.deleted': 'Successfully deleted', - 'success.completed': 'Task marked as completed', - 'confirm.delete': 'Are you sure you want to delete this task?', - 'confirm.complete': 'Mark this task as completed?', - 'error.required': 'This field is required', - 'error.load_failed': 'Failed to load data', - }, - validationMessages: { - completed_date_required: 'Completed date is required when status is Completed', - recurrence_fields_required: 'Recurrence type is required for recurring tasks', - }, - }, - - // ─── Chinese Simplified ─────────────────────────────────────────── - 'zh-CN': { - objects: { - task: { - label: '任务', - pluralLabel: '任务', - fields: { - subject: { label: '主题', help: '任务的简要标题' }, - description: { label: '描述' }, - status: { - label: '状态', - options: { - not_started: '未开始', - in_progress: '进行中', - waiting: '等待中', - completed: '已完成', - deferred: '已推迟', - }, - }, - priority: { - label: '优先级', - options: { - low: '低', - normal: '普通', - high: '高', - urgent: '紧急', - }, - }, - category: { label: '分类' }, - due_date: { label: '截止日期' }, - reminder_date: { label: '提醒日期/时间' }, - completed_date: { label: '完成日期' }, - owner: { label: '负责人' }, - tags: { - label: '标签', - options: { - important: '重要', - quick_win: '速胜', - blocked: '受阻', - follow_up: '待跟进', - review: '待审核', - }, - }, - is_recurring: { label: '周期性任务' }, - recurrence_type: { label: '重复类型' }, - recurrence_interval: { label: '重复间隔' }, - is_completed: { label: '是否完成' }, - is_overdue: { label: '是否逾期' }, - progress_percent: { label: '进度 (%)' }, - estimated_hours: { label: '预估工时' }, - actual_hours: { label: '实际工时' }, - notes: { label: '备注' }, - category_color: { label: '分类颜色' }, - }, - }, - }, - apps: { - todo_app: { - label: '待办管理', - description: '个人任务管理应用', - }, - }, - messages: { - 'common.save': '保存', - 'common.cancel': '取消', - 'common.delete': '删除', - 'common.edit': '编辑', - 'common.create': '新建', - 'common.search': '搜索', - 'common.filter': '筛选', - 'common.sort': '排序', - 'common.refresh': '刷新', - 'common.export': '导出', - 'common.back': '返回', - 'common.confirm': '确认', - 'success.saved': '保存成功', - 'success.deleted': '删除成功', - 'success.completed': '任务已标记为完成', - 'confirm.delete': '确定要删除此任务吗?', - 'confirm.complete': '确定将此任务标记为完成?', - 'error.required': '此字段为必填项', - 'error.load_failed': '数据加载失败', - }, - validationMessages: { - completed_date_required: '状态为"已完成"时,完成日期为必填项', - recurrence_fields_required: '周期性任务必须指定重复类型', - }, - }, - - // ─── Japanese ───────────────────────────────────────────────────── - 'ja-JP': { - objects: { - task: { - label: 'タスク', - pluralLabel: 'タスク', - fields: { - subject: { label: '件名', help: 'タスクの簡単なタイトル' }, - description: { label: '説明' }, - status: { - label: 'ステータス', - options: { - not_started: '未着手', - in_progress: '進行中', - waiting: '待機中', - completed: '完了', - deferred: '延期', - }, - }, - priority: { - label: '優先度', - options: { - low: '低', - normal: '通常', - high: '高', - urgent: '緊急', - }, - }, - category: { label: 'カテゴリ' }, - due_date: { label: '期日' }, - reminder_date: { label: 'リマインダー日時' }, - completed_date: { label: '完了日' }, - owner: { label: '担当者' }, - tags: { - label: 'タグ', - options: { - important: '重要', - quick_win: 'クイックウィン', - blocked: 'ブロック中', - follow_up: 'フォローアップ', - review: 'レビュー', - }, - }, - is_recurring: { label: '繰り返しタスク' }, - recurrence_type: { label: '繰り返しタイプ' }, - recurrence_interval: { label: '繰り返し間隔' }, - is_completed: { label: '完了済み' }, - is_overdue: { label: '期限超過' }, - progress_percent: { label: '進捗率 (%)' }, - estimated_hours: { label: '見積時間' }, - actual_hours: { label: '実績時間' }, - notes: { label: 'メモ' }, - category_color: { label: 'カテゴリ色' }, - }, - }, - }, - apps: { - todo_app: { - label: 'ToDo マネージャー', - description: '個人タスク管理アプリケーション', - }, - }, - messages: { - 'common.save': '保存', - 'common.cancel': 'キャンセル', - 'common.delete': '削除', - 'common.edit': '編集', - 'common.create': '新規作成', - 'common.search': '検索', - 'common.filter': 'フィルター', - 'common.sort': '並べ替え', - 'common.refresh': '更新', - 'common.export': 'エクスポート', - 'common.back': '戻る', - 'common.confirm': '確認', - 'success.saved': '保存しました', - 'success.deleted': '削除しました', - 'success.completed': 'タスクを完了にしました', - 'confirm.delete': 'このタスクを削除してもよろしいですか?', - 'confirm.complete': 'このタスクを完了にしますか?', - 'error.required': 'この項目は必須です', - 'error.load_failed': 'データの読み込みに失敗しました', - }, - validationMessages: { - completed_date_required: 'ステータスが「完了」の場合、完了日は必須です', - recurrence_fields_required: '繰り返しタスクには繰り返しタイプが必要です', - }, - }, + en, + 'zh-CN': zhCN, + 'ja-JP': jaJP, }; diff --git a/examples/app-todo/src/translations/zh-CN.ts b/examples/app-todo/src/translations/zh-CN.ts new file mode 100644 index 0000000000..859bbc382a --- /dev/null +++ b/examples/app-todo/src/translations/zh-CN.ts @@ -0,0 +1,96 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import type { TranslationData } from '@objectstack/spec/system'; + +/** + * 简体中文 (zh-CN) — Todo App Translations + * + * Per-locale file: one file per language, following the `per_locale` convention. + */ +export const zhCN: TranslationData = { + objects: { + task: { + label: '任务', + pluralLabel: '任务', + fields: { + subject: { label: '主题', help: '任务的简要标题' }, + description: { label: '描述' }, + status: { + label: '状态', + options: { + not_started: '未开始', + in_progress: '进行中', + waiting: '等待中', + completed: '已完成', + deferred: '已推迟', + }, + }, + priority: { + label: '优先级', + options: { + low: '低', + normal: '普通', + high: '高', + urgent: '紧急', + }, + }, + category: { label: '分类' }, + due_date: { label: '截止日期' }, + reminder_date: { label: '提醒日期/时间' }, + completed_date: { label: '完成日期' }, + owner: { label: '负责人' }, + tags: { + label: '标签', + options: { + important: '重要', + quick_win: '速胜', + blocked: '受阻', + follow_up: '待跟进', + review: '待审核', + }, + }, + is_recurring: { label: '周期性任务' }, + recurrence_type: { label: '重复类型' }, + recurrence_interval: { label: '重复间隔' }, + is_completed: { label: '是否完成' }, + is_overdue: { label: '是否逾期' }, + progress_percent: { label: '进度 (%)' }, + estimated_hours: { label: '预估工时' }, + actual_hours: { label: '实际工时' }, + notes: { label: '备注' }, + category_color: { label: '分类颜色' }, + }, + }, + }, + apps: { + todo_app: { + label: '待办管理', + description: '个人任务管理应用', + }, + }, + messages: { + 'common.save': '保存', + 'common.cancel': '取消', + 'common.delete': '删除', + 'common.edit': '编辑', + 'common.create': '新建', + 'common.search': '搜索', + 'common.filter': '筛选', + 'common.sort': '排序', + 'common.refresh': '刷新', + 'common.export': '导出', + 'common.back': '返回', + 'common.confirm': '确认', + 'success.saved': '保存成功', + 'success.deleted': '删除成功', + 'success.completed': '任务已标记为完成', + 'confirm.delete': '确定要删除此任务吗?', + 'confirm.complete': '确定将此任务标记为完成?', + 'error.required': '此字段为必填项', + 'error.load_failed': '数据加载失败', + }, + validationMessages: { + completed_date_required: '状态为"已完成"时,完成日期为必填项', + recurrence_fields_required: '周期性任务必须指定重复类型', + }, +}; diff --git a/packages/spec/src/stack.zod.ts b/packages/spec/src/stack.zod.ts index 547be74309..3027456b38 100644 --- a/packages/spec/src/stack.zod.ts +++ b/packages/spec/src/stack.zod.ts @@ -4,7 +4,7 @@ import { z } from 'zod'; import { ManifestSchema } from './kernel/manifest.zod'; import { DatasourceSchema } from './data/datasource.zod'; -import { TranslationBundleSchema } from './system/translation.zod'; +import { TranslationBundleSchema, TranslationConfigSchema } from './system/translation.zod'; import { objectStackErrorMap, formatZodError } from './shared/error-map.zod'; // Data Protocol @@ -75,6 +75,7 @@ export const ObjectStackDefinitionSchema = z.object({ manifest: ManifestSchema.describe('Project Package Configuration'), datasources: z.array(DatasourceSchema).optional().describe('External Data Connections'), translations: z.array(TranslationBundleSchema).optional().describe('I18n Translation Bundles'), + i18n: TranslationConfigSchema.optional().describe('Internationalization configuration'), /** * ObjectQL: Data Layer diff --git a/packages/spec/src/system/translation.test.ts b/packages/spec/src/system/translation.test.ts index b68980c6c8..9a9423be1f 100644 --- a/packages/spec/src/system/translation.test.ts +++ b/packages/spec/src/system/translation.test.ts @@ -3,7 +3,13 @@ import { TranslationDataSchema, TranslationBundleSchema, LocaleSchema, + FieldTranslationSchema, + ObjectTranslationDataSchema, + TranslationFileOrganizationSchema, + TranslationConfigSchema, type TranslationBundle, + type ObjectTranslationData, + type TranslationConfig, } from './translation.zod'; describe('LocaleSchema', () => { @@ -426,3 +432,185 @@ describe('TranslationDataSchema - validationMessages', () => { expect(data.validationMessages).toBeUndefined(); }); }); + +// ============================================================================ +// FieldTranslationSchema +// ============================================================================ + +describe('FieldTranslationSchema', () => { + it('should accept label only', () => { + const result = FieldTranslationSchema.parse({ label: 'Account Name' }); + expect(result.label).toBe('Account Name'); + expect(result.help).toBeUndefined(); + expect(result.options).toBeUndefined(); + }); + + it('should accept label with help text', () => { + const result = FieldTranslationSchema.parse({ + label: 'Industry', + help: 'Select the primary industry', + }); + expect(result.help).toBe('Select the primary industry'); + }); + + it('should accept field with options', () => { + const result = FieldTranslationSchema.parse({ + label: 'Status', + options: { active: 'Active', inactive: 'Inactive' }, + }); + expect(result.options?.active).toBe('Active'); + }); + + it('should accept empty object', () => { + const result = FieldTranslationSchema.parse({}); + expect(result).toBeDefined(); + }); +}); + +// ============================================================================ +// ObjectTranslationDataSchema — per-object file validation +// ============================================================================ + +describe('ObjectTranslationDataSchema', () => { + it('should accept minimal object translation', () => { + const data: ObjectTranslationData = { + label: 'Account', + }; + const result = ObjectTranslationDataSchema.parse(data); + expect(result.label).toBe('Account'); + expect(result.pluralLabel).toBeUndefined(); + expect(result.fields).toBeUndefined(); + }); + + it('should accept full object translation (en/account.json)', () => { + const data = ObjectTranslationDataSchema.parse({ + label: 'Account', + pluralLabel: 'Accounts', + fields: { + name: { label: 'Account Name', help: 'Legal name of the company' }, + type: { + label: 'Type', + options: { customer: 'Customer', partner: 'Partner', vendor: 'Vendor' }, + }, + industry: { label: 'Industry' }, + }, + }); + expect(data.label).toBe('Account'); + expect(data.pluralLabel).toBe('Accounts'); + expect(data.fields?.name.label).toBe('Account Name'); + expect(data.fields?.type.options?.customer).toBe('Customer'); + }); + + it('should accept Chinese object translation (zh-CN/account.json)', () => { + const data = ObjectTranslationDataSchema.parse({ + label: '客户', + pluralLabel: '客户', + fields: { + name: { label: '客户名称', help: '公司或组织的法定名称' }, + type: { + label: '类型', + options: { customer: '正式客户', partner: '合作伙伴' }, + }, + }, + }); + expect(data.label).toBe('客户'); + expect(data.fields?.name.help).toBe('公司或组织的法定名称'); + }); + + it('should reject object translation without label', () => { + expect(() => + ObjectTranslationDataSchema.parse({ pluralLabel: 'Accounts' }), + ).toThrow(); + }); + + it('should compose into TranslationDataSchema via objects record', () => { + const localeData = TranslationDataSchema.parse({ + objects: { + account: { label: 'Account', pluralLabel: 'Accounts' }, + contact: { label: 'Contact' }, + }, + }); + expect(localeData.objects?.account.label).toBe('Account'); + expect(localeData.objects?.contact.label).toBe('Contact'); + }); +}); + +// ============================================================================ +// TranslationFileOrganizationSchema +// ============================================================================ + +describe('TranslationFileOrganizationSchema', () => { + it('should accept bundled', () => { + expect(TranslationFileOrganizationSchema.parse('bundled')).toBe('bundled'); + }); + + it('should accept per_locale', () => { + expect(TranslationFileOrganizationSchema.parse('per_locale')).toBe('per_locale'); + }); + + it('should accept per_namespace', () => { + expect(TranslationFileOrganizationSchema.parse('per_namespace')).toBe('per_namespace'); + }); + + it('should reject invalid value', () => { + expect(() => TranslationFileOrganizationSchema.parse('flat')).toThrow(); + }); +}); + +// ============================================================================ +// TranslationConfigSchema +// ============================================================================ + +describe('TranslationConfigSchema', () => { + it('should accept minimal config with defaults', () => { + const config: TranslationConfig = TranslationConfigSchema.parse({ + defaultLocale: 'en', + supportedLocales: ['en'], + }); + expect(config.defaultLocale).toBe('en'); + expect(config.supportedLocales).toEqual(['en']); + expect(config.fallbackLocale).toBeUndefined(); + expect(config.fileOrganization).toBe('per_locale'); + expect(config.lazyLoad).toBe(false); + expect(config.cache).toBe(true); + }); + + it('should accept full multi-language config', () => { + const config = TranslationConfigSchema.parse({ + defaultLocale: 'en', + supportedLocales: ['en', 'zh-CN', 'ja-JP', 'es-ES'], + fallbackLocale: 'en', + fileOrganization: 'per_namespace', + lazyLoad: true, + cache: true, + }); + expect(config.supportedLocales).toHaveLength(4); + expect(config.fileOrganization).toBe('per_namespace'); + expect(config.lazyLoad).toBe(true); + }); + + it('should accept bundled organization for small projects', () => { + const config = TranslationConfigSchema.parse({ + defaultLocale: 'en', + supportedLocales: ['en', 'zh-CN'], + fileOrganization: 'bundled', + }); + expect(config.fileOrganization).toBe('bundled'); + }); + + it('should reject config without defaultLocale', () => { + expect(() => + TranslationConfigSchema.parse({ + supportedLocales: ['en'], + }), + ).toThrow(); + }); + + it('should reject config without supportedLocales', () => { + expect(() => + TranslationConfigSchema.parse({ + defaultLocale: 'en', + }), + ).toThrow(); + }); +}); diff --git a/packages/spec/src/system/translation.zod.ts b/packages/spec/src/system/translation.zod.ts index 49c8ac8890..983eba0a5a 100644 --- a/packages/spec/src/system/translation.zod.ts +++ b/packages/spec/src/system/translation.zod.ts @@ -2,28 +2,79 @@ import { z } from 'zod'; +// ──────────────────────────────────────────────────────────────────────────── +// Locale +// ──────────────────────────────────────────────────────────────────────────── + +export const LocaleSchema = z.string().describe('BCP-47 Language Tag (e.g. en-US, zh-CN)'); + +// ──────────────────────────────────────────────────────────────────────────── +// Object-level Translation (per-object file) +// ──────────────────────────────────────────────────────────────────────────── + +/** + * Field Translation Schema + * Translation data for a single field. + */ +export const FieldTranslationSchema = z.object({ + label: z.string().optional().describe('Translated field label'), + help: z.string().optional().describe('Translated help text'), + options: z.record(z.string(), z.string()).optional().describe('Option value to translated label map'), +}).describe('Translation data for a single field'); + +export type FieldTranslation = z.infer; + +/** + * Object Translation Data Schema + * + * Translation data for a **single object** in a **single locale**. + * Use this schema to validate per-object translation files. + * + * File convention: `i18n/{locale}/{object_name}.json` + * + * @example + * ```json + * // i18n/en/account.json + * { + * "label": "Account", + * "pluralLabel": "Accounts", + * "fields": { + * "name": { "label": "Account Name", "help": "Legal name" }, + * "type": { "label": "Type", "options": { "customer": "Customer" } } + * } + * } + * ``` + */ +export const ObjectTranslationDataSchema = z.object({ + /** Translated singular label for the object */ + label: z.string().describe('Translated singular label'), + /** Translated plural label for the object */ + pluralLabel: z.string().optional().describe('Translated plural label'), + /** Field-level translations keyed by field name (snake_case) */ + fields: z.record(z.string(), FieldTranslationSchema).optional().describe('Field-level translations'), +}).describe('Translation data for a single object'); + +export type ObjectTranslationData = z.infer; + +// ──────────────────────────────────────────────────────────────────────────── +// Locale-level Translation Data (per-locale aggregate) +// ──────────────────────────────────────────────────────────────────────────── + /** - * Translation Schema - * Supports i18n for labels, messages, and options. + * Translation Data Schema + * Supports i18n for labels, messages, and options within a single locale. * Example structure: * ```json * { - * "en": { "objects": { "account": { "label": "Account" } } }, - * "zh-CN": { "objects": { "account": { "label": "客户" } } } + * "objects": { "account": { "label": "Account" } }, + * "apps": { "crm": { "label": "CRM" } }, + * "messages": { "common.save": "Save" } * } * ``` */ export const TranslationDataSchema = z.object({ /** Object translations */ - objects: z.record(z.string(), z.object({ - label: z.string().describe('Translated singular label'), - pluralLabel: z.string().optional().describe('Translated plural label'), - fields: z.record(z.string(), z.object({ - label: z.string().optional().describe('Translated field label'), - help: z.string().optional().describe('Translated help text'), - options: z.record(z.string(), z.string()).optional().describe('Option value to translated label map'), - })).optional().describe('Field-level translations'), - })).optional().describe('Object translations keyed by object name'), + objects: z.record(z.string(), ObjectTranslationDataSchema).optional().describe('Object translations keyed by object name'), /** App/Menu translations */ apps: z.record(z.string(), z.object({ @@ -38,8 +89,99 @@ export const TranslationDataSchema = z.object({ validationMessages: z.record(z.string(), z.string()).optional().describe('Translatable validation error messages keyed by rule name (e.g., {"discount_limit": "折扣不能超过40%"})'), }).describe('Translation data for objects, apps, and UI messages'); -export const LocaleSchema = z.string().describe('BCP-47 Language Tag (e.g. en-US, zh-CN)'); +export type TranslationData = z.infer; + +// ──────────────────────────────────────────────────────────────────────────── +// Translation Bundle (all locales) +// ──────────────────────────────────────────────────────────────────────────── export const TranslationBundleSchema = z.record(LocaleSchema, TranslationDataSchema).describe('Map of locale codes to translation data'); export type TranslationBundle = z.infer; + +// ──────────────────────────────────────────────────────────────────────────── +// File Organization Convention +// ──────────────────────────────────────────────────────────────────────────── + +/** + * Translation File Organization Strategy + * + * Defines how translation files are organized on disk. + * + * - `bundled` — All locales in a single `TranslationBundle` file. + * Best for small projects with few objects. + * ``` + * src/translations/ + * crm.translation.ts # { en: {...}, "zh-CN": {...} } + * ``` + * + * - `per_locale` — 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 + * ``` + * + * - `per_namespace` — 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 + * common.json # messages + app labels + * zh-CN/ + * account.json + * contact.json + * common.json + * ``` + */ +export const TranslationFileOrganizationSchema = z.enum([ + 'bundled', + 'per_locale', + 'per_namespace', +]).describe('Translation file organization strategy'); + +export type TranslationFileOrganization = z.infer; + +// ──────────────────────────────────────────────────────────────────────────── +// Translation Configuration +// ──────────────────────────────────────────────────────────────────────────── + +/** + * Translation Configuration Schema + * + * Defines internationalization settings for the stack. + * + * @example + * ```typescript + * export default defineStack({ + * i18n: { + * defaultLocale: 'en', + * supportedLocales: ['en', 'zh-CN', 'ja-JP'], + * fallbackLocale: 'en', + * fileOrganization: 'per_locale', + * }, + * translations: [...], + * }); + * ``` + */ +export const TranslationConfigSchema = z.object({ + /** Default locale for the application */ + defaultLocale: LocaleSchema.describe('Default locale (e.g., "en")'), + /** Supported BCP-47 locale codes */ + supportedLocales: z.array(LocaleSchema).describe('Supported BCP-47 locale codes'), + /** Fallback locale when translation is not found */ + fallbackLocale: LocaleSchema.optional().describe('Fallback locale code'), + /** How translation files are organized on disk */ + fileOrganization: TranslationFileOrganizationSchema.default('per_locale') + .describe('File organization strategy'), + /** Load translations on demand instead of eagerly */ + lazyLoad: z.boolean().default(false).describe('Load translations on demand'), + /** Cache loaded translations in memory */ + cache: z.boolean().default(true).describe('Cache loaded translations'), +}).describe('Internationalization configuration'); + +export type TranslationConfig = z.infer; From d8d41242263c80351a5ad04c538f1fdef03ca15e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 11:14:52 +0000 Subject: [PATCH 3/3] feat(app-crm): refactor translations to per-locale file splitting Split monolithic crm.translation.ts into per-locale files: - en.ts, zh-CN.ts, ja-JP.ts, es-ES.ts - Add i18n config to objectstack.config.ts Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- examples/app-crm/objectstack.config.ts | 8 + .../src/translations/crm.translation.ts | 695 +----------------- examples/app-crm/src/translations/en.ts | 179 +++++ examples/app-crm/src/translations/es-ES.ts | 178 +++++ examples/app-crm/src/translations/ja-JP.ts | 178 +++++ examples/app-crm/src/translations/zh-CN.ts | 178 +++++ 6 files changed, 733 insertions(+), 683 deletions(-) create mode 100644 examples/app-crm/src/translations/en.ts create mode 100644 examples/app-crm/src/translations/es-ES.ts create mode 100644 examples/app-crm/src/translations/ja-JP.ts create mode 100644 examples/app-crm/src/translations/zh-CN.ts diff --git a/examples/app-crm/objectstack.config.ts b/examples/app-crm/objectstack.config.ts index 28ffba6522..83f4354feb 100644 --- a/examples/app-crm/objectstack.config.ts +++ b/examples/app-crm/objectstack.config.ts @@ -50,6 +50,14 @@ export default defineStack({ // Seed Data (top-level, registered as metadata) data: CrmSeedData, + // I18n Configuration — per-locale file organization + i18n: { + defaultLocale: 'en', + supportedLocales: ['en', 'zh-CN', 'ja-JP', 'es-ES'], + fallbackLocale: 'en', + fileOrganization: 'per_locale', + }, + // I18n Translation Bundles (en, zh-CN, ja-JP, es-ES) translations: Object.values(translations), diff --git a/examples/app-crm/src/translations/crm.translation.ts b/examples/app-crm/src/translations/crm.translation.ts index 0cbd1dfc58..c310b5d3a1 100644 --- a/examples/app-crm/src/translations/crm.translation.ts +++ b/examples/app-crm/src/translations/crm.translation.ts @@ -1,10 +1,18 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import type { TranslationBundle } from '@objectstack/spec/system'; +import { en } from './en'; +import { zhCN } from './zh-CN'; +import { jaJP } from './ja-JP'; +import { esES } from './es-ES'; /** * CRM App — Internationalization (i18n) * + * Demonstrates **per-locale file splitting** convention: + * each language is defined in its own file (`en.ts`, `zh-CN.ts`, `ja-JP.ts`, `es-ES.ts`) + * and assembled into a single `TranslationBundle` here. + * * Enterprise-grade multi-language translations covering: * - Core CRM objects: Account, Contact, Lead, Opportunity * - Select-field option labels for each object @@ -14,687 +22,8 @@ import type { TranslationBundle } from '@objectstack/spec/system'; * Supported locales: en, zh-CN, ja-JP, es-ES */ export const CrmTranslations: TranslationBundle = { - // ─── English (base) ─────────────────────────────────────────────── - en: { - objects: { - account: { - label: 'Account', - pluralLabel: 'Accounts', - fields: { - account_number: { label: 'Account Number' }, - name: { label: 'Account Name', help: 'Legal name of the company or organization' }, - type: { - label: 'Type', - options: { prospect: 'Prospect', customer: 'Customer', partner: 'Partner', former: 'Former' }, - }, - industry: { - label: 'Industry', - options: { - technology: 'Technology', finance: 'Finance', healthcare: 'Healthcare', - retail: 'Retail', manufacturing: 'Manufacturing', education: 'Education', - }, - }, - annual_revenue: { label: 'Annual Revenue' }, - number_of_employees: { label: 'Number of Employees' }, - phone: { label: 'Phone' }, - website: { label: 'Website' }, - billing_address: { label: 'Billing Address' }, - office_location: { label: 'Office Location' }, - owner: { label: 'Account Owner' }, - parent_account: { label: 'Parent Account' }, - description: { label: 'Description' }, - is_active: { label: 'Active' }, - last_activity_date: { label: 'Last Activity Date' }, - }, - }, - - contact: { - label: 'Contact', - pluralLabel: 'Contacts', - fields: { - salutation: { label: 'Salutation' }, - first_name: { label: 'First Name' }, - last_name: { label: 'Last Name' }, - full_name: { label: 'Full Name' }, - account: { label: 'Account' }, - email: { label: 'Email' }, - phone: { label: 'Phone' }, - mobile: { label: 'Mobile' }, - title: { label: 'Title' }, - department: { - label: 'Department', - options: { - Executive: 'Executive', Sales: 'Sales', Marketing: 'Marketing', - Engineering: 'Engineering', Support: 'Support', Finance: 'Finance', - HR: 'Human Resources', Operations: 'Operations', - }, - }, - owner: { label: 'Contact Owner' }, - description: { label: 'Description' }, - is_primary: { label: 'Primary Contact' }, - }, - }, - - lead: { - label: 'Lead', - pluralLabel: 'Leads', - fields: { - first_name: { label: 'First Name' }, - last_name: { label: 'Last Name' }, - company: { label: 'Company' }, - title: { label: 'Title' }, - email: { label: 'Email' }, - phone: { label: 'Phone' }, - status: { - label: 'Status', - options: { - new: 'New', contacted: 'Contacted', qualified: 'Qualified', - unqualified: 'Unqualified', converted: 'Converted', - }, - }, - lead_source: { - label: 'Lead Source', - options: { - Web: 'Web', Referral: 'Referral', Event: 'Event', - Partner: 'Partner', Advertisement: 'Advertisement', 'Cold Call': 'Cold Call', - }, - }, - owner: { label: 'Lead Owner' }, - is_converted: { label: 'Converted' }, - description: { label: 'Description' }, - }, - }, - - opportunity: { - label: 'Opportunity', - pluralLabel: 'Opportunities', - fields: { - name: { label: 'Opportunity Name' }, - account: { label: 'Account' }, - primary_contact: { label: 'Primary Contact' }, - owner: { label: 'Opportunity Owner' }, - amount: { label: 'Amount' }, - expected_revenue: { label: 'Expected Revenue' }, - stage: { - label: 'Stage', - options: { - prospecting: 'Prospecting', qualification: 'Qualification', - needs_analysis: 'Needs Analysis', proposal: 'Proposal', - negotiation: 'Negotiation', closed_won: 'Closed Won', closed_lost: 'Closed Lost', - }, - }, - probability: { label: 'Probability (%)' }, - close_date: { label: 'Close Date' }, - type: { - label: 'Type', - options: { - 'New Business': 'New Business', - 'Existing Customer - Upgrade': 'Existing Customer - Upgrade', - 'Existing Customer - Renewal': 'Existing Customer - Renewal', - 'Existing Customer - Expansion': 'Existing Customer - Expansion', - }, - }, - forecast_category: { - label: 'Forecast Category', - options: { - Pipeline: 'Pipeline', 'Best Case': 'Best Case', - Commit: 'Commit', Omitted: 'Omitted', Closed: 'Closed', - }, - }, - description: { label: 'Description' }, - next_step: { label: 'Next Step' }, - }, - }, - }, - - apps: { - crm_enterprise: { - label: 'Enterprise CRM', - description: 'Customer relationship management for sales, service, and marketing', - }, - }, - - messages: { - 'common.save': 'Save', - 'common.cancel': 'Cancel', - 'common.delete': 'Delete', - 'common.edit': 'Edit', - 'common.create': 'Create', - 'common.search': 'Search', - 'common.filter': 'Filter', - 'common.export': 'Export', - 'common.back': 'Back', - 'common.confirm': 'Confirm', - 'nav.sales': 'Sales', - 'nav.service': 'Service', - 'nav.marketing': 'Marketing', - 'nav.products': 'Products', - 'nav.analytics': 'Analytics', - 'success.saved': 'Record saved successfully', - 'success.converted': 'Lead converted successfully', - 'confirm.delete': 'Are you sure you want to delete this record?', - 'confirm.convert_lead': 'Convert this lead to account, contact, and opportunity?', - 'error.required': 'This field is required', - 'error.load_failed': 'Failed to load data', - }, - - validationMessages: { - amount_required_for_closed: 'Amount is required when stage is Closed Won', - close_date_required: 'Close date is required for opportunities', - discount_limit: 'Discount cannot exceed 40%', - }, - }, - - // ─── Chinese Simplified ─────────────────────────────────────────── - 'zh-CN': { - objects: { - account: { - label: '客户', - pluralLabel: '客户', - fields: { - account_number: { label: '客户编号' }, - name: { label: '客户名称', help: '公司或组织的法定名称' }, - type: { - label: '类型', - options: { prospect: '潜在客户', customer: '正式客户', partner: '合作伙伴', former: '前客户' }, - }, - industry: { - label: '行业', - options: { - technology: '科技', finance: '金融', healthcare: '医疗', - retail: '零售', manufacturing: '制造', education: '教育', - }, - }, - annual_revenue: { label: '年营收' }, - number_of_employees: { label: '员工人数' }, - phone: { label: '电话' }, - website: { label: '网站' }, - billing_address: { label: '账单地址' }, - office_location: { label: '办公地点' }, - owner: { label: '客户负责人' }, - parent_account: { label: '母公司' }, - description: { label: '描述' }, - is_active: { label: '是否活跃' }, - last_activity_date: { label: '最近活动日期' }, - }, - }, - - contact: { - label: '联系人', - pluralLabel: '联系人', - fields: { - salutation: { label: '称谓' }, - first_name: { label: '名' }, - last_name: { label: '姓' }, - full_name: { label: '全名' }, - account: { label: '所属客户' }, - email: { label: '邮箱' }, - phone: { label: '电话' }, - mobile: { label: '手机' }, - title: { label: '职位' }, - department: { - label: '部门', - options: { - Executive: '管理层', Sales: '销售部', Marketing: '市场部', - Engineering: '工程部', Support: '支持部', Finance: '财务部', - HR: '人力资源', Operations: '运营部', - }, - }, - owner: { label: '联系人负责人' }, - description: { label: '描述' }, - is_primary: { label: '主要联系人' }, - }, - }, - - lead: { - label: '线索', - pluralLabel: '线索', - fields: { - first_name: { label: '名' }, - last_name: { label: '姓' }, - company: { label: '公司' }, - title: { label: '职位' }, - email: { label: '邮箱' }, - phone: { label: '电话' }, - status: { - label: '状态', - options: { - new: '新建', contacted: '已联系', qualified: '已确认', - unqualified: '不合格', converted: '已转化', - }, - }, - lead_source: { - label: '线索来源', - options: { - Web: '网站', Referral: '推荐', Event: '活动', - Partner: '合作伙伴', Advertisement: '广告', 'Cold Call': '陌生拜访', - }, - }, - owner: { label: '线索负责人' }, - is_converted: { label: '已转化' }, - description: { label: '描述' }, - }, - }, - - opportunity: { - label: '商机', - pluralLabel: '商机', - fields: { - name: { label: '商机名称' }, - account: { label: '所属客户' }, - primary_contact: { label: '主要联系人' }, - owner: { label: '商机负责人' }, - amount: { label: '金额' }, - expected_revenue: { label: '预期收入' }, - stage: { - label: '阶段', - options: { - prospecting: '寻找客户', qualification: '资格审查', - needs_analysis: '需求分析', proposal: '提案', - negotiation: '谈判', closed_won: '成交', closed_lost: '失败', - }, - }, - probability: { label: '成交概率 (%)' }, - close_date: { label: '预计成交日期' }, - type: { - label: '类型', - options: { - 'New Business': '新业务', - 'Existing Customer - Upgrade': '老客户升级', - 'Existing Customer - Renewal': '老客户续约', - 'Existing Customer - Expansion': '老客户拓展', - }, - }, - forecast_category: { - label: '预测类别', - options: { - Pipeline: '管道', 'Best Case': '最佳情况', - Commit: '承诺', Omitted: '已排除', Closed: '已关闭', - }, - }, - description: { label: '描述' }, - next_step: { label: '下一步' }, - }, - }, - }, - - apps: { - crm_enterprise: { - label: '企业 CRM', - description: '涵盖销售、服务和市场营销的客户关系管理系统', - }, - }, - - messages: { - 'common.save': '保存', - 'common.cancel': '取消', - 'common.delete': '删除', - 'common.edit': '编辑', - 'common.create': '新建', - 'common.search': '搜索', - 'common.filter': '筛选', - 'common.export': '导出', - 'common.back': '返回', - 'common.confirm': '确认', - 'nav.sales': '销售', - 'nav.service': '服务', - 'nav.marketing': '营销', - 'nav.products': '产品', - 'nav.analytics': '数据分析', - 'success.saved': '记录保存成功', - 'success.converted': '线索转化成功', - 'confirm.delete': '确定要删除此记录吗?', - 'confirm.convert_lead': '将此线索转化为客户、联系人和商机?', - 'error.required': '此字段为必填项', - 'error.load_failed': '数据加载失败', - }, - - validationMessages: { - amount_required_for_closed: '阶段为"成交"时,金额为必填项', - close_date_required: '商机必须填写预计成交日期', - discount_limit: '折扣不能超过40%', - }, - }, - - // ─── Japanese ───────────────────────────────────────────────────── - 'ja-JP': { - objects: { - account: { - label: '取引先', - pluralLabel: '取引先', - fields: { - account_number: { label: '取引先番号' }, - name: { label: '取引先名', help: '会社または組織の正式名称' }, - type: { - label: 'タイプ', - options: { prospect: '見込み客', customer: '顧客', partner: 'パートナー', former: '過去の取引先' }, - }, - industry: { - label: '業種', - options: { - technology: 'テクノロジー', finance: '金融', healthcare: 'ヘルスケア', - retail: '小売', manufacturing: '製造', education: '教育', - }, - }, - annual_revenue: { label: '年間売上' }, - number_of_employees: { label: '従業員数' }, - phone: { label: '電話番号' }, - website: { label: 'Webサイト' }, - billing_address: { label: '請求先住所' }, - office_location: { label: 'オフィス所在地' }, - owner: { label: '取引先責任者' }, - parent_account: { label: '親取引先' }, - description: { label: '説明' }, - is_active: { label: '有効' }, - last_activity_date: { label: '最終活動日' }, - }, - }, - - contact: { - label: '取引先責任者', - pluralLabel: '取引先責任者', - fields: { - salutation: { label: '敬称' }, - first_name: { label: '名' }, - last_name: { label: '姓' }, - full_name: { label: '氏名' }, - account: { label: '取引先' }, - email: { label: 'メール' }, - phone: { label: '電話' }, - mobile: { label: '携帯電話' }, - title: { label: '役職' }, - department: { - label: '部門', - options: { - Executive: '経営層', Sales: '営業部', Marketing: 'マーケティング部', - Engineering: 'エンジニアリング部', Support: 'サポート部', Finance: '経理部', - HR: '人事部', Operations: 'オペレーション部', - }, - }, - owner: { label: '所有者' }, - description: { label: '説明' }, - is_primary: { label: '主担当者' }, - }, - }, - - lead: { - label: 'リード', - pluralLabel: 'リード', - fields: { - first_name: { label: '名' }, - last_name: { label: '姓' }, - company: { label: '会社名' }, - title: { label: '役職' }, - email: { label: 'メール' }, - phone: { label: '電話' }, - status: { - label: 'ステータス', - options: { - new: '新規', contacted: 'コンタクト済み', qualified: '適格', - unqualified: '不適格', converted: '取引開始済み', - }, - }, - lead_source: { - label: 'リードソース', - options: { - Web: 'Web', Referral: '紹介', Event: 'イベント', - Partner: 'パートナー', Advertisement: '広告', 'Cold Call': 'コールドコール', - }, - }, - owner: { label: 'リード所有者' }, - is_converted: { label: '取引開始済み' }, - description: { label: '説明' }, - }, - }, - - opportunity: { - label: '商談', - pluralLabel: '商談', - fields: { - name: { label: '商談名' }, - account: { label: '取引先' }, - primary_contact: { label: '主担当者' }, - owner: { label: '商談所有者' }, - amount: { label: '金額' }, - expected_revenue: { label: '期待収益' }, - stage: { - label: 'フェーズ', - options: { - prospecting: '見込み調査', qualification: '選定', - needs_analysis: 'ニーズ分析', proposal: '提案', - negotiation: '交渉', closed_won: '成立', closed_lost: '不成立', - }, - }, - probability: { label: '確度 (%)' }, - close_date: { label: '完了予定日' }, - type: { - label: 'タイプ', - options: { - 'New Business': '新規ビジネス', - 'Existing Customer - Upgrade': '既存顧客 - アップグレード', - 'Existing Customer - Renewal': '既存顧客 - 更新', - 'Existing Customer - Expansion': '既存顧客 - 拡大', - }, - }, - forecast_category: { - label: '売上予測カテゴリ', - options: { - Pipeline: 'パイプライン', 'Best Case': '最良ケース', - Commit: 'コミット', Omitted: '除外', Closed: '完了', - }, - }, - description: { label: '説明' }, - next_step: { label: '次のステップ' }, - }, - }, - }, - - apps: { - crm_enterprise: { - label: 'エンタープライズ CRM', - description: '営業・サービス・マーケティング向け顧客関係管理システム', - }, - }, - - messages: { - 'common.save': '保存', - 'common.cancel': 'キャンセル', - 'common.delete': '削除', - 'common.edit': '編集', - 'common.create': '新規作成', - 'common.search': '検索', - 'common.filter': 'フィルター', - 'common.export': 'エクスポート', - 'common.back': '戻る', - 'common.confirm': '確認', - 'nav.sales': '営業', - 'nav.service': 'サービス', - 'nav.marketing': 'マーケティング', - 'nav.products': '製品', - 'nav.analytics': 'アナリティクス', - 'success.saved': 'レコードを保存しました', - 'success.converted': 'リードを取引開始しました', - 'confirm.delete': 'このレコードを削除してもよろしいですか?', - 'confirm.convert_lead': 'このリードを取引先・取引先責任者・商談に変換しますか?', - 'error.required': 'この項目は必須です', - 'error.load_failed': 'データの読み込みに失敗しました', - }, - - validationMessages: { - amount_required_for_closed: 'フェーズが「成立」の場合、金額は必須です', - close_date_required: '商談には完了予定日が必要です', - discount_limit: '割引は40%を超えることはできません', - }, - }, - - // ─── Spanish ────────────────────────────────────────────────────── - 'es-ES': { - objects: { - account: { - label: 'Cuenta', - pluralLabel: 'Cuentas', - fields: { - account_number: { label: 'Número de Cuenta' }, - name: { label: 'Nombre de Cuenta', help: 'Nombre legal de la empresa u organización' }, - type: { - label: 'Tipo', - options: { prospect: 'Prospecto', customer: 'Cliente', partner: 'Socio', former: 'Anterior' }, - }, - industry: { - label: 'Industria', - options: { - technology: 'Tecnología', finance: 'Finanzas', healthcare: 'Salud', - retail: 'Comercio', manufacturing: 'Manufactura', education: 'Educación', - }, - }, - annual_revenue: { label: 'Ingresos Anuales' }, - number_of_employees: { label: 'Número de Empleados' }, - phone: { label: 'Teléfono' }, - website: { label: 'Sitio Web' }, - billing_address: { label: 'Dirección de Facturación' }, - office_location: { label: 'Ubicación de Oficina' }, - owner: { label: 'Propietario de Cuenta' }, - parent_account: { label: 'Cuenta Matriz' }, - description: { label: 'Descripción' }, - is_active: { label: 'Activo' }, - last_activity_date: { label: 'Fecha de Última Actividad' }, - }, - }, - - contact: { - label: 'Contacto', - pluralLabel: 'Contactos', - fields: { - salutation: { label: 'Título' }, - first_name: { label: 'Nombre' }, - last_name: { label: 'Apellido' }, - full_name: { label: 'Nombre Completo' }, - account: { label: 'Cuenta' }, - email: { label: 'Correo Electrónico' }, - phone: { label: 'Teléfono' }, - mobile: { label: 'Móvil' }, - title: { label: 'Cargo' }, - department: { - label: 'Departamento', - options: { - Executive: 'Ejecutivo', Sales: 'Ventas', Marketing: 'Marketing', - Engineering: 'Ingeniería', Support: 'Soporte', Finance: 'Finanzas', - HR: 'Recursos Humanos', Operations: 'Operaciones', - }, - }, - owner: { label: 'Propietario de Contacto' }, - description: { label: 'Descripción' }, - is_primary: { label: 'Contacto Principal' }, - }, - }, - - lead: { - label: 'Prospecto', - pluralLabel: 'Prospectos', - fields: { - first_name: { label: 'Nombre' }, - last_name: { label: 'Apellido' }, - company: { label: 'Empresa' }, - title: { label: 'Cargo' }, - email: { label: 'Correo Electrónico' }, - phone: { label: 'Teléfono' }, - status: { - label: 'Estado', - options: { - new: 'Nuevo', contacted: 'Contactado', qualified: 'Calificado', - unqualified: 'No Calificado', converted: 'Convertido', - }, - }, - lead_source: { - label: 'Origen del Prospecto', - options: { - Web: 'Web', Referral: 'Referencia', Event: 'Evento', - Partner: 'Socio', Advertisement: 'Publicidad', 'Cold Call': 'Llamada en Frío', - }, - }, - owner: { label: 'Propietario' }, - is_converted: { label: 'Convertido' }, - description: { label: 'Descripción' }, - }, - }, - - opportunity: { - label: 'Oportunidad', - pluralLabel: 'Oportunidades', - fields: { - name: { label: 'Nombre de Oportunidad' }, - account: { label: 'Cuenta' }, - primary_contact: { label: 'Contacto Principal' }, - owner: { label: 'Propietario de Oportunidad' }, - amount: { label: 'Monto' }, - expected_revenue: { label: 'Ingreso Esperado' }, - stage: { - label: 'Etapa', - options: { - prospecting: 'Prospección', qualification: 'Calificación', - needs_analysis: 'Análisis de Necesidades', proposal: 'Propuesta', - negotiation: 'Negociación', closed_won: 'Cerrada Ganada', closed_lost: 'Cerrada Perdida', - }, - }, - probability: { label: 'Probabilidad (%)' }, - close_date: { label: 'Fecha de Cierre' }, - type: { - label: 'Tipo', - options: { - 'New Business': 'Nuevo Negocio', - 'Existing Customer - Upgrade': 'Cliente Existente - Mejora', - 'Existing Customer - Renewal': 'Cliente Existente - Renovación', - 'Existing Customer - Expansion': 'Cliente Existente - Expansión', - }, - }, - forecast_category: { - label: 'Categoría de Pronóstico', - options: { - Pipeline: 'Pipeline', 'Best Case': 'Mejor Caso', - Commit: 'Compromiso', Omitted: 'Omitida', Closed: 'Cerrada', - }, - }, - description: { label: 'Descripción' }, - next_step: { label: 'Próximo Paso' }, - }, - }, - }, - - apps: { - crm_enterprise: { - label: 'CRM Empresarial', - description: 'Gestión de relaciones con clientes para ventas, servicio y marketing', - }, - }, - - messages: { - 'common.save': 'Guardar', - 'common.cancel': 'Cancelar', - 'common.delete': 'Eliminar', - 'common.edit': 'Editar', - 'common.create': 'Crear', - 'common.search': 'Buscar', - 'common.filter': 'Filtrar', - 'common.export': 'Exportar', - 'common.back': 'Volver', - 'common.confirm': 'Confirmar', - 'nav.sales': 'Ventas', - 'nav.service': 'Servicio', - 'nav.marketing': 'Marketing', - 'nav.products': 'Productos', - 'nav.analytics': 'Analítica', - 'success.saved': 'Registro guardado exitosamente', - 'success.converted': 'Prospecto convertido exitosamente', - 'confirm.delete': '¿Está seguro de que desea eliminar este registro?', - 'confirm.convert_lead': '¿Convertir este prospecto en cuenta, contacto y oportunidad?', - 'error.required': 'Este campo es obligatorio', - 'error.load_failed': 'Error al cargar los datos', - }, - - validationMessages: { - amount_required_for_closed: 'El monto es obligatorio cuando la etapa es Cerrada Ganada', - close_date_required: 'La fecha de cierre es obligatoria para las oportunidades', - discount_limit: 'El descuento no puede superar el 40%', - }, - }, + en, + 'zh-CN': zhCN, + 'ja-JP': jaJP, + 'es-ES': esES, }; diff --git a/examples/app-crm/src/translations/en.ts b/examples/app-crm/src/translations/en.ts new file mode 100644 index 0000000000..39024a3c10 --- /dev/null +++ b/examples/app-crm/src/translations/en.ts @@ -0,0 +1,179 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import type { TranslationData } from '@objectstack/spec/system'; + +/** + * English (en) — CRM App Translations + * + * Per-locale file: one file per language, following the `per_locale` convention. + * Each file exports a single `TranslationData` object for its locale. + */ +export const en: TranslationData = { + objects: { + account: { + label: 'Account', + pluralLabel: 'Accounts', + fields: { + account_number: { label: 'Account Number' }, + name: { label: 'Account Name', help: 'Legal name of the company or organization' }, + type: { + label: 'Type', + options: { prospect: 'Prospect', customer: 'Customer', partner: 'Partner', former: 'Former' }, + }, + industry: { + label: 'Industry', + options: { + technology: 'Technology', finance: 'Finance', healthcare: 'Healthcare', + retail: 'Retail', manufacturing: 'Manufacturing', education: 'Education', + }, + }, + annual_revenue: { label: 'Annual Revenue' }, + number_of_employees: { label: 'Number of Employees' }, + phone: { label: 'Phone' }, + website: { label: 'Website' }, + billing_address: { label: 'Billing Address' }, + office_location: { label: 'Office Location' }, + owner: { label: 'Account Owner' }, + parent_account: { label: 'Parent Account' }, + description: { label: 'Description' }, + is_active: { label: 'Active' }, + last_activity_date: { label: 'Last Activity Date' }, + }, + }, + + contact: { + label: 'Contact', + pluralLabel: 'Contacts', + fields: { + salutation: { label: 'Salutation' }, + first_name: { label: 'First Name' }, + last_name: { label: 'Last Name' }, + full_name: { label: 'Full Name' }, + account: { label: 'Account' }, + email: { label: 'Email' }, + phone: { label: 'Phone' }, + mobile: { label: 'Mobile' }, + title: { label: 'Title' }, + department: { + label: 'Department', + options: { + Executive: 'Executive', Sales: 'Sales', Marketing: 'Marketing', + Engineering: 'Engineering', Support: 'Support', Finance: 'Finance', + HR: 'Human Resources', Operations: 'Operations', + }, + }, + owner: { label: 'Contact Owner' }, + description: { label: 'Description' }, + is_primary: { label: 'Primary Contact' }, + }, + }, + + lead: { + label: 'Lead', + pluralLabel: 'Leads', + fields: { + first_name: { label: 'First Name' }, + last_name: { label: 'Last Name' }, + company: { label: 'Company' }, + title: { label: 'Title' }, + email: { label: 'Email' }, + phone: { label: 'Phone' }, + status: { + label: 'Status', + options: { + new: 'New', contacted: 'Contacted', qualified: 'Qualified', + unqualified: 'Unqualified', converted: 'Converted', + }, + }, + lead_source: { + label: 'Lead Source', + options: { + Web: 'Web', Referral: 'Referral', Event: 'Event', + Partner: 'Partner', Advertisement: 'Advertisement', 'Cold Call': 'Cold Call', + }, + }, + owner: { label: 'Lead Owner' }, + is_converted: { label: 'Converted' }, + description: { label: 'Description' }, + }, + }, + + opportunity: { + label: 'Opportunity', + pluralLabel: 'Opportunities', + fields: { + name: { label: 'Opportunity Name' }, + account: { label: 'Account' }, + primary_contact: { label: 'Primary Contact' }, + owner: { label: 'Opportunity Owner' }, + amount: { label: 'Amount' }, + expected_revenue: { label: 'Expected Revenue' }, + stage: { + label: 'Stage', + options: { + prospecting: 'Prospecting', qualification: 'Qualification', + needs_analysis: 'Needs Analysis', proposal: 'Proposal', + negotiation: 'Negotiation', closed_won: 'Closed Won', closed_lost: 'Closed Lost', + }, + }, + probability: { label: 'Probability (%)' }, + close_date: { label: 'Close Date' }, + type: { + label: 'Type', + options: { + 'New Business': 'New Business', + 'Existing Customer - Upgrade': 'Existing Customer - Upgrade', + 'Existing Customer - Renewal': 'Existing Customer - Renewal', + 'Existing Customer - Expansion': 'Existing Customer - Expansion', + }, + }, + forecast_category: { + label: 'Forecast Category', + options: { + Pipeline: 'Pipeline', 'Best Case': 'Best Case', + Commit: 'Commit', Omitted: 'Omitted', Closed: 'Closed', + }, + }, + description: { label: 'Description' }, + next_step: { label: 'Next Step' }, + }, + }, + }, + + apps: { + crm_enterprise: { + label: 'Enterprise CRM', + description: 'Customer relationship management for sales, service, and marketing', + }, + }, + + messages: { + 'common.save': 'Save', + 'common.cancel': 'Cancel', + 'common.delete': 'Delete', + 'common.edit': 'Edit', + 'common.create': 'Create', + 'common.search': 'Search', + 'common.filter': 'Filter', + 'common.export': 'Export', + 'common.back': 'Back', + 'common.confirm': 'Confirm', + 'nav.sales': 'Sales', + 'nav.service': 'Service', + 'nav.marketing': 'Marketing', + 'nav.products': 'Products', + 'nav.analytics': 'Analytics', + 'success.saved': 'Record saved successfully', + 'success.converted': 'Lead converted successfully', + 'confirm.delete': 'Are you sure you want to delete this record?', + 'confirm.convert_lead': 'Convert this lead to account, contact, and opportunity?', + 'error.required': 'This field is required', + 'error.load_failed': 'Failed to load data', + }, + + validationMessages: { + amount_required_for_closed: 'Amount is required when stage is Closed Won', + close_date_required: 'Close date is required for opportunities', + discount_limit: 'Discount cannot exceed 40%', + }, +}; diff --git a/examples/app-crm/src/translations/es-ES.ts b/examples/app-crm/src/translations/es-ES.ts new file mode 100644 index 0000000000..521c7ba1f6 --- /dev/null +++ b/examples/app-crm/src/translations/es-ES.ts @@ -0,0 +1,178 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import type { TranslationData } from '@objectstack/spec/system'; + +/** + * Español (es-ES) — CRM App Translations + * + * Per-locale file: one file per language, following the `per_locale` convention. + */ +export const esES: TranslationData = { + objects: { + account: { + label: 'Cuenta', + pluralLabel: 'Cuentas', + fields: { + account_number: { label: 'Número de Cuenta' }, + name: { label: 'Nombre de Cuenta', help: 'Nombre legal de la empresa u organización' }, + type: { + label: 'Tipo', + options: { prospect: 'Prospecto', customer: 'Cliente', partner: 'Socio', former: 'Anterior' }, + }, + industry: { + label: 'Industria', + options: { + technology: 'Tecnología', finance: 'Finanzas', healthcare: 'Salud', + retail: 'Comercio', manufacturing: 'Manufactura', education: 'Educación', + }, + }, + annual_revenue: { label: 'Ingresos Anuales' }, + number_of_employees: { label: 'Número de Empleados' }, + phone: { label: 'Teléfono' }, + website: { label: 'Sitio Web' }, + billing_address: { label: 'Dirección de Facturación' }, + office_location: { label: 'Ubicación de Oficina' }, + owner: { label: 'Propietario de Cuenta' }, + parent_account: { label: 'Cuenta Matriz' }, + description: { label: 'Descripción' }, + is_active: { label: 'Activo' }, + last_activity_date: { label: 'Fecha de Última Actividad' }, + }, + }, + + contact: { + label: 'Contacto', + pluralLabel: 'Contactos', + fields: { + salutation: { label: 'Título' }, + first_name: { label: 'Nombre' }, + last_name: { label: 'Apellido' }, + full_name: { label: 'Nombre Completo' }, + account: { label: 'Cuenta' }, + email: { label: 'Correo Electrónico' }, + phone: { label: 'Teléfono' }, + mobile: { label: 'Móvil' }, + title: { label: 'Cargo' }, + department: { + label: 'Departamento', + options: { + Executive: 'Ejecutivo', Sales: 'Ventas', Marketing: 'Marketing', + Engineering: 'Ingeniería', Support: 'Soporte', Finance: 'Finanzas', + HR: 'Recursos Humanos', Operations: 'Operaciones', + }, + }, + owner: { label: 'Propietario de Contacto' }, + description: { label: 'Descripción' }, + is_primary: { label: 'Contacto Principal' }, + }, + }, + + lead: { + label: 'Prospecto', + pluralLabel: 'Prospectos', + fields: { + first_name: { label: 'Nombre' }, + last_name: { label: 'Apellido' }, + company: { label: 'Empresa' }, + title: { label: 'Cargo' }, + email: { label: 'Correo Electrónico' }, + phone: { label: 'Teléfono' }, + status: { + label: 'Estado', + options: { + new: 'Nuevo', contacted: 'Contactado', qualified: 'Calificado', + unqualified: 'No Calificado', converted: 'Convertido', + }, + }, + lead_source: { + label: 'Origen del Prospecto', + options: { + Web: 'Web', Referral: 'Referencia', Event: 'Evento', + Partner: 'Socio', Advertisement: 'Publicidad', 'Cold Call': 'Llamada en Frío', + }, + }, + owner: { label: 'Propietario' }, + is_converted: { label: 'Convertido' }, + description: { label: 'Descripción' }, + }, + }, + + opportunity: { + label: 'Oportunidad', + pluralLabel: 'Oportunidades', + fields: { + name: { label: 'Nombre de Oportunidad' }, + account: { label: 'Cuenta' }, + primary_contact: { label: 'Contacto Principal' }, + owner: { label: 'Propietario de Oportunidad' }, + amount: { label: 'Monto' }, + expected_revenue: { label: 'Ingreso Esperado' }, + stage: { + label: 'Etapa', + options: { + prospecting: 'Prospección', qualification: 'Calificación', + needs_analysis: 'Análisis de Necesidades', proposal: 'Propuesta', + negotiation: 'Negociación', closed_won: 'Cerrada Ganada', closed_lost: 'Cerrada Perdida', + }, + }, + probability: { label: 'Probabilidad (%)' }, + close_date: { label: 'Fecha de Cierre' }, + type: { + label: 'Tipo', + options: { + 'New Business': 'Nuevo Negocio', + 'Existing Customer - Upgrade': 'Cliente Existente - Mejora', + 'Existing Customer - Renewal': 'Cliente Existente - Renovación', + 'Existing Customer - Expansion': 'Cliente Existente - Expansión', + }, + }, + forecast_category: { + label: 'Categoría de Pronóstico', + options: { + Pipeline: 'Pipeline', 'Best Case': 'Mejor Caso', + Commit: 'Compromiso', Omitted: 'Omitida', Closed: 'Cerrada', + }, + }, + description: { label: 'Descripción' }, + next_step: { label: 'Próximo Paso' }, + }, + }, + }, + + apps: { + crm_enterprise: { + label: 'CRM Empresarial', + description: 'Gestión de relaciones con clientes para ventas, servicio y marketing', + }, + }, + + messages: { + 'common.save': 'Guardar', + 'common.cancel': 'Cancelar', + 'common.delete': 'Eliminar', + 'common.edit': 'Editar', + 'common.create': 'Crear', + 'common.search': 'Buscar', + 'common.filter': 'Filtrar', + 'common.export': 'Exportar', + 'common.back': 'Volver', + 'common.confirm': 'Confirmar', + 'nav.sales': 'Ventas', + 'nav.service': 'Servicio', + 'nav.marketing': 'Marketing', + 'nav.products': 'Productos', + 'nav.analytics': 'Analítica', + 'success.saved': 'Registro guardado exitosamente', + 'success.converted': 'Prospecto convertido exitosamente', + 'confirm.delete': '¿Está seguro de que desea eliminar este registro?', + 'confirm.convert_lead': '¿Convertir este prospecto en cuenta, contacto y oportunidad?', + 'error.required': 'Este campo es obligatorio', + 'error.load_failed': 'Error al cargar los datos', + }, + + validationMessages: { + amount_required_for_closed: 'El monto es obligatorio cuando la etapa es Cerrada Ganada', + close_date_required: 'La fecha de cierre es obligatoria para las oportunidades', + discount_limit: 'El descuento no puede superar el 40%', + }, +}; diff --git a/examples/app-crm/src/translations/ja-JP.ts b/examples/app-crm/src/translations/ja-JP.ts new file mode 100644 index 0000000000..09c122ab4b --- /dev/null +++ b/examples/app-crm/src/translations/ja-JP.ts @@ -0,0 +1,178 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import type { TranslationData } from '@objectstack/spec/system'; + +/** + * 日本語 (ja-JP) — CRM App Translations + * + * Per-locale file: one file per language, following the `per_locale` convention. + */ +export const jaJP: TranslationData = { + objects: { + account: { + label: '取引先', + pluralLabel: '取引先', + fields: { + account_number: { label: '取引先番号' }, + name: { label: '取引先名', help: '会社または組織の正式名称' }, + type: { + label: 'タイプ', + options: { prospect: '見込み客', customer: '顧客', partner: 'パートナー', former: '過去の取引先' }, + }, + industry: { + label: '業種', + options: { + technology: 'テクノロジー', finance: '金融', healthcare: 'ヘルスケア', + retail: '小売', manufacturing: '製造', education: '教育', + }, + }, + annual_revenue: { label: '年間売上' }, + number_of_employees: { label: '従業員数' }, + phone: { label: '電話番号' }, + website: { label: 'Webサイト' }, + billing_address: { label: '請求先住所' }, + office_location: { label: 'オフィス所在地' }, + owner: { label: '取引先責任者' }, + parent_account: { label: '親取引先' }, + description: { label: '説明' }, + is_active: { label: '有効' }, + last_activity_date: { label: '最終活動日' }, + }, + }, + + contact: { + label: '取引先責任者', + pluralLabel: '取引先責任者', + fields: { + salutation: { label: '敬称' }, + first_name: { label: '名' }, + last_name: { label: '姓' }, + full_name: { label: '氏名' }, + account: { label: '取引先' }, + email: { label: 'メール' }, + phone: { label: '電話' }, + mobile: { label: '携帯電話' }, + title: { label: '役職' }, + department: { + label: '部門', + options: { + Executive: '経営層', Sales: '営業部', Marketing: 'マーケティング部', + Engineering: 'エンジニアリング部', Support: 'サポート部', Finance: '経理部', + HR: '人事部', Operations: 'オペレーション部', + }, + }, + owner: { label: '所有者' }, + description: { label: '説明' }, + is_primary: { label: '主担当者' }, + }, + }, + + lead: { + label: 'リード', + pluralLabel: 'リード', + fields: { + first_name: { label: '名' }, + last_name: { label: '姓' }, + company: { label: '会社名' }, + title: { label: '役職' }, + email: { label: 'メール' }, + phone: { label: '電話' }, + status: { + label: 'ステータス', + options: { + new: '新規', contacted: 'コンタクト済み', qualified: '適格', + unqualified: '不適格', converted: '取引開始済み', + }, + }, + lead_source: { + label: 'リードソース', + options: { + Web: 'Web', Referral: '紹介', Event: 'イベント', + Partner: 'パートナー', Advertisement: '広告', 'Cold Call': 'コールドコール', + }, + }, + owner: { label: 'リード所有者' }, + is_converted: { label: '取引開始済み' }, + description: { label: '説明' }, + }, + }, + + opportunity: { + label: '商談', + pluralLabel: '商談', + fields: { + name: { label: '商談名' }, + account: { label: '取引先' }, + primary_contact: { label: '主担当者' }, + owner: { label: '商談所有者' }, + amount: { label: '金額' }, + expected_revenue: { label: '期待収益' }, + stage: { + label: 'フェーズ', + options: { + prospecting: '見込み調査', qualification: '選定', + needs_analysis: 'ニーズ分析', proposal: '提案', + negotiation: '交渉', closed_won: '成立', closed_lost: '不成立', + }, + }, + probability: { label: '確度 (%)' }, + close_date: { label: '完了予定日' }, + type: { + label: 'タイプ', + options: { + 'New Business': '新規ビジネス', + 'Existing Customer - Upgrade': '既存顧客 - アップグレード', + 'Existing Customer - Renewal': '既存顧客 - 更新', + 'Existing Customer - Expansion': '既存顧客 - 拡大', + }, + }, + forecast_category: { + label: '売上予測カテゴリ', + options: { + Pipeline: 'パイプライン', 'Best Case': '最良ケース', + Commit: 'コミット', Omitted: '除外', Closed: '完了', + }, + }, + description: { label: '説明' }, + next_step: { label: '次のステップ' }, + }, + }, + }, + + apps: { + crm_enterprise: { + label: 'エンタープライズ CRM', + description: '営業・サービス・マーケティング向け顧客関係管理システム', + }, + }, + + messages: { + 'common.save': '保存', + 'common.cancel': 'キャンセル', + 'common.delete': '削除', + 'common.edit': '編集', + 'common.create': '新規作成', + 'common.search': '検索', + 'common.filter': 'フィルター', + 'common.export': 'エクスポート', + 'common.back': '戻る', + 'common.confirm': '確認', + 'nav.sales': '営業', + 'nav.service': 'サービス', + 'nav.marketing': 'マーケティング', + 'nav.products': '製品', + 'nav.analytics': 'アナリティクス', + 'success.saved': 'レコードを保存しました', + 'success.converted': 'リードを取引開始しました', + 'confirm.delete': 'このレコードを削除してもよろしいですか?', + 'confirm.convert_lead': 'このリードを取引先・取引先責任者・商談に変換しますか?', + 'error.required': 'この項目は必須です', + 'error.load_failed': 'データの読み込みに失敗しました', + }, + + validationMessages: { + amount_required_for_closed: 'フェーズが「成立」の場合、金額は必須です', + close_date_required: '商談には完了予定日が必要です', + discount_limit: '割引は40%を超えることはできません', + }, +}; diff --git a/examples/app-crm/src/translations/zh-CN.ts b/examples/app-crm/src/translations/zh-CN.ts new file mode 100644 index 0000000000..48a0cab690 --- /dev/null +++ b/examples/app-crm/src/translations/zh-CN.ts @@ -0,0 +1,178 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import type { TranslationData } from '@objectstack/spec/system'; + +/** + * 简体中文 (zh-CN) — CRM App Translations + * + * Per-locale file: one file per language, following the `per_locale` convention. + */ +export const zhCN: TranslationData = { + objects: { + account: { + label: '客户', + pluralLabel: '客户', + fields: { + account_number: { label: '客户编号' }, + name: { label: '客户名称', help: '公司或组织的法定名称' }, + type: { + label: '类型', + options: { prospect: '潜在客户', customer: '正式客户', partner: '合作伙伴', former: '前客户' }, + }, + industry: { + label: '行业', + options: { + technology: '科技', finance: '金融', healthcare: '医疗', + retail: '零售', manufacturing: '制造', education: '教育', + }, + }, + annual_revenue: { label: '年营收' }, + number_of_employees: { label: '员工人数' }, + phone: { label: '电话' }, + website: { label: '网站' }, + billing_address: { label: '账单地址' }, + office_location: { label: '办公地点' }, + owner: { label: '客户负责人' }, + parent_account: { label: '母公司' }, + description: { label: '描述' }, + is_active: { label: '是否活跃' }, + last_activity_date: { label: '最近活动日期' }, + }, + }, + + contact: { + label: '联系人', + pluralLabel: '联系人', + fields: { + salutation: { label: '称谓' }, + first_name: { label: '名' }, + last_name: { label: '姓' }, + full_name: { label: '全名' }, + account: { label: '所属客户' }, + email: { label: '邮箱' }, + phone: { label: '电话' }, + mobile: { label: '手机' }, + title: { label: '职位' }, + department: { + label: '部门', + options: { + Executive: '管理层', Sales: '销售部', Marketing: '市场部', + Engineering: '工程部', Support: '支持部', Finance: '财务部', + HR: '人力资源', Operations: '运营部', + }, + }, + owner: { label: '联系人负责人' }, + description: { label: '描述' }, + is_primary: { label: '主要联系人' }, + }, + }, + + lead: { + label: '线索', + pluralLabel: '线索', + fields: { + first_name: { label: '名' }, + last_name: { label: '姓' }, + company: { label: '公司' }, + title: { label: '职位' }, + email: { label: '邮箱' }, + phone: { label: '电话' }, + status: { + label: '状态', + options: { + new: '新建', contacted: '已联系', qualified: '已确认', + unqualified: '不合格', converted: '已转化', + }, + }, + lead_source: { + label: '线索来源', + options: { + Web: '网站', Referral: '推荐', Event: '活动', + Partner: '合作伙伴', Advertisement: '广告', 'Cold Call': '陌生拜访', + }, + }, + owner: { label: '线索负责人' }, + is_converted: { label: '已转化' }, + description: { label: '描述' }, + }, + }, + + opportunity: { + label: '商机', + pluralLabel: '商机', + fields: { + name: { label: '商机名称' }, + account: { label: '所属客户' }, + primary_contact: { label: '主要联系人' }, + owner: { label: '商机负责人' }, + amount: { label: '金额' }, + expected_revenue: { label: '预期收入' }, + stage: { + label: '阶段', + options: { + prospecting: '寻找客户', qualification: '资格审查', + needs_analysis: '需求分析', proposal: '提案', + negotiation: '谈判', closed_won: '成交', closed_lost: '失败', + }, + }, + probability: { label: '成交概率 (%)' }, + close_date: { label: '预计成交日期' }, + type: { + label: '类型', + options: { + 'New Business': '新业务', + 'Existing Customer - Upgrade': '老客户升级', + 'Existing Customer - Renewal': '老客户续约', + 'Existing Customer - Expansion': '老客户拓展', + }, + }, + forecast_category: { + label: '预测类别', + options: { + Pipeline: '管道', 'Best Case': '最佳情况', + Commit: '承诺', Omitted: '已排除', Closed: '已关闭', + }, + }, + description: { label: '描述' }, + next_step: { label: '下一步' }, + }, + }, + }, + + apps: { + crm_enterprise: { + label: '企业 CRM', + description: '涵盖销售、服务和市场营销的客户关系管理系统', + }, + }, + + messages: { + 'common.save': '保存', + 'common.cancel': '取消', + 'common.delete': '删除', + 'common.edit': '编辑', + 'common.create': '新建', + 'common.search': '搜索', + 'common.filter': '筛选', + 'common.export': '导出', + 'common.back': '返回', + 'common.confirm': '确认', + 'nav.sales': '销售', + 'nav.service': '服务', + 'nav.marketing': '营销', + 'nav.products': '产品', + 'nav.analytics': '数据分析', + 'success.saved': '记录保存成功', + 'success.converted': '线索转化成功', + 'confirm.delete': '确定要删除此记录吗?', + 'confirm.convert_lead': '将此线索转化为客户、联系人和商机?', + 'error.required': '此字段为必填项', + 'error.load_failed': '数据加载失败', + }, + + validationMessages: { + amount_required_for_closed: '阶段为"成交"时,金额为必填项', + close_date_required: '商机必须填写预计成交日期', + discount_limit: '折扣不能超过40%', + }, +};