@object-ui/i18n 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +115 -0
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.js +2 -0
  6. package/dist/locales/ar.d.ts +73 -0
  7. package/dist/locales/ar.js +68 -0
  8. package/dist/locales/de.d.ts +73 -0
  9. package/dist/locales/de.js +73 -0
  10. package/dist/locales/en.d.ts +73 -0
  11. package/dist/locales/en.js +73 -0
  12. package/dist/locales/es.d.ts +73 -0
  13. package/dist/locales/es.js +68 -0
  14. package/dist/locales/fr.d.ts +73 -0
  15. package/dist/locales/fr.js +73 -0
  16. package/dist/locales/index.d.ts +730 -0
  17. package/dist/locales/ja.d.ts +73 -0
  18. package/dist/locales/ja.js +68 -0
  19. package/dist/locales/ko.d.ts +73 -0
  20. package/dist/locales/ko.js +68 -0
  21. package/dist/locales/pt.d.ts +73 -0
  22. package/dist/locales/pt.js +68 -0
  23. package/dist/locales/ru.d.ts +73 -0
  24. package/dist/locales/ru.js +68 -0
  25. package/dist/locales/zh.d.ts +73 -0
  26. package/dist/locales/zh.js +73 -0
  27. package/dist/provider.d.ts +19 -1
  28. package/dist/provider.js +7 -2
  29. package/dist/utils/index.d.ts +1 -0
  30. package/dist/utils/index.js +1 -0
  31. package/dist/utils/spec-formatters.d.ts +140 -0
  32. package/dist/utils/spec-formatters.js +118 -0
  33. package/package.json +4 -4
  34. package/src/__tests__/spec-formatters.test.ts +208 -0
  35. package/src/index.ts +12 -0
  36. package/src/locales/ar.ts +68 -0
  37. package/src/locales/de.ts +73 -0
  38. package/src/locales/en.ts +73 -0
  39. package/src/locales/es.ts +68 -0
  40. package/src/locales/fr.ts +73 -0
  41. package/src/locales/ja.ts +68 -0
  42. package/src/locales/ko.ts +68 -0
  43. package/src/locales/pt.ts +68 -0
  44. package/src/locales/ru.ts +68 -0
  45. package/src/locales/zh.ts +73 -0
  46. package/src/provider.tsx +25 -2
  47. package/src/utils/index.ts +11 -0
  48. package/src/utils/spec-formatters.ts +220 -0
  49. package/tsconfig.tsbuildinfo +1 -1
package/src/locales/ja.ts CHANGED
@@ -98,6 +98,74 @@ const ja = {
98
98
  saveLayout: 'レイアウトを保存',
99
99
  resetLayout: 'レイアウトをリセット',
100
100
  },
