@object-ui/i18n 0.5.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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +19 -0
- package/README.md +115 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/locales/ar.d.ts +73 -0
- package/dist/locales/ar.js +68 -0
- package/dist/locales/de.d.ts +73 -0
- package/dist/locales/de.js +73 -0
- package/dist/locales/en.d.ts +73 -0
- package/dist/locales/en.js +73 -0
- package/dist/locales/es.d.ts +73 -0
- package/dist/locales/es.js +68 -0
- package/dist/locales/fr.d.ts +73 -0
- package/dist/locales/fr.js +73 -0
- package/dist/locales/index.d.ts +730 -0
- package/dist/locales/ja.d.ts +73 -0
- package/dist/locales/ja.js +68 -0
- package/dist/locales/ko.d.ts +73 -0
- package/dist/locales/ko.js +68 -0
- package/dist/locales/pt.d.ts +73 -0
- package/dist/locales/pt.js +68 -0
- package/dist/locales/ru.d.ts +73 -0
- package/dist/locales/ru.js +68 -0
- package/dist/locales/zh.d.ts +73 -0
- package/dist/locales/zh.js +73 -0
- package/dist/provider.d.ts +19 -1
- package/dist/provider.js +8 -3
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/spec-formatters.d.ts +140 -0
- package/dist/utils/spec-formatters.js +118 -0
- package/package.json +5 -4
- package/src/__tests__/provider.test.tsx +117 -0
- package/src/__tests__/spec-formatters.test.ts +208 -0
- package/src/index.ts +12 -0
- package/src/locales/ar.ts +68 -0
- package/src/locales/de.ts +73 -0
- package/src/locales/en.ts +73 -0
- package/src/locales/es.ts +68 -0
- package/src/locales/fr.ts +73 -0
- package/src/locales/ja.ts +68 -0
- package/src/locales/ko.ts +68 -0
- package/src/locales/pt.ts +68 -0
- package/src/locales/ru.ts +68 -0
- package/src/locales/zh.ts +73 -0
- package/src/provider.tsx +26 -3
- package/src/utils/index.ts +11 -0
- package/src/utils/spec-formatters.ts +220 -0
- package/tsconfig.tsbuildinfo +1 -1
package/dist/locales/zh.d.ts
CHANGED
|
@@ -98,6 +98,79 @@ declare const zh: {
|
|
|
98
98
|
readonly saveLayout: "保存布局";
|
|
99
99
|
readonly resetLayout: "重置布局";
|
|
100
100
|
};
|
|
101
|
+
readonly console: {
|
|
102
|
+
readonly title: "ObjectStack 控制台";
|
|
103
|
+
readonly initializing: "正在初始化应用程序...";
|
|
104
|
+
readonly loadingSteps: {
|
|
105
|
+
readonly connecting: "正在连接数据源";
|
|
106
|
+
readonly loadingConfig: "正在加载配置";
|
|
107
|
+
readonly preparingWorkspace: "正在准备工作区";
|
|
108
|
+
};
|
|
109
|
+
readonly shortcuts: {
|
|
110
|
+
readonly title: "键盘快捷键";
|
|
111
|
+
readonly description: "所有可用键盘快捷键的快速参考。";
|
|
112
|
+
readonly groups: {
|
|
113
|
+
readonly general: "通用";
|
|
114
|
+
readonly navigation: "导航";
|
|
115
|
+
readonly dataViews: "数据视图";
|
|
116
|
+
readonly preferences: "偏好设置";
|
|
117
|
+
};
|
|
118
|
+
readonly openCommandPalette: "打开命令面板";
|
|
119
|
+
readonly showShortcuts: "显示键盘快捷键";
|
|
120
|
+
readonly closeDialog: "关闭对话框/面板";
|
|
121
|
+
readonly toggleSidebar: "切换侧边栏";
|
|
122
|
+
readonly focusSearch: "聚焦搜索";
|
|
123
|
+
readonly createRecord: "新建记录";
|
|
124
|
+
readonly refreshData: "刷新数据";
|
|
125
|
+
readonly editRecord: "编辑选中记录";
|
|
126
|
+
readonly toggleDarkMode: "切换深色模式";
|
|
127
|
+
};
|
|
128
|
+
readonly commandPalette: {
|
|
129
|
+
readonly placeholder: "输入命令或搜索...";
|
|
130
|
+
readonly noResults: "未找到结果。";
|
|
131
|
+
readonly objects: "对象";
|
|
132
|
+
readonly dashboards: "仪表盘";
|
|
133
|
+
readonly pages: "页面";
|
|
134
|
+
readonly reports: "报表";
|
|
135
|
+
readonly switchApp: "切换应用";
|
|
136
|
+
readonly current: "当前";
|
|
137
|
+
readonly preferences: "偏好设置";
|
|
138
|
+
readonly lightTheme: "浅色主题";
|
|
139
|
+
readonly darkTheme: "深色主题";
|
|
140
|
+
readonly systemTheme: "系统主题";
|
|
141
|
+
readonly actions: "操作";
|
|
142
|
+
readonly openFullSearch: "打开完整搜索页面";
|
|
143
|
+
};
|
|
144
|
+
readonly errors: {
|
|
145
|
+
readonly somethingWentWrong: "出错了";
|
|
146
|
+
readonly unexpectedError: "渲染此视图时发生意外错误。";
|
|
147
|
+
readonly tryAgain: "重试";
|
|
148
|
+
readonly goHome: "返回首页";
|
|
149
|
+
readonly errorDetails: "错误详情(仅开发模式)";
|
|
150
|
+
};
|
|
151
|
+
readonly theme: {
|
|
152
|
+
readonly toggle: "切换主题";
|
|
153
|
+
readonly light: "浅色";
|
|
154
|
+
readonly dark: "深色";
|
|
155
|
+
readonly system: "系统";
|
|
156
|
+
};
|
|
157
|
+
readonly objectView: {
|
|
158
|
+
readonly objectNotFound: "未找到对象";
|
|
159
|
+
readonly objectNotFoundDescription: "对象\"{{objectName}}\"在当前配置中不存在。";
|
|
160
|
+
readonly objectNotFoundHint: "请检查您的应用导航设置或从侧边栏选择其他对象。";
|
|
161
|
+
readonly allRecords: "所有记录";
|
|
162
|
+
readonly exitDesignMode: "退出设计模式";
|
|
163
|
+
readonly enterDesignMode: "进入设计模式";
|
|
164
|
+
readonly metadataInspector: "元数据检查器";
|
|
165
|
+
readonly editView: "编辑视图";
|
|
166
|
+
readonly addView: "添加视图";
|
|
167
|
+
readonly designTools: "设计工具";
|
|
168
|
+
readonly new: "新建";
|
|
169
|
+
};
|
|
170
|
+
readonly localeSwitcher: {
|
|
171
|
+
readonly label: "语言";
|
|
172
|
+
};
|
|
173
|
+
};
|
|
101
174
|
readonly errors: {
|
|
102
175
|
readonly networkError: "网络错误,请检查网络连接。";
|
|
103
176
|
readonly serverError: "服务器错误,请稍后重试。";
|
package/dist/locales/zh.js
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/dist/provider.d.ts
CHANGED
|
@@ -21,6 +21,24 @@ export interface I18nProviderProps {
|
|
|
21
21
|
config?: I18nConfig;
|
|
22
22
|
/** Pre-created i18next instance (overrides config) */
|
|
23
23
|
instance?: I18nInstance;
|
|
24
|
+
/**
|
|
25
|
+
* Dynamic language pack loader (v2.0.7).
|
|
26
|
+
* When set, language packs are loaded lazily instead of being bundled.
|
|
27
|
+
* Should return a translation resource object for the given language code.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```tsx
|
|
31
|
+
* <I18nProvider
|
|
32
|
+
* loadLanguage={async (lang) => {
|
|
33
|
+
* const mod = await import(`./locales/${lang}.json`);
|
|
34
|
+
* return mod.default;
|
|
35
|
+
* }}
|
|
36
|
+
* >
|
|
37
|
+
* <App />
|
|
38
|
+
* </I18nProvider>
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
loadLanguage?: (lang: string) => Promise<Record<string, unknown>>;
|
|
24
42
|
/** Children to render */
|
|
25
43
|
children: React.ReactNode;
|
|
26
44
|
}
|
|
@@ -34,7 +52,7 @@ export interface I18nProviderProps {
|
|
|
34
52
|
* </I18nProvider>
|
|
35
53
|
* ```
|
|
36
54
|
*/
|
|
37
|
-
export declare function I18nProvider({ config, instance: externalInstance, children }: I18nProviderProps): React.FunctionComponentElement<React.ProviderProps<I18nContextValue | null>>;
|
|
55
|
+
export declare function I18nProvider({ config, instance: externalInstance, loadLanguage, children }: I18nProviderProps): React.FunctionComponentElement<React.ProviderProps<I18nContextValue | null>>;
|
|
38
56
|
/**
|
|
39
57
|
* Hook to access Object UI i18n context
|
|
40
58
|
*
|
package/dist/provider.js
CHANGED
|
@@ -17,8 +17,8 @@ const ObjectI18nContext = createContext(null);
|
|
|
17
17
|
* </I18nProvider>
|
|
18
18
|
* ```
|
|
19
19
|
*/
|
|
20
|
-
export function I18nProvider({ config, instance: externalInstance, children }) {
|
|
21
|
-
const i18nInstance = useMemo(() => externalInstance || createI18n(config), [externalInstance]);
|
|
20
|
+
export function I18nProvider({ config, instance: externalInstance, loadLanguage, children }) {
|
|
21
|
+
const i18nInstance = useMemo(() => externalInstance || createI18n(config), [externalInstance, config]);
|
|
22
22
|
const [language, setLanguage] = useState(i18nInstance.language || 'en');
|
|
23
23
|
const direction = getDirection(language);
|
|
24
24
|
useEffect(() => {
|
|
@@ -38,11 +38,16 @@ export function I18nProvider({ config, instance: externalInstance, children }) {
|
|
|
38
38
|
const contextValue = useMemo(() => ({
|
|
39
39
|
language,
|
|
40
40
|
changeLanguage: async (lang) => {
|
|
41
|
+
// Dynamic language pack loading (v2.0.7)
|
|
42
|
+
if (loadLanguage && !i18nInstance.hasResourceBundle(lang, 'translation')) {
|
|
43
|
+
const resources = await loadLanguage(lang);
|
|
44
|
+
i18nInstance.addResourceBundle(lang, 'translation', resources, true, true);
|
|
45
|
+
}
|
|
41
46
|
await i18nInstance.changeLanguage(lang);
|
|
42
47
|
},
|
|
43
48
|
direction,
|
|
44
49
|
i18n: i18nInstance,
|
|
45
|
-
}), [language, direction, i18nInstance]);
|
|
50
|
+
}), [language, direction, i18nInstance, loadLanguage]);
|
|
46
51
|
return React.createElement(ObjectI18nContext.Provider, { value: contextValue }, React.createElement(I18nextProvider, { i18n: i18nInstance }, children));
|
|
47
52
|
}
|
|
48
53
|
/**
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
1
|
export { formatDate, formatDateTime, formatRelativeTime, formatCurrency, formatNumber, type DateFormatOptions, type CurrencyFormatOptions, type NumberFormatOptions, } from './formatting';
|
|
2
|
+
export { resolvePlural, formatDateSpec, formatNumberSpec, applyLocaleConfig, type SpecPluralRule, type SpecDateFormat, type SpecNumberFormat, type SpecLocaleConfig, } from './spec-formatters';
|
package/dist/utils/index.js
CHANGED
|
@@ -0,0 +1,140 @@
|
|
|
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
|
+
* @object-ui/i18n - Spec-aligned i18n utilities
|
|
10
|
+
*
|
|
11
|
+
* Runtime consumers for @objectstack/spec v2.0.7 i18n types:
|
|
12
|
+
* - PluralRuleSchema → resolvePlural()
|
|
13
|
+
* - DateFormatSchema → formatDateSpec()
|
|
14
|
+
* - NumberFormatSchema → formatNumberSpec()
|
|
15
|
+
* - LocaleConfigSchema → applyLocaleConfig()
|
|
16
|
+
*
|
|
17
|
+
* @module spec-formatters
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Spec-aligned PluralRule (mirrors @objectstack/spec PluralRuleSchema).
|
|
21
|
+
*/
|
|
22
|
+
export interface SpecPluralRule {
|
|
23
|
+
/** Translation key */
|
|
24
|
+
key: string;
|
|
25
|
+
/** Form for zero items */
|
|
26
|
+
zero?: string;
|
|
27
|
+
/** Form for exactly one item */
|
|
28
|
+
one?: string;
|
|
29
|
+
/** Form for exactly two items */
|
|
30
|
+
two?: string;
|
|
31
|
+
/** Form for few items (language-specific) */
|
|
32
|
+
few?: string;
|
|
33
|
+
/** Form for many items (language-specific) */
|
|
34
|
+
many?: string;
|
|
35
|
+
/** Default/fallback form (required) */
|
|
36
|
+
other: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Resolve a plural form based on count, following CLDR plural rules.
|
|
40
|
+
* Uses the Intl.PluralRules API for correct locale-aware pluralization.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* const rule: SpecPluralRule = {
|
|
45
|
+
* key: 'items.count',
|
|
46
|
+
* zero: 'No items',
|
|
47
|
+
* one: '{count} item',
|
|
48
|
+
* other: '{count} items',
|
|
49
|
+
* };
|
|
50
|
+
* resolvePlural(rule, 0, 'en'); // → 'No items'
|
|
51
|
+
* resolvePlural(rule, 1, 'en'); // → '1 item'
|
|
52
|
+
* resolvePlural(rule, 5, 'en'); // → '5 items'
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export declare function resolvePlural(rule: SpecPluralRule, count: number, locale?: string): string;
|
|
56
|
+
/**
|
|
57
|
+
* Spec-aligned DateFormat (mirrors @objectstack/spec DateFormatSchema).
|
|
58
|
+
*/
|
|
59
|
+
export interface SpecDateFormat {
|
|
60
|
+
dateStyle?: 'full' | 'long' | 'medium' | 'short';
|
|
61
|
+
timeStyle?: 'full' | 'long' | 'medium' | 'short';
|
|
62
|
+
timeZone?: string;
|
|
63
|
+
hour12?: boolean;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Format a date using @objectstack/spec DateFormatSchema configuration.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* formatDateSpec(new Date(), {
|
|
71
|
+
* dateStyle: 'medium',
|
|
72
|
+
* timeStyle: 'short',
|
|
73
|
+
* timeZone: 'America/New_York',
|
|
74
|
+
* }, 'en-US');
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export declare function formatDateSpec(date: Date | string | number, format: SpecDateFormat, locale?: string): string;
|
|
78
|
+
/**
|
|
79
|
+
* Spec-aligned NumberFormat (mirrors @objectstack/spec NumberFormatSchema).
|
|
80
|
+
*/
|
|
81
|
+
export interface SpecNumberFormat {
|
|
82
|
+
style?: 'currency' | 'percent' | 'decimal' | 'unit';
|
|
83
|
+
currency?: string;
|
|
84
|
+
unit?: string;
|
|
85
|
+
minimumFractionDigits?: number;
|
|
86
|
+
maximumFractionDigits?: number;
|
|
87
|
+
useGrouping?: boolean;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Format a number using @objectstack/spec NumberFormatSchema configuration.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* formatNumberSpec(1234.56, {
|
|
95
|
+
* style: 'currency',
|
|
96
|
+
* currency: 'USD',
|
|
97
|
+
* maximumFractionDigits: 2,
|
|
98
|
+
* }, 'en-US'); // → '$1,234.56'
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export declare function formatNumberSpec(value: number, format: SpecNumberFormat, locale?: string): string;
|
|
102
|
+
/**
|
|
103
|
+
* Spec-aligned LocaleConfig (mirrors @objectstack/spec LocaleConfigSchema).
|
|
104
|
+
*/
|
|
105
|
+
export interface SpecLocaleConfig {
|
|
106
|
+
/** BCP 47 language code (e.g., 'en-US', 'zh-CN') */
|
|
107
|
+
code: string;
|
|
108
|
+
/** Fallback locale chain */
|
|
109
|
+
fallbackChain?: string[];
|
|
110
|
+
/** Text direction */
|
|
111
|
+
direction?: 'ltr' | 'rtl';
|
|
112
|
+
/** Number formatting defaults */
|
|
113
|
+
numberFormat?: SpecNumberFormat;
|
|
114
|
+
/** Date formatting defaults */
|
|
115
|
+
dateFormat?: SpecDateFormat;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Apply a LocaleConfigSchema to configure i18n formatting defaults.
|
|
119
|
+
* Returns resolved formatting functions bound to the locale config.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* const locale = applyLocaleConfig({
|
|
124
|
+
* code: 'zh-CN',
|
|
125
|
+
* direction: 'ltr',
|
|
126
|
+
* numberFormat: { style: 'decimal', useGrouping: true },
|
|
127
|
+
* dateFormat: { dateStyle: 'medium' },
|
|
128
|
+
* });
|
|
129
|
+
* locale.formatDate(new Date()); // Chinese medium date
|
|
130
|
+
* locale.formatNumber(1234.5); // 1,234.5
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
export declare function applyLocaleConfig(config: SpecLocaleConfig): {
|
|
134
|
+
code: string;
|
|
135
|
+
direction: "ltr" | "rtl";
|
|
136
|
+
fallbackChain: string[];
|
|
137
|
+
formatDate: (date: Date | string | number, overrides?: Partial<SpecDateFormat>) => string;
|
|
138
|
+
formatNumber: (value: number, overrides?: Partial<SpecNumberFormat>) => string;
|
|
139
|
+
resolvePlural: (rule: SpecPluralRule, count: number) => string;
|
|
140
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
* Resolve a plural form based on count, following CLDR plural rules.
|
|
10
|
+
* Uses the Intl.PluralRules API for correct locale-aware pluralization.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const rule: SpecPluralRule = {
|
|
15
|
+
* key: 'items.count',
|
|
16
|
+
* zero: 'No items',
|
|
17
|
+
* one: '{count} item',
|
|
18
|
+
* other: '{count} items',
|
|
19
|
+
* };
|
|
20
|
+
* resolvePlural(rule, 0, 'en'); // → 'No items'
|
|
21
|
+
* resolvePlural(rule, 1, 'en'); // → '1 item'
|
|
22
|
+
* resolvePlural(rule, 5, 'en'); // → '5 items'
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function resolvePlural(rule, count, locale = 'en') {
|
|
26
|
+
const pr = new Intl.PluralRules(locale);
|
|
27
|
+
const category = pr.select(count);
|
|
28
|
+
// Prefer the explicit form, fall back to 'other'
|
|
29
|
+
const template = (category === 'zero' && rule.zero) ||
|
|
30
|
+
(category === 'one' && rule.one) ||
|
|
31
|
+
(category === 'two' && rule.two) ||
|
|
32
|
+
(category === 'few' && rule.few) ||
|
|
33
|
+
(category === 'many' && rule.many) ||
|
|
34
|
+
rule.other;
|
|
35
|
+
// Replace {count} placeholder
|
|
36
|
+
return template.replace(/\{count\}/g, String(count));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Format a date using @objectstack/spec DateFormatSchema configuration.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* formatDateSpec(new Date(), {
|
|
44
|
+
* dateStyle: 'medium',
|
|
45
|
+
* timeStyle: 'short',
|
|
46
|
+
* timeZone: 'America/New_York',
|
|
47
|
+
* }, 'en-US');
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export function formatDateSpec(date, format, locale = 'en') {
|
|
51
|
+
const d = date instanceof Date ? date : new Date(date);
|
|
52
|
+
if (isNaN(d.getTime()))
|
|
53
|
+
return String(date);
|
|
54
|
+
const options = {};
|
|
55
|
+
if (format.dateStyle)
|
|
56
|
+
options.dateStyle = format.dateStyle;
|
|
57
|
+
if (format.timeStyle)
|
|
58
|
+
options.timeStyle = format.timeStyle;
|
|
59
|
+
if (format.timeZone)
|
|
60
|
+
options.timeZone = format.timeZone;
|
|
61
|
+
if (format.hour12 !== undefined)
|
|
62
|
+
options.hour12 = format.hour12;
|
|
63
|
+
return new Intl.DateTimeFormat(locale, options).format(d);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Format a number using @objectstack/spec NumberFormatSchema configuration.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* formatNumberSpec(1234.56, {
|
|
71
|
+
* style: 'currency',
|
|
72
|
+
* currency: 'USD',
|
|
73
|
+
* maximumFractionDigits: 2,
|
|
74
|
+
* }, 'en-US'); // → '$1,234.56'
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export function formatNumberSpec(value, format, locale = 'en') {
|
|
78
|
+
const options = {
|
|
79
|
+
style: format.style || 'decimal',
|
|
80
|
+
};
|
|
81
|
+
if (format.currency)
|
|
82
|
+
options.currency = format.currency;
|
|
83
|
+
if (format.unit)
|
|
84
|
+
options.unit = format.unit;
|
|
85
|
+
if (format.minimumFractionDigits !== undefined)
|
|
86
|
+
options.minimumFractionDigits = format.minimumFractionDigits;
|
|
87
|
+
if (format.maximumFractionDigits !== undefined)
|
|
88
|
+
options.maximumFractionDigits = format.maximumFractionDigits;
|
|
89
|
+
if (format.useGrouping !== undefined)
|
|
90
|
+
options.useGrouping = format.useGrouping;
|
|
91
|
+
return new Intl.NumberFormat(locale, options).format(value);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Apply a LocaleConfigSchema to configure i18n formatting defaults.
|
|
95
|
+
* Returns resolved formatting functions bound to the locale config.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```ts
|
|
99
|
+
* const locale = applyLocaleConfig({
|
|
100
|
+
* code: 'zh-CN',
|
|
101
|
+
* direction: 'ltr',
|
|
102
|
+
* numberFormat: { style: 'decimal', useGrouping: true },
|
|
103
|
+
* dateFormat: { dateStyle: 'medium' },
|
|
104
|
+
* });
|
|
105
|
+
* locale.formatDate(new Date()); // Chinese medium date
|
|
106
|
+
* locale.formatNumber(1234.5); // 1,234.5
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export function applyLocaleConfig(config) {
|
|
110
|
+
return {
|
|
111
|
+
code: config.code,
|
|
112
|
+
direction: config.direction || 'ltr',
|
|
113
|
+
fallbackChain: config.fallbackChain || [],
|
|
114
|
+
formatDate: (date, overrides) => formatDateSpec(date, { ...config.dateFormat, ...overrides }, config.code),
|
|
115
|
+
formatNumber: (value, overrides) => formatNumberSpec(value, { ...config.numberFormat, ...overrides }, config.code),
|
|
116
|
+
resolvePlural: (rule, count) => resolvePlural(rule, count, config.code),
|
|
117
|
+
};
|
|
118
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/i18n",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"sideEffects": false,
|
|
5
6
|
"license": "MIT",
|
|
6
7
|
"description": "Internationalization (i18n) support for Object UI with 10+ language packs, RTL layout, and date/currency formatting.",
|
|
7
8
|
"homepage": "https://www.objectui.org",
|
|
@@ -27,15 +28,15 @@
|
|
|
27
28
|
}
|
|
28
29
|
},
|
|
29
30
|
"dependencies": {
|
|
30
|
-
"i18next": "^25.8.
|
|
31
|
+
"i18next": "^25.8.7",
|
|
31
32
|
"react-i18next": "^16.5.4"
|
|
32
33
|
},
|
|
33
34
|
"peerDependencies": {
|
|
34
35
|
"react": "^18.0.0 || ^19.0.0"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
|
-
"@types/react": "
|
|
38
|
-
"react": "
|
|
38
|
+
"@types/react": "19.2.13",
|
|
39
|
+
"react": "19.2.4",
|
|
39
40
|
"typescript": "^5.9.3",
|
|
40
41
|
"vitest": "^4.0.18"
|
|
41
42
|
},
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { renderHook, waitFor, act } from '@testing-library/react';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { I18nProvider, useObjectTranslation, useI18nContext } from '../provider';
|
|
5
|
+
import { createI18n } from '../i18n';
|
|
6
|
+
|
|
7
|
+
describe('I18nProvider', () => {
|
|
8
|
+
it('creates i18n instance from config', () => {
|
|
9
|
+
const wrapper = ({ children }: { children: React.ReactNode }) =>
|
|
10
|
+
React.createElement(I18nProvider, { config: { defaultLanguage: 'en', detectBrowserLanguage: false } }, children);
|
|
11
|
+
|
|
12
|
+
const { result } = renderHook(() => useObjectTranslation(), { wrapper });
|
|
13
|
+
|
|
14
|
+
expect(result.current.i18n).toBeDefined();
|
|
15
|
+
expect(result.current.language).toBe('en');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('accepts pre-created instance', () => {
|
|
19
|
+
const instance = createI18n({ defaultLanguage: 'fr', detectBrowserLanguage: false });
|
|
20
|
+
|
|
21
|
+
const wrapper = ({ children }: { children: React.ReactNode }) =>
|
|
22
|
+
React.createElement(I18nProvider, { instance }, children);
|
|
23
|
+
|
|
24
|
+
const { result } = renderHook(() => useObjectTranslation(), { wrapper });
|
|
25
|
+
|
|
26
|
+
expect(result.current.i18n).toBeDefined();
|
|
27
|
+
expect(result.current.language).toBe('fr');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('useObjectTranslation', () => {
|
|
32
|
+
const wrapper = ({ children }: { children: React.ReactNode }) =>
|
|
33
|
+
React.createElement(I18nProvider, { config: { defaultLanguage: 'en', detectBrowserLanguage: false } }, children);
|
|
34
|
+
|
|
35
|
+
it('returns t, language, changeLanguage, direction, i18n', () => {
|
|
36
|
+
const { result } = renderHook(() => useObjectTranslation(), { wrapper });
|
|
37
|
+
|
|
38
|
+
expect(result.current.t).toBeTypeOf('function');
|
|
39
|
+
expect(result.current.language).toBeTypeOf('string');
|
|
40
|
+
expect(result.current.changeLanguage).toBeTypeOf('function');
|
|
41
|
+
expect(result.current.direction).toBeTypeOf('string');
|
|
42
|
+
expect(result.current.i18n).toBeDefined();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('translates keys correctly', () => {
|
|
46
|
+
const { result } = renderHook(() => useObjectTranslation(), { wrapper });
|
|
47
|
+
|
|
48
|
+
expect(result.current.t('common.save')).toBe('Save');
|
|
49
|
+
expect(result.current.t('common.cancel')).toBe('Cancel');
|
|
50
|
+
expect(result.current.t('common.delete')).toBe('Delete');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('returns en as default language', () => {
|
|
54
|
+
const { result } = renderHook(() => useObjectTranslation(), { wrapper });
|
|
55
|
+
|
|
56
|
+
expect(result.current.language).toBe('en');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('works with Chinese language', () => {
|
|
60
|
+
const zhWrapper = ({ children }: { children: React.ReactNode }) =>
|
|
61
|
+
React.createElement(I18nProvider, { config: { defaultLanguage: 'zh', detectBrowserLanguage: false } }, children);
|
|
62
|
+
|
|
63
|
+
const { result } = renderHook(() => useObjectTranslation(), { wrapper: zhWrapper });
|
|
64
|
+
|
|
65
|
+
expect(result.current.language).toBe('zh');
|
|
66
|
+
expect(result.current.t('common.save')).toBe('保存');
|
|
67
|
+
expect(result.current.t('common.cancel')).toBe('取消');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('changeLanguage updates language', async () => {
|
|
71
|
+
const { result } = renderHook(() => useObjectTranslation(), { wrapper });
|
|
72
|
+
|
|
73
|
+
expect(result.current.language).toBe('en');
|
|
74
|
+
|
|
75
|
+
await act(async () => {
|
|
76
|
+
await result.current.changeLanguage('zh');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await waitFor(() => {
|
|
80
|
+
expect(result.current.language).toBe('zh');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('returns RTL direction for Arabic', () => {
|
|
85
|
+
const arWrapper = ({ children }: { children: React.ReactNode }) =>
|
|
86
|
+
React.createElement(I18nProvider, { config: { defaultLanguage: 'ar', detectBrowserLanguage: false } }, children);
|
|
87
|
+
|
|
88
|
+
const { result } = renderHook(() => useObjectTranslation(), { wrapper: arWrapper });
|
|
89
|
+
|
|
90
|
+
expect(result.current.direction).toBe('rtl');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('useI18nContext', () => {
|
|
95
|
+
it('throws when used outside provider', () => {
|
|
96
|
+
// Suppress console.error from React for the expected error
|
|
97
|
+
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
98
|
+
|
|
99
|
+
expect(() => {
|
|
100
|
+
renderHook(() => useI18nContext());
|
|
101
|
+
}).toThrow('useI18nContext must be used within an I18nProvider');
|
|
102
|
+
|
|
103
|
+
spy.mockRestore();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('returns context inside provider', () => {
|
|
107
|
+
const wrapper = ({ children }: { children: React.ReactNode }) =>
|
|
108
|
+
React.createElement(I18nProvider, { config: { defaultLanguage: 'en', detectBrowserLanguage: false } }, children);
|
|
109
|
+
|
|
110
|
+
const { result } = renderHook(() => useI18nContext(), { wrapper });
|
|
111
|
+
|
|
112
|
+
expect(result.current.language).toBe('en');
|
|
113
|
+
expect(result.current.changeLanguage).toBeTypeOf('function');
|
|
114
|
+
expect(result.current.direction).toBe('ltr');
|
|
115
|
+
expect(result.current.i18n).toBeDefined();
|
|
116
|
+
});
|
|
117
|
+
});
|