101
+ console: {
102
+ title: 'ObjectStack コンソール',
103
+ initializing: 'アプリケーションを初期化中...',
104
+ loadingSteps: {
105
+ connecting: 'データソースに接続中',
106
+ loadingConfig: '設定を読み込み中',
107
+ preparingWorkspace: 'ワークスペースを準備中',
108
+ },
109
+ shortcuts: {
110
+ title: 'キーボードショートカット',
111
+ description: '利用可能なすべてのキーボードショートカットのクイックリファレンス。',
112
+ groups: { general: '一般', navigation: 'ナビゲーション', dataViews: 'データビュー', preferences: '設定' },
113
+ openCommandPalette: 'コマンドパレットを開く',
114
+ showShortcuts: 'キーボードショートカットを表示',
115
+ closeDialog: 'ダイアログ / パネルを閉じる',
116
+ toggleSidebar: 'サイドバーを切り替え',
117
+ focusSearch: '検索にフォーカス',
118
+ createRecord: '新しいレコードを作成',
119
+ refreshData: 'データを更新',
120
+ editRecord: '選択したレコードを編集',
121
+ toggleDarkMode: 'ダークモードを切り替え',
122
+ },
123
+ commandPalette: {
124
+ placeholder: 'コマンドを入力または検索...',
125
+ noResults: '結果が見つかりません。',
126
+ objects: 'オブジェクト',
127
+ dashboards: 'ダッシュボード',
128
+ pages: 'ページ',
129
+ reports: 'レポート',
130
+ switchApp: 'アプリを切り替え',
131
+ current: '現在',
132
+ preferences: '設定',
133
+ lightTheme: 'ライトテーマ',
134
+ darkTheme: 'ダークテーマ',
135
+ systemTheme: 'システムテーマ',
136
+ actions: 'アクション',
137
+ openFullSearch: '完全な検索ページを開く',
138
+ },
139
+ errors: {
140
+ somethingWentWrong: '問題が発生しました',
141
+ unexpectedError: 'このビューのレンダリング中に予期しないエラーが発生しました。',
142
+ tryAgain: '再試行',
143
+ goHome: 'ホームに戻る',
144
+ errorDetails: 'エラー詳細(開発モードのみ)',
145
+ },
146
+ theme: {
147
+ toggle: 'テーマを切り替え',
148
+ light: 'ライト',
149
+ dark: 'ダーク',
150
+ system: 'システム',
151
+ },
152
+ objectView: {
153
+ objectNotFound: 'オブジェクトが見つかりません',
154
+ objectNotFoundDescription: 'オブジェクト「{{objectName}}」は現在の設定に存在しません。',
155
+ objectNotFoundHint: 'アプリのナビゲーション設定を確認するか、サイドバーから別のオブジェクトを選択してください。',
156
+ allRecords: 'すべてのレコード',
157
+ exitDesignMode: 'デザインモードを終了',
158
+ enterDesignMode: 'デザインモードに入る',
159
+ metadataInspector: 'メタデータインスペクター',
160
+ editView: 'ビューを編集',
161
+ addView: 'ビューを追加',
162
+ designTools: 'デザインツール',
163
+ new: '新規',
164
+ },
165
+ localeSwitcher: {
166
+ label: '言語',
167
+ },
168
+ },
101
169
  errors: {
102
170
  networkError: 'ネットワークエラーです。接続を確認してください。',
103
171
  serverError: 'サーバーエラーです。後でもう一度お試しください。',
package/src/locales/ko.ts CHANGED
@@ -98,6 +98,74 @@ const ko = {
98
98
  saveLayout: '레이아웃 저장',
99
99
  resetLayout: '레이아웃 초기화',
100
100
  },
101
+ console: {
102
+ title: 'ObjectStack 콘솔',
103
+ initializing: '애플리케이션 초기화 중...',
104
+ loadingSteps: {
105
+ connecting: '데이터 소스에 연결 중',
106
+ loadingConfig: '설정 로드 중',
107
+ preparingWorkspace: '워크스페이스 준비 중',
108
+ },
109
+ shortcuts: {
110
+ title: '키보드 단축키',
111
+ description: '사용 가능한 모든 키보드 단축키 빠른 참조.',
112
+ groups: { general: '일반', navigation: '탐색', dataViews: '데이터 뷰', preferences: '환경설정' },
113
+ openCommandPalette: '명령 팔레트 열기',
114
+ showShortcuts: '키보드 단축키 표시',
115
+ closeDialog: '대화상자 / 패널 닫기',
116
+ toggleSidebar: '사이드바 전환',
117
+ focusSearch: '검색에 포커스',
118
+ createRecord: '새 레코드 생성',
119
+ refreshData: '데이터 새로고침',
120
+ editRecord: '선택한 레코드 편집',
121
+ toggleDarkMode: '다크 모드 전환',
122
+ },
123
+ commandPalette: {
124
+ placeholder: '명령어를 입력하거나 검색...',
125
+ noResults: '결과를 찾을 수 없습니다.',
126
+ objects: '객체',
127
+ dashboards: '대시보드',
128
+ pages: '페이지',
129
+ reports: '보고서',
130
+ switchApp: '앱 전환',
131
+ current: '현재',
132
+ preferences: '환경설정',
133
+ lightTheme: '라이트 테마',
134
+ darkTheme: '다크 테마',
135
+ systemTheme: '시스템 테마',
136
+ actions: '작업',
137
+ openFullSearch: '전체 검색 페이지 열기',
138
+ },
139
+ errors: {
140
+ somethingWentWrong: '문제가 발생했습니다',
141
+ unexpectedError: '이 뷰를 렌더링하는 중 예기치 않은 오류가 발생했습니다.',
142
+ tryAgain: '다시 시도',
143
+ goHome: '홈으로 이동',
144
+ errorDetails: '오류 상세정보 (개발 모드 전용)',
145
+ },
146
+ theme: {
147
+ toggle: '테마 전환',
148
+ light: '라이트',
149
+ dark: '다크',
150
+ system: '시스템',
151
+ },
152
+ objectView: {
153
+ objectNotFound: '객체를 찾을 수 없음',
154
+ objectNotFoundDescription: '객체 "{{objectName}}"이(가) 현재 구성에 존재하지 않습니다.',
155
+ objectNotFoundHint: '앱 탐색 설정을 확인하거나 사이드바에서 다른 객체를 선택하세요.',
156
+ allRecords: '모든 레코드',
157
+ exitDesignMode: '디자인 모드 종료',
158
+ enterDesignMode: '디자인 모드 시작',
159
+ metadataInspector: '메타데이터 인스펙터',
160
+ editView: '뷰 편집',
161
+ addView: '뷰 추가',
162
+ designTools: '디자인 도구',
163
+ new: '새로 만들기',
164
+ },
165
+ localeSwitcher: {
166
+ label: '언어',
167
+ },
168
+ },
101
169
  errors: {
102
170
  networkError: '네트워크 오류입니다. 연결을 확인해주세요.',
103
171
  serverError: '서버 오류입니다. 나중에 다시 시도해주세요.',
package/src/locales/pt.ts CHANGED
@@ -98,6 +98,74 @@ const pt = {
98
98
  saveLayout: 'Salvar layout',
99
99
  resetLayout: 'Redefinir layout',
100
100
  },
101
+ console: {
102
+ title: 'Console ObjectStack',
103
+ initializing: 'Inicializando aplicação...',
104
+ loadingSteps: {
105
+ connecting: 'Conectando à fonte de dados',
106
+ loadingConfig: 'Carregando configuração',
107
+ preparingWorkspace: 'Preparando espaço de trabalho',
108
+ },
109
+ shortcuts: {
110
+ title: 'Atalhos de teclado',
111
+ description: 'Referência rápida de todos os atalhos de teclado disponíveis.',
112
+ groups: { general: 'Geral', navigation: 'Navegação', dataViews: 'Visualizações de dados', preferences: 'Preferências' },
113
+ openCommandPalette: 'Abrir paleta de comandos',
114
+ showShortcuts: 'Mostrar atalhos de teclado',
115
+ closeDialog: 'Fechar diálogo / painel',
116
+ toggleSidebar: 'Alternar barra lateral',
117
+ focusSearch: 'Focar na pesquisa',
118
+ createRecord: 'Criar novo registro',
119
+ refreshData: 'Atualizar dados',
120
+ editRecord: 'Editar registro selecionado',
121
+ toggleDarkMode: 'Alternar modo escuro',
122
+ },
123
+ commandPalette: {
124
+ placeholder: 'Digite um comando ou pesquise...',
125
+ noResults: 'Nenhum resultado encontrado.',
126
+ objects: 'Objetos',
127
+ dashboards: 'Painéis',
128
+ pages: 'Páginas',
129
+ reports: 'Relatórios',
130
+ switchApp: 'Trocar aplicativo',
131
+ current: 'Atual',
132
+ preferences: 'Preferências',
133
+ lightTheme: 'Tema claro',
134
+ darkTheme: 'Tema escuro',
135
+ systemTheme: 'Tema do sistema',
136
+ actions: 'Ações',
137
+ openFullSearch: 'Abrir página de pesquisa completa',
138
+ },
139
+ errors: {
140
+ somethingWentWrong: 'Algo deu errado',
141
+ unexpectedError: 'Ocorreu um erro inesperado ao renderizar esta visualização.',
142
+ tryAgain: 'Tentar novamente',
143
+ goHome: 'Ir para início',
144
+ errorDetails: 'Detalhes do erro (somente desenvolvimento)',
145
+ },
146
+ theme: {
147
+ toggle: 'Alternar tema',
148
+ light: 'Claro',
149
+ dark: 'Escuro',
150
+ system: 'Sistema',
151
+ },
152
+ objectView: {
153
+ objectNotFound: 'Objeto não encontrado',
154
+ objectNotFoundDescription: 'O objeto "{{objectName}}" não existe na configuração atual.',
155
+ objectNotFoundHint: 'Verifique as configurações de navegação do seu aplicativo ou selecione um objeto diferente na barra lateral.',
156
+ allRecords: 'Todos os registros',
157
+ exitDesignMode: 'Sair do modo de design',
158
+ enterDesignMode: 'Entrar no modo de design',
159
+ metadataInspector: 'Inspetor de metadados',
160
+ editView: 'Editar visualização',
161
+ addView: 'Adicionar visualização',
162
+ designTools: 'Ferramentas de design',
163
+ new: 'Novo',
164
+ },
165
+ localeSwitcher: {
166
+ label: 'Idioma',
167
+ },
168
+ },
101
169
  errors: {
102
170
  networkError: 'Erro de rede. Por favor, verifique sua conexão.',
103
171
  serverError: 'Erro no servidor. Por favor, tente novamente mais tarde.',
package/src/locales/ru.ts CHANGED
@@ -98,6 +98,74 @@ const ru = {
98
98
  saveLayout: 'Сохранить макет',
99
99
  resetLayout: 'Сбросить макет',
100
100
  },
101
+ console: {
102
+ title: 'Консоль ObjectStack',
103
+ initializing: 'Инициализация приложения...',
104
+ loadingSteps: {
105
+ connecting: 'Подключение к источнику данных',
106
+ loadingConfig: 'Загрузка конфигурации',
107
+ preparingWorkspace: 'Подготовка рабочего пространства',
108
+ },
109
+ shortcuts: {
110
+ title: 'Горячие клавиши',
111
+ description: 'Краткий справочник по всем доступным горячим клавишам.',
112
+ groups: { general: 'Общие', navigation: 'Навигация', dataViews: 'Представления данных', preferences: 'Настройки' },
113
+ openCommandPalette: 'Открыть палитру команд',
114
+ showShortcuts: 'Показать горячие клавиши',
115
+ closeDialog: 'Закрыть диалог / панель',
116
+ toggleSidebar: 'Переключить боковую панель',
117
+ focusSearch: 'Фокус на поиске',
118
+ createRecord: 'Создать новую запись',
119
+ refreshData: 'Обновить данные',
120
+ editRecord: 'Редактировать выбранную запись',
121
+ toggleDarkMode: 'Переключить тёмный режим',
122
+ },
123
+ commandPalette: {
124
+ placeholder: 'Введите команду или поиск...',
125
+ noResults: 'Результаты не найдены.',
126
+ objects: 'Объекты',
127
+ dashboards: 'Панели мониторинга',
128
+ pages: 'Страницы',
129
+ reports: 'Отчёты',
130
+ switchApp: 'Переключить приложение',
131
+ current: 'Текущее',
132
+ preferences: 'Настройки',
133
+ lightTheme: 'Светлая тема',
134
+ darkTheme: 'Тёмная тема',
135
+ systemTheme: 'Системная тема',
136
+ actions: 'Действия',
137
+ openFullSearch: 'Открыть полную страницу поиска',
138
+ },
139
+ errors: {
140
+ somethingWentWrong: 'Что-то пошло не так',
141
+ unexpectedError: 'При отображении этого представления произошла непредвиденная ошибка.',
142
+ tryAgain: 'Попробовать снова',
143
+ goHome: 'На главную',
144
+ errorDetails: 'Подробности ошибки (только для разработки)',
145
+ },
146
+ theme: {
147
+ toggle: 'Переключить тему',
148
+ light: 'Светлая',
149
+ dark: 'Тёмная',
150
+ system: 'Системная',
151
+ },
152
+ objectView: {
153
+ objectNotFound: 'Объект не найден',
154
+ objectNotFoundDescription: 'Объект «{{objectName}}» не существует в текущей конфигурации.',
155
+ objectNotFoundHint: 'Проверьте настройки навигации приложения или выберите другой объект на боковой панели.',
156
+ allRecords: 'Все записи',
157
+ exitDesignMode: 'Выйти из режима дизайна',
158
+ enterDesignMode: 'Войти в режим дизайна',
159
+ metadataInspector: 'Инспектор метаданных',
160
+ editView: 'Редактировать представление',
161
+ addView: 'Добавить представление',
162
+ designTools: 'Инструменты дизайна',
163
+ new: 'Создать',
164
+ },
165
+ localeSwitcher: {
166
+ label: 'Язык',
167
+ },
168
+ },
101
169
  errors: {
102
170
  networkError: 'Ошибка сети. Проверьте подключение к интернету.',
103
171
  serverError: 'Ошибка сервера. Попробуйте позже.',
package/src/locales/zh.ts CHANGED
@@ -98,6 +98,79 @@ const zh = {
98
98
  saveLayout: '保存布局',
99
99
  resetLayout: '重置布局',
100
100
  },
101
+ console: {
102
+ title: 'ObjectStack 控制台',
103
+ initializing: '正在初始化应用程序...',
104
+ loadingSteps: {
105
+ connecting: '正在连接数据源',
106
+ loadingConfig: '正在加载配置',
107
+ preparingWorkspace: '正在准备工作区',
108
+ },
109
+ shortcuts: {
110
+ title: '键盘快捷键',
111
+ description: '所有可用键盘快捷键的快速参考。',
112
+ groups: {
113
+ general: '通用',
114
+ navigation: '导航',
115
+ dataViews: '数据视图',
116
+ preferences: '偏好设置',
117
+ },
118
+ openCommandPalette: '打开命令面板',
119
+ showShortcuts: '显示键盘快捷键',
120
+ closeDialog: '关闭对话框/面板',
121
+ toggleSidebar: '切换侧边栏',
122
+ focusSearch: '聚焦搜索',
123
+ createRecord: '新建记录',
124
+ refreshData: '刷新数据',
125
+ editRecord: '编辑选中记录',
126
+ toggleDarkMode: '切换深色模式',
127
+ },
128
+ commandPalette: {
129
+ placeholder: '输入命令或搜索...',
130
+ noResults: '未找到结果。',
131
+ objects: '对象',
132
+ dashboards: '仪表盘',
133
+ pages: '页面',
134
+ reports: '报表',
135
+ switchApp: '切换应用',
136
+ current: '当前',
137
+ preferences: '偏好设置',
138
+ lightTheme: '浅色主题',
139
+ darkTheme: '深色主题',
140
+ systemTheme: '系统主题',
141
+ actions: '操作',
142
+ openFullSearch: '打开完整搜索页面',
143
+ },
144
+ errors: {
145
+ somethingWentWrong: '出错了',
146
+ unexpectedError: '渲染此视图时发生意外错误。',
147
+ tryAgain: '重试',
148
+ goHome: '返回首页',
149
+ errorDetails: '错误详情(仅开发模式)',
150
+ },
151
+ theme: {
152
+ toggle: '切换主题',
153
+ light: '浅色',
154
+ dark: '深色',
155
+ system: '系统',
156
+ },
157
+ objectView: {
158
+ objectNotFound: '未找到对象',
159
+ objectNotFoundDescription: '对象"{{objectName}}"在当前配置中不存在。',
160
+ objectNotFoundHint: '请检查您的应用导航设置或从侧边栏选择其他对象。',
161
+ allRecords: '所有记录',
162
+ exitDesignMode: '退出设计模式',
163
+ enterDesignMode: '进入设计模式',
164
+ metadataInspector: '元数据检查器',
165
+ editView: '编辑视图',
166
+ addView: '添加视图',
167
+ designTools: '设计工具',
168
+ new: '新建',
169
+ },
170
+ localeSwitcher: {
171
+ label: '语言',
172
+ },
173
+ },
101
174
  errors: {
102
175
  networkError: '网络错误,请检查网络连接。',
103
176
  serverError: '服务器错误,请稍后重试。',
package/src/provider.tsx CHANGED
@@ -26,6 +26,24 @@ export interface I18nProviderProps {
26
26
  config?: I18nConfig;
27
27
  /** Pre-created i18next instance (overrides config) */
28
28
  instance?: I18nInstance;
29
+ /**
30
+ * Dynamic language pack loader (v2.0.7).
31
+ * When set, language packs are loaded lazily instead of being bundled.
32
+ * Should return a translation resource object for the given language code.
33
+ *
34
+ * @example
35
+ * ```tsx
36
+ * <I18nProvider
37
+ * loadLanguage={async (lang) => {
38
+ * const mod = await import(`./locales/${lang}.json`);
39
+ * return mod.default;
40
+ * }}
41
+ * >
42
+ * <App />
43
+ * </I18nProvider>
44
+ * ```
45
+ */
46
+ loadLanguage?: (lang: string) => Promise<Record<string, unknown>>;
29
47
  /** Children to render */
30
48
  children: React.ReactNode;
31
49
  }
@@ -40,7 +58,7 @@ export interface I18nProviderProps {
40
58
  * </I18nProvider>
41
59
  * ```
42
60
  */
43
- export function I18nProvider({ config, instance: externalInstance, children }: I18nProviderProps) {
61
+ export function I18nProvider({ config, instance: externalInstance, loadLanguage, children }: I18nProviderProps) {
44
62
  const i18nInstance = useMemo(
45
63
  () => externalInstance || createI18n(config),
46
64
  [externalInstance, config],
@@ -69,12 +87,17 @@ export function I18nProvider({ config, instance: externalInstance, children }: I
69
87
  () => ({
70
88
  language,
71
89
  changeLanguage: async (lang: string) => {
90
+ // Dynamic language pack loading (v2.0.7)
91
+ if (loadLanguage && !i18nInstance.hasResourceBundle(lang, 'translation')) {
92
+ const resources = await loadLanguage(lang);
93
+ i18nInstance.addResourceBundle(lang, 'translation', resources, true, true);
94
+ }
72
95
  await i18nInstance.changeLanguage(lang);
73
96
  },
74
97
  direction,
75
98
  i18n: i18nInstance,
76
99
  }),
77
- [language, direction, i18nInstance],
100
+ [language, direction, i18nInstance, loadLanguage],
78
101
  );
79
102
 
80
103
  return React.createElement(
@@ -8,3 +8,14 @@ export {
8
8
  type CurrencyFormatOptions,
9
9
  type NumberFormatOptions,
10
10
  } from './formatting';
11
+
12
+ export {
13
+ resolvePlural,
14
+ formatDateSpec,
15
+ formatNumberSpec,
16
+ applyLocaleConfig,
17
+ type SpecPluralRule,
18
+ type SpecDateFormat,
19
+ type SpecNumberFormat,
20
+ type SpecLocaleConfig,
21
+ } from './spec-formatters';
@@ -0,0 +1,220 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ /**
10
+ * @object-ui/i18n - Spec-aligned i18n utilities
11
+ *
12
+ * Runtime consumers for @objectstack/spec v2.0.7 i18n types:
13
+ * - PluralRuleSchema → resolvePlural()
14
+ * - DateFormatSchema → formatDateSpec()
15
+ * - NumberFormatSchema → formatNumberSpec()
16
+ * - LocaleConfigSchema → applyLocaleConfig()
17
+ *
18
+ * @module spec-formatters
19
+ */
20
+
21
+ // ============================================================================
22
+ // PluralRuleSchema Consumer
23
+ // ============================================================================
24
+
25
+ /**
26
+ * Spec-aligned PluralRule (mirrors @objectstack/spec PluralRuleSchema).
27
+ */
28
+ export interface SpecPluralRule {
29
+ /** Translation key */
30
+ key: string;
31
+ /** Form for zero items */
32
+ zero?: string;
33
+ /** Form for exactly one item */
34
+ one?: string;
35
+ /** Form for exactly two items */
36
+ two?: string;
37
+ /** Form for few items (language-specific) */
38
+ few?: string;
39
+ /** Form for many items (language-specific) */
40
+ many?: string;
41
+ /** Default/fallback form (required) */
42
+ other: string;
43
+ }
44
+
45
+ /**
46
+ * Resolve a plural form based on count, following CLDR plural rules.
47
+ * Uses the Intl.PluralRules API for correct locale-aware pluralization.
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * const rule: SpecPluralRule = {
52
+ * key: 'items.count',
53
+ * zero: 'No items',
54
+ * one: '{count} item',
55
+ * other: '{count} items',
56
+ * };
57
+ * resolvePlural(rule, 0, 'en'); // → 'No items'
58
+ * resolvePlural(rule, 1, 'en'); // → '1 item'
59
+ * resolvePlural(rule, 5, 'en'); // → '5 items'
60
+ * ```
61
+ */
62
+ export function resolvePlural(
63
+ rule: SpecPluralRule,
64
+ count: number,
65
+ locale = 'en',
66
+ ): string {
67
+ const pr = new Intl.PluralRules(locale);
68
+ const category = pr.select(count);
69
+
70
+ // Prefer the explicit form, fall back to 'other'
71
+ const template =
72
+ (category === 'zero' && rule.zero) ||
73
+ (category === 'one' && rule.one) ||
74
+ (category === 'two' && rule.two) ||
75
+ (category === 'few' && rule.few) ||
76
+ (category === 'many' && rule.many) ||
77
+ rule.other;
78
+
79
+ // Replace {count} placeholder
80
+ return template.replace(/\{count\}/g, String(count));
81
+ }
82
+
83
+ // ============================================================================
84
+ // DateFormatSchema Consumer
85
+ // ============================================================================
86
+
87
+ /**
88
+ * Spec-aligned DateFormat (mirrors @objectstack/spec DateFormatSchema).
89
+ */
90
+ export interface SpecDateFormat {
91
+ dateStyle?: 'full' | 'long' | 'medium' | 'short';
92
+ timeStyle?: 'full' | 'long' | 'medium' | 'short';
93
+ timeZone?: string;
94
+ hour12?: boolean;
95
+ }
96
+
97
+ /**
98
+ * Format a date using @objectstack/spec DateFormatSchema configuration.
99
+ *
100
+ * @example
101
+ * ```ts
102
+ * formatDateSpec(new Date(), {
103
+ * dateStyle: 'medium',
104
+ * timeStyle: 'short',
105
+ * timeZone: 'America/New_York',
106
+ * }, 'en-US');
107
+ * ```
108
+ */
109
+ export function formatDateSpec(
110
+ date: Date | string | number,
111
+ format: SpecDateFormat,
112
+ locale = 'en',
113
+ ): string {
114
+ const d = date instanceof Date ? date : new Date(date);
115
+ if (isNaN(d.getTime())) return String(date);
116
+
117
+ const options: Intl.DateTimeFormatOptions = {};
118
+ if (format.dateStyle) options.dateStyle = format.dateStyle;
119
+ if (format.timeStyle) options.timeStyle = format.timeStyle;
120
+ if (format.timeZone) options.timeZone = format.timeZone;
121
+ if (format.hour12 !== undefined) options.hour12 = format.hour12;
122
+
123
+ return new Intl.DateTimeFormat(locale, options).format(d);
124
+ }
125
+
126
+ // ============================================================================
127
+ // NumberFormatSchema Consumer
128
+ // ============================================================================
129
+
130
+ /**
131
+ * Spec-aligned NumberFormat (mirrors @objectstack/spec NumberFormatSchema).
132
+ */
133
+ export interface SpecNumberFormat {
134
+ style?: 'currency' | 'percent' | 'decimal' | 'unit';
135
+ currency?: string;
136
+ unit?: string;
137
+ minimumFractionDigits?: number;
138
+ maximumFractionDigits?: number;
139
+ useGrouping?: boolean;
140
+ }
141
+
142
+ /**
143
+ * Format a number using @objectstack/spec NumberFormatSchema configuration.
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * formatNumberSpec(1234.56, {
148
+ * style: 'currency',
149
+ * currency: 'USD',
150
+ * maximumFractionDigits: 2,
151
+ * }, 'en-US'); // → '$1,234.56'
152
+ * ```
153
+ */
154
+ export function formatNumberSpec(
155
+ value: number,
156
+ format: SpecNumberFormat,
157
+ locale = 'en',
158
+ ): string {
159
+ const options: Intl.NumberFormatOptions = {
160
+ style: format.style || 'decimal',
161
+ };
162
+
163
+ if (format.currency) options.currency = format.currency;
164
+ if (format.unit) options.unit = format.unit;
165
+ if (format.minimumFractionDigits !== undefined) options.minimumFractionDigits = format.minimumFractionDigits;
166
+ if (format.maximumFractionDigits !== undefined) options.maximumFractionDigits = format.maximumFractionDigits;
167
+ if (format.useGrouping !== undefined) options.useGrouping = format.useGrouping;
168
+
169
+ return new Intl.NumberFormat(locale, options).format(value);
170
+ }
171
+
172
+ // ============================================================================
173
+ // LocaleConfigSchema Consumer
174
+ // ============================================================================
175
+
176
+ /**
177
+ * Spec-aligned LocaleConfig (mirrors @objectstack/spec LocaleConfigSchema).
178
+ */
179
+ export interface SpecLocaleConfig {
180
+ /** BCP 47 language code (e.g., 'en-US', 'zh-CN') */
181
+ code: string;
182
+ /** Fallback locale chain */
183
+ fallbackChain?: string[];
184
+ /** Text direction */
185
+ direction?: 'ltr' | 'rtl';
186
+ /** Number formatting defaults */
187
+ numberFormat?: SpecNumberFormat;
188
+ /** Date formatting defaults */
189
+ dateFormat?: SpecDateFormat;
190
+ }
191
+
192
+ /**
193
+ * Apply a LocaleConfigSchema to configure i18n formatting defaults.
194
+ * Returns resolved formatting functions bound to the locale config.
195
+ *
196
+ * @example
197
+ * ```ts
198
+ * const locale = applyLocaleConfig({
199
+ * code: 'zh-CN',
200
+ * direction: 'ltr',
201
+ * numberFormat: { style: 'decimal', useGrouping: true },
202
+ * dateFormat: { dateStyle: 'medium' },
203
+ * });
204
+ * locale.formatDate(new Date()); // Chinese medium date
205
+ * locale.formatNumber(1234.5); // 1,234.5
206
+ * ```
207
+ */
208
+ export function applyLocaleConfig(config: SpecLocaleConfig) {
209
+ return {
210
+ code: config.code,
211
+ direction: config.direction || 'ltr',
212
+ fallbackChain: config.fallbackChain || [],
213
+ formatDate: (date: Date | string | number, overrides?: Partial<SpecDateFormat>) =>
214
+ formatDateSpec(date, { ...config.dateFormat, ...overrides }, config.code),
215
+ formatNumber: (value: number, overrides?: Partial<SpecNumberFormat>) =>
216
+ formatNumberSpec(value, { ...config.numberFormat, ...overrides }, config.code),
217
+ resolvePlural: (rule: SpecPluralRule, count: number) =>
218
+ resolvePlural(rule, count, config.code),
219
+ };
220
+ }