@object-ui/i18n 3.3.0 → 3.3.2
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/CHANGELOG.md +4 -0
- package/README.md +21 -1
- package/dist/i18n.js +10 -1
- package/dist/locales/ar.d.ts +58 -0
- package/dist/locales/ar.js +58 -0
- package/dist/locales/de.d.ts +58 -0
- package/dist/locales/de.js +58 -0
- package/dist/locales/en.d.ts +78 -0
- package/dist/locales/en.js +78 -0
- package/dist/locales/es.d.ts +58 -0
- package/dist/locales/es.js +58 -0
- package/dist/locales/fr.d.ts +58 -0
- package/dist/locales/fr.js +58 -0
- package/dist/locales/index.d.ts +620 -0
- package/dist/locales/ja.d.ts +58 -0
- package/dist/locales/ja.js +58 -0
- package/dist/locales/ko.d.ts +58 -0
- package/dist/locales/ko.js +58 -0
- package/dist/locales/pt.d.ts +58 -0
- package/dist/locales/pt.js +58 -0
- package/dist/locales/ru.d.ts +58 -0
- package/dist/locales/ru.js +58 -0
- package/dist/locales/zh.d.ts +78 -0
- package/dist/locales/zh.js +78 -0
- package/dist/useObjectLabel.d.ts +46 -0
- package/dist/useObjectLabel.js +79 -12
- package/package.json +30 -7
- package/.turbo/turbo-build.log +0 -4
- package/src/__tests__/i18n.test.ts +0 -255
- package/src/__tests__/provider.test.tsx +0 -117
- package/src/__tests__/spec-formatters.test.ts +0 -208
- package/src/__tests__/useObjectLabel.test.tsx +0 -131
- package/src/i18n.ts +0 -98
- package/src/index.ts +0 -81
- package/src/locales/ar.ts +0 -742
- package/src/locales/de.ts +0 -755
- package/src/locales/en.ts +0 -836
- package/src/locales/es.ts +0 -741
- package/src/locales/fr.ts +0 -755
- package/src/locales/index.ts +0 -42
- package/src/locales/ja.ts +0 -752
- package/src/locales/ko.ts +0 -741
- package/src/locales/pt.ts +0 -741
- package/src/locales/ru.ts +0 -761
- package/src/locales/zh.ts +0 -835
- package/src/provider.tsx +0 -177
- package/src/useObjectLabel.ts +0 -122
- package/src/useSafeTranslation.ts +0 -49
- package/src/utils/formatting.ts +0 -136
- package/src/utils/index.ts +0 -21
- package/src/utils/spec-formatters.ts +0 -220
- package/tsconfig.json +0 -12
- package/tsconfig.tsbuildinfo +0 -1
package/dist/locales/zh.js
CHANGED
|
@@ -559,6 +559,17 @@ const zh = {
|
|
|
559
559
|
ungrouped: '通用',
|
|
560
560
|
deleteConfirmTitle: '删除字段?',
|
|
561
561
|
deleteConfirmMessage: '这将永久删除该字段。此字段中的现有数据将丢失。',
|
|
562
|
+
basicSection: '基础',
|
|
563
|
+
typeSpecificSection: '类型设置',
|
|
564
|
+
advancedSection: '高级',
|
|
565
|
+
typeCategory: {
|
|
566
|
+
text: '文本',
|
|
567
|
+
number: '数字',
|
|
568
|
+
date: '日期时间',
|
|
569
|
+
choice: '选择',
|
|
570
|
+
relation: '关联',
|
|
571
|
+
advanced: '高级',
|
|
572
|
+
},
|
|
562
573
|
},
|
|
563
574
|
},
|
|
564
575
|
console: {
|
|
@@ -575,6 +586,16 @@ const zh = {
|
|
|
575
586
|
loadingConfig: '正在加载配置',
|
|
576
587
|
preparingWorkspace: '正在准备工作区',
|
|
577
588
|
},
|
|
589
|
+
error: {
|
|
590
|
+
connectionFailed: '无法连接到服务器',
|
|
591
|
+
serverUnreachable: '无法访问服务器 {{url}}。',
|
|
592
|
+
checkServer: '请检查网络连接,或确认后端服务正在运行。',
|
|
593
|
+
timeout: '连接超时(10 秒)。',
|
|
594
|
+
},
|
|
595
|
+
actions: {
|
|
596
|
+
retry: '重试',
|
|
597
|
+
retrying: '正在重试…',
|
|
598
|
+
},
|
|
578
599
|
shortcuts: {
|
|
579
600
|
title: '键盘快捷键',
|
|
580
601
|
description: '所有可用键盘快捷键的快速参考。',
|
|
@@ -830,5 +851,62 @@ const zh = {
|
|
|
830
851
|
timeout: '请求超时,请重试。',
|
|
831
852
|
unknown: '发生未知错误。',
|
|
832
853
|
},
|
|
854
|
+
workspace: {
|
|
855
|
+
label: '工作区',
|
|
856
|
+
default: '我的工作区',
|
|
857
|
+
switch: '切换工作区',
|
|
858
|
+
create: '创建工作区',
|
|
859
|
+
createTitle: '创建工作区',
|
|
860
|
+
createDescription: '工作区是团队协作的共享空间。',
|
|
861
|
+
createButton: '创建工作区',
|
|
862
|
+
nameLabel: '工作区名称',
|
|
863
|
+
namePlaceholder: '例如:极客科技',
|
|
864
|
+
slugLabel: 'URL 标识',
|
|
865
|
+
slugHint: '用于 URL 中,仅支持小写字母、数字和连字符。',
|
|
866
|
+
invite: '邀请成员',
|
|
867
|
+
members: '成员',
|
|
868
|
+
settings: '工作区设置',
|
|
869
|
+
},
|
|
870
|
+
sidebar: {
|
|
871
|
+
settings: '设置',
|
|
872
|
+
help: '帮助',
|
|
873
|
+
helpTooltip: '帮助与文档',
|
|
874
|
+
activityFeed: '活动动态',
|
|
875
|
+
},
|
|
876
|
+
home: {
|
|
877
|
+
title: '首页',
|
|
878
|
+
subtitle: '您的工作区仪表盘',
|
|
879
|
+
nav: '首页',
|
|
880
|
+
allApps: '全部应用',
|
|
881
|
+
loading: '正在加载工作区...',
|
|
882
|
+
recent: '最近使用',
|
|
883
|
+
starred: '收藏',
|
|
884
|
+
welcome: '欢迎使用 ObjectUI',
|
|
885
|
+
welcomeDescription: '从创建您的第一个应用开始,或者先完成系统设置。',
|
|
886
|
+
createFirstApp: '创建您的第一个应用',
|
|
887
|
+
systemSettings: '系统设置',
|
|
888
|
+
quickActions: {
|
|
889
|
+
title: '快捷操作',
|
|
890
|
+
createApp: '创建应用',
|
|
891
|
+
createAppDesc: '从新应用开始',
|
|
892
|
+
manageObjects: '管理对象',
|
|
893
|
+
manageObjectsDesc: '配置数据模型',
|
|
894
|
+
systemSettings: '系统设置',
|
|
895
|
+
systemSettingsDesc: '配置您的工作区',
|
|
896
|
+
},
|
|
897
|
+
recentApps: {
|
|
898
|
+
title: '最近访问',
|
|
899
|
+
itemType: {
|
|
900
|
+
object: '对象',
|
|
901
|
+
dashboard: '仪表盘',
|
|
902
|
+
page: '页面',
|
|
903
|
+
record: '记录',
|
|
904
|
+
},
|
|
905
|
+
},
|
|
906
|
+
appCard: {
|
|
907
|
+
noDescription: '暂无描述',
|
|
908
|
+
default: '默认',
|
|
909
|
+
},
|
|
910
|
+
},
|
|
833
911
|
};
|
|
834
912
|
export default zh;
|
package/dist/useObjectLabel.d.ts
CHANGED
|
@@ -50,6 +50,43 @@ export declare function useObjectLabel(): {
|
|
|
50
50
|
* Resolve translated field label, falling back to the provided fallback string.
|
|
51
51
|
*/
|
|
52
52
|
fieldLabel: (objectName: string, fieldName: string, fallback: string) => string;
|
|
53
|
+
/**
|
|
54
|
+
* Resolve a translated select option label for a given object field.
|
|
55
|
+
* Falls back to the provided fallback (usually the English option label).
|
|
56
|
+
*/
|
|
57
|
+
fieldOptionLabel: (objectName: string, fieldName: string, optionValue: string, fallback: string) => string;
|
|
58
|
+
/**
|
|
59
|
+
* Translate all options for a given field, returning a new options array
|
|
60
|
+
* with translated labels. Pass the objectName and fieldName to look up
|
|
61
|
+
* translations; the original label is used as fallback.
|
|
62
|
+
*/
|
|
63
|
+
translateOptions: (objectName: string, fieldName: string, options: Array<{
|
|
64
|
+
value: string;
|
|
65
|
+
label: string;
|
|
66
|
+
[key: string]: any;
|
|
67
|
+
}>) => Array<{
|
|
68
|
+
value: string;
|
|
69
|
+
label: string;
|
|
70
|
+
[key: string]: any;
|
|
71
|
+
}>;
|
|
72
|
+
/**
|
|
73
|
+
* Resolve translated app label, falling back to appDef.label.
|
|
74
|
+
* Looks up `{ns}.apps.{appName}.label` from loaded i18next resources.
|
|
75
|
+
*/
|
|
76
|
+
appLabel: (appDef: {
|
|
77
|
+
name: string;
|
|
78
|
+
label?: string;
|
|
79
|
+
}) => string;
|
|
80
|
+
/**
|
|
81
|
+
* Resolve translated app description, falling back to appDef.description.
|
|
82
|
+
* Returns the translated value even when metadata has no description —
|
|
83
|
+
* translation-only descriptions (defined only in i18n bundles) are common
|
|
84
|
+
* in examples where the app metadata is English-only.
|
|
85
|
+
*/
|
|
86
|
+
appDescription: (appDef: {
|
|
87
|
+
name: string;
|
|
88
|
+
description?: string;
|
|
89
|
+
}) => string | undefined;
|
|
53
90
|
};
|
|
54
91
|
/**
|
|
55
92
|
* Safe wrapper for useObjectLabel that falls back to identity functions
|
|
@@ -58,4 +95,13 @@ export declare function useObjectLabel(): {
|
|
|
58
95
|
*/
|
|
59
96
|
export declare function useSafeFieldLabel(): {
|
|
60
97
|
fieldLabel: (objectName: string, fieldName: string, fallback: string) => string;
|
|
98
|
+
translateOptions: (objectName: string, fieldName: string, options: Array<{
|
|
99
|
+
value: string;
|
|
100
|
+
label: string;
|
|
101
|
+
[key: string]: any;
|
|
102
|
+
}>) => Array<{
|
|
103
|
+
value: string;
|
|
104
|
+
label: string;
|
|
105
|
+
[key: string]: any;
|
|
106
|
+
}>;
|
|
61
107
|
};
|
package/dist/useObjectLabel.js
CHANGED
|
@@ -48,7 +48,8 @@ export function useObjectLabel() {
|
|
|
48
48
|
/**
|
|
49
49
|
* Discover app namespace(s) from loaded i18next resources.
|
|
50
50
|
* Returns top-level keys (outside built-in Object UI keys) that contain
|
|
51
|
-
* an `objects` or `
|
|
51
|
+
* an `objects`, `fields`, or `apps` sub-key — e.g. "crm" when resources
|
|
52
|
+
* include crm.objects.* or crm.apps.*.
|
|
52
53
|
*/
|
|
53
54
|
const getAppNamespaces = () => {
|
|
54
55
|
if (!i18n || typeof i18n.getResourceBundle !== 'function')
|
|
@@ -58,17 +59,30 @@ export function useObjectLabel() {
|
|
|
58
59
|
if (!bundle)
|
|
59
60
|
return [];
|
|
60
61
|
return Object.keys(bundle).filter((key) => !BUILTIN_KEYS.has(key) && bundle[key] && typeof bundle[key] === 'object'
|
|
61
|
-
&& (bundle[key].objects || bundle[key].fields));
|
|
62
|
+
&& (bundle[key].objects || bundle[key].fields || bundle[key].apps));
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Strip an ObjectStack namespace prefix (e.g. `crm__lead` → `lead`) so that
|
|
66
|
+
* translations authored against short object names still resolve when the
|
|
67
|
+
* runtime presents fully-qualified names. The first `__` separates the
|
|
68
|
+
* package namespace from the base name; everything after is preserved.
|
|
69
|
+
*/
|
|
70
|
+
const stripNamespace = (name) => {
|
|
71
|
+
const idx = name.indexOf('__');
|
|
72
|
+
return idx > 0 ? name.slice(idx + 2) : name;
|
|
62
73
|
};
|
|
63
74
|
/** Try resolving a key across all discovered app namespaces. */
|
|
64
|
-
const resolve = (
|
|
75
|
+
const resolve = (suffixes, fallback) => {
|
|
76
|
+
const suffixList = Array.isArray(suffixes) ? suffixes : [suffixes];
|
|
65
77
|
try {
|
|
66
78
|
const namespaces = getAppNamespaces();
|
|
67
79
|
for (const ns of namespaces) {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
80
|
+
for (const suffix of suffixList) {
|
|
81
|
+
const key = `${ns}.${suffix}`;
|
|
82
|
+
const translated = t(key, { defaultValue: '' });
|
|
83
|
+
if (translated && translated !== key && translated !== '') {
|
|
84
|
+
return translated;
|
|
85
|
+
}
|
|
72
86
|
}
|
|
73
87
|
}
|
|
74
88
|
}
|
|
@@ -77,23 +91,75 @@ export function useObjectLabel() {
|
|
|
77
91
|
}
|
|
78
92
|
return fallback;
|
|
79
93
|
};
|
|
94
|
+
/** Build suffix candidates: prefer the given name, fall back to the base (unprefixed) name. */
|
|
95
|
+
const objectSuffixes = (objectName, tail) => {
|
|
96
|
+
const base = stripNamespace(objectName);
|
|
97
|
+
return base !== objectName
|
|
98
|
+
? [`objects.${objectName}.${tail}`, `objects.${base}.${tail}`]
|
|
99
|
+
: [`objects.${objectName}.${tail}`];
|
|
100
|
+
};
|
|
101
|
+
const fieldSuffixes = (objectName, fieldName) => {
|
|
102
|
+
const base = stripNamespace(objectName);
|
|
103
|
+
return base !== objectName
|
|
104
|
+
? [`fields.${objectName}.${fieldName}`, `fields.${base}.${fieldName}`]
|
|
105
|
+
: [`fields.${objectName}.${fieldName}`];
|
|
106
|
+
};
|
|
107
|
+
const optionSuffixes = (objectName, fieldName, optionValue) => {
|
|
108
|
+
const base = stripNamespace(objectName);
|
|
109
|
+
return base !== objectName
|
|
110
|
+
? [
|
|
111
|
+
`fieldOptions.${objectName}.${fieldName}.${optionValue}`,
|
|
112
|
+
`fieldOptions.${base}.${fieldName}.${optionValue}`,
|
|
113
|
+
]
|
|
114
|
+
: [`fieldOptions.${objectName}.${fieldName}.${optionValue}`];
|
|
115
|
+
};
|
|
80
116
|
return {
|
|
81
117
|
/**
|
|
82
118
|
* Resolve translated object label, falling back to objectDef.label.
|
|
83
119
|
*/
|
|
84
|
-
objectLabel: (objectDef) => resolve(
|
|
120
|
+
objectLabel: (objectDef) => resolve(objectSuffixes(objectDef.name, 'label'), objectDef.label),
|
|
85
121
|
/**
|
|
86
122
|
* Resolve translated object description, falling back to objectDef.description.
|
|
87
123
|
*/
|
|
88
124
|
objectDescription: (objectDef) => {
|
|
89
125
|
if (!objectDef.description)
|
|
90
126
|
return undefined;
|
|
91
|
-
return resolve(
|
|
127
|
+
return resolve(objectSuffixes(objectDef.name, 'description'), objectDef.description);
|
|
92
128
|
},
|
|
93
129
|
/**
|
|
94
130
|
* Resolve translated field label, falling back to the provided fallback string.
|
|
95
131
|
*/
|
|
96
|
-
fieldLabel: (objectName, fieldName, fallback) => resolve(
|
|
132
|
+
fieldLabel: (objectName, fieldName, fallback) => resolve(fieldSuffixes(objectName, fieldName), fallback),
|
|
133
|
+
/**
|
|
134
|
+
* Resolve a translated select option label for a given object field.
|
|
135
|
+
* Falls back to the provided fallback (usually the English option label).
|
|
136
|
+
*/
|
|
137
|
+
fieldOptionLabel: (objectName, fieldName, optionValue, fallback) => resolve(optionSuffixes(objectName, fieldName, optionValue), fallback),
|
|
138
|
+
/**
|
|
139
|
+
* Translate all options for a given field, returning a new options array
|
|
140
|
+
* with translated labels. Pass the objectName and fieldName to look up
|
|
141
|
+
* translations; the original label is used as fallback.
|
|
142
|
+
*/
|
|
143
|
+
translateOptions: (objectName, fieldName, options) => options.map(opt => ({
|
|
144
|
+
...opt,
|
|
145
|
+
label: resolve(optionSuffixes(objectName, fieldName, opt.value), opt.label),
|
|
146
|
+
})),
|
|
147
|
+
/**
|
|
148
|
+
* Resolve translated app label, falling back to appDef.label.
|
|
149
|
+
* Looks up `{ns}.apps.{appName}.label` from loaded i18next resources.
|
|
150
|
+
*/
|
|
151
|
+
appLabel: (appDef) => resolve(`apps.${appDef.name}.label`, appDef.label ?? appDef.name),
|
|
152
|
+
/**
|
|
153
|
+
* Resolve translated app description, falling back to appDef.description.
|
|
154
|
+
* Returns the translated value even when metadata has no description —
|
|
155
|
+
* translation-only descriptions (defined only in i18n bundles) are common
|
|
156
|
+
* in examples where the app metadata is English-only.
|
|
157
|
+
*/
|
|
158
|
+
appDescription: (appDef) => {
|
|
159
|
+
const fallback = appDef.description ?? '';
|
|
160
|
+
const resolved = resolve(`apps.${appDef.name}.description`, fallback);
|
|
161
|
+
return resolved || undefined;
|
|
162
|
+
},
|
|
97
163
|
};
|
|
98
164
|
}
|
|
99
165
|
/**
|
|
@@ -103,12 +169,13 @@ export function useObjectLabel() {
|
|
|
103
169
|
*/
|
|
104
170
|
export function useSafeFieldLabel() {
|
|
105
171
|
try {
|
|
106
|
-
const { fieldLabel } = useObjectLabel();
|
|
107
|
-
return { fieldLabel };
|
|
172
|
+
const { fieldLabel, translateOptions } = useObjectLabel();
|
|
173
|
+
return { fieldLabel, translateOptions };
|
|
108
174
|
}
|
|
109
175
|
catch {
|
|
110
176
|
return {
|
|
111
177
|
fieldLabel: (_objectName, _fieldName, fallback) => fallback,
|
|
178
|
+
translateOptions: (_objectName, _fieldName, options) => options,
|
|
112
179
|
};
|
|
113
180
|
}
|
|
114
181
|
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/i18n",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"description": "Internationalization (i18n) support for Object UI with 10+ language packs, RTL layout, and date/currency formatting.",
|
|
8
|
-
"homepage": "https://www.objectui.org",
|
|
8
|
+
"homepage": "https://www.objectui.org/docs/packages/i18n",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "https://github.com/objectstack-ai/objectui.git",
|
|
11
|
+
"url": "git+https://github.com/objectstack-ai/objectui.git",
|
|
12
12
|
"directory": "packages/i18n"
|
|
13
13
|
},
|
|
14
14
|
"bugs": {
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
}
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"i18next": "^26.0.
|
|
34
|
-
"react-i18next": "^17.0.
|
|
33
|
+
"i18next": "^26.0.8",
|
|
34
|
+
"react-i18next": "^17.0.6"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"react": "^18.0.0 || ^19.0.0"
|
|
@@ -39,9 +39,32 @@
|
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/react": "19.2.14",
|
|
41
41
|
"react": "19.2.5",
|
|
42
|
-
"typescript": "^6.0.
|
|
43
|
-
"vitest": "^4.1.
|
|
42
|
+
"typescript": "^6.0.3",
|
|
43
|
+
"vitest": "^4.1.5"
|
|
44
44
|
},
|
|
45
|
+
"keywords": [
|
|
46
|
+
"objectui",
|
|
47
|
+
"sdui",
|
|
48
|
+
"schema-driven-ui",
|
|
49
|
+
"react",
|
|
50
|
+
"tailwind",
|
|
51
|
+
"shadcn",
|
|
52
|
+
"objectstack",
|
|
53
|
+
"i18n",
|
|
54
|
+
"internationalization",
|
|
55
|
+
"localization",
|
|
56
|
+
"rtl"
|
|
57
|
+
],
|
|
58
|
+
"author": "ObjectStack Team <team@objectstack.ai>",
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public"
|
|
61
|
+
},
|
|
62
|
+
"files": [
|
|
63
|
+
"dist",
|
|
64
|
+
"README.md",
|
|
65
|
+
"CHANGELOG.md",
|
|
66
|
+
"LICENSE"
|
|
67
|
+
],
|
|
45
68
|
"scripts": {
|
|
46
69
|
"build": "tsc",
|
|
47
70
|
"test": "vitest run",
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
createI18n,
|
|
4
|
-
getDirection,
|
|
5
|
-
getAvailableLanguages,
|
|
6
|
-
builtInLocales,
|
|
7
|
-
isRTL,
|
|
8
|
-
RTL_LANGUAGES,
|
|
9
|
-
formatDate,
|
|
10
|
-
formatDateTime,
|
|
11
|
-
formatCurrency,
|
|
12
|
-
formatNumber,
|
|
13
|
-
} from '../index';
|
|
14
|
-
|
|
15
|
-
describe('@object-ui/i18n', () => {
|
|
16
|
-
describe('createI18n', () => {
|
|
17
|
-
it('creates an i18next instance with default config', () => {
|
|
18
|
-
const i18n = createI18n({ detectBrowserLanguage: false });
|
|
19
|
-
expect(i18n).toBeDefined();
|
|
20
|
-
expect(i18n.language).toBe('en');
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('creates an instance with specified default language', () => {
|
|
24
|
-
const i18n = createI18n({ defaultLanguage: 'zh', detectBrowserLanguage: false });
|
|
25
|
-
expect(i18n.language).toBe('zh');
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('loads all built-in locales', () => {
|
|
29
|
-
const i18n = createI18n({ detectBrowserLanguage: false });
|
|
30
|
-
const langs = getAvailableLanguages(i18n);
|
|
31
|
-
expect(langs).toContain('en');
|
|
32
|
-
expect(langs).toContain('zh');
|
|
33
|
-
expect(langs).toContain('ja');
|
|
34
|
-
expect(langs).toContain('ko');
|
|
35
|
-
expect(langs).toContain('de');
|
|
36
|
-
expect(langs).toContain('fr');
|
|
37
|
-
expect(langs).toContain('es');
|
|
38
|
-
expect(langs).toContain('pt');
|
|
39
|
-
expect(langs).toContain('ru');
|
|
40
|
-
expect(langs).toContain('ar');
|
|
41
|
-
expect(langs.length).toBeGreaterThanOrEqual(10);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('translates common keys in English', () => {
|
|
45
|
-
const i18n = createI18n({ defaultLanguage: 'en', detectBrowserLanguage: false });
|
|
46
|
-
expect(i18n.t('common.save')).toBe('Save');
|
|
47
|
-
expect(i18n.t('common.cancel')).toBe('Cancel');
|
|
48
|
-
expect(i18n.t('common.delete')).toBe('Delete');
|
|
49
|
-
expect(i18n.t('common.loading')).toBe('Loading...');
|
|
50
|
-
expect(i18n.t('common.selectOption')).toBe('Select an option');
|
|
51
|
-
expect(i18n.t('common.select')).toBe('Select...');
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('translates new form keys in English', () => {
|
|
55
|
-
const i18n = createI18n({ defaultLanguage: 'en', detectBrowserLanguage: false });
|
|
56
|
-
expect(i18n.t('form.createTitle', { object: 'Contact' })).toBe('Create Contact');
|
|
57
|
-
expect(i18n.t('form.editTitle', { object: 'Contact' })).toBe('Edit Contact');
|
|
58
|
-
expect(i18n.t('form.createDescription', { object: 'Contact' })).toBe('Add a new Contact to your database.');
|
|
59
|
-
expect(i18n.t('form.editDescription', { object: 'Contact' })).toBe('Update details for Contact');
|
|
60
|
-
expect(i18n.t('form.saveRecord')).toBe('Save Record');
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('translates common keys in Chinese', () => {
|
|
64
|
-
const i18n = createI18n({ defaultLanguage: 'zh', detectBrowserLanguage: false });
|
|
65
|
-
expect(i18n.t('common.save')).toBe('保存');
|
|
66
|
-
expect(i18n.t('common.cancel')).toBe('取消');
|
|
67
|
-
expect(i18n.t('common.delete')).toBe('删除');
|
|
68
|
-
expect(i18n.t('common.loading')).toBe('加载中...');
|
|
69
|
-
expect(i18n.t('common.selectOption')).toBe('请选择');
|
|
70
|
-
expect(i18n.t('common.select')).toBe('选择...');
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('translates new form keys in Chinese', () => {
|
|
74
|
-
const i18n = createI18n({ defaultLanguage: 'zh', detectBrowserLanguage: false });
|
|
75
|
-
expect(i18n.t('form.createTitle', { object: '联系人' })).toBe('新建联系人');
|
|
76
|
-
expect(i18n.t('form.editTitle', { object: '联系人' })).toBe('编辑联系人');
|
|
77
|
-
expect(i18n.t('form.createDescription', { object: '联系人' })).toBe('向数据库添加新的联系人。');
|
|
78
|
-
expect(i18n.t('form.editDescription', { object: '联系人' })).toBe('更新联系人的详情');
|
|
79
|
-
expect(i18n.t('form.saveRecord')).toBe('保存记录');
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('translates toolbarEnabledCount in Chinese (not English fallback)', () => {
|
|
83
|
-
const i18n = createI18n({ defaultLanguage: 'zh', detectBrowserLanguage: false });
|
|
84
|
-
expect(i18n.t('console.objectView.toolbarEnabledCount', { count: 3, total: 5 })).toBe('已启用 3/5 项');
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('translates common keys in Japanese', () => {
|
|
88
|
-
const i18n = createI18n({ defaultLanguage: 'ja', detectBrowserLanguage: false });
|
|
89
|
-
expect(i18n.t('common.save')).toBe('保存');
|
|
90
|
-
expect(i18n.t('common.cancel')).toBe('キャンセル');
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('translates common keys in Korean', () => {
|
|
94
|
-
const i18n = createI18n({ defaultLanguage: 'ko', detectBrowserLanguage: false });
|
|
95
|
-
expect(i18n.t('common.save')).toBe('저장');
|
|
96
|
-
expect(i18n.t('common.cancel')).toBe('취소');
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('supports interpolation in validation messages', () => {
|
|
100
|
-
const i18n = createI18n({ defaultLanguage: 'en', detectBrowserLanguage: false });
|
|
101
|
-
expect(i18n.t('validation.required', { field: 'Name' })).toBe('Name is required');
|
|
102
|
-
expect(i18n.t('validation.minLength', { field: 'Password', min: 8 })).toBe(
|
|
103
|
-
'Password must be at least 8 characters',
|
|
104
|
-
);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('supports interpolation in Chinese', () => {
|
|
108
|
-
const i18n = createI18n({ defaultLanguage: 'zh', detectBrowserLanguage: false });
|
|
109
|
-
expect(i18n.t('validation.required', { field: '姓名' })).toBe('姓名不能为空');
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('falls back to English for unknown language', () => {
|
|
113
|
-
const i18n = createI18n({ defaultLanguage: 'xx', fallbackLanguage: 'en', detectBrowserLanguage: false });
|
|
114
|
-
expect(i18n.t('common.save')).toBe('Save');
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('merges custom resources with built-in locales', () => {
|
|
118
|
-
const i18n = createI18n({
|
|
119
|
-
defaultLanguage: 'en',
|
|
120
|
-
detectBrowserLanguage: false,
|
|
121
|
-
resources: {
|
|
122
|
-
en: { custom: { greeting: 'Hello!' } },
|
|
123
|
-
},
|
|
124
|
-
});
|
|
125
|
-
expect(i18n.t('custom.greeting')).toBe('Hello!');
|
|
126
|
-
// Built-in translations still work
|
|
127
|
-
expect(i18n.t('common.save')).toBe('Save');
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('changes language dynamically', async () => {
|
|
131
|
-
const i18n = createI18n({ defaultLanguage: 'en', detectBrowserLanguage: false });
|
|
132
|
-
expect(i18n.t('common.save')).toBe('Save');
|
|
133
|
-
|
|
134
|
-
await i18n.changeLanguage('zh');
|
|
135
|
-
expect(i18n.language).toBe('zh');
|
|
136
|
-
expect(i18n.t('common.save')).toBe('保存');
|
|
137
|
-
|
|
138
|
-
await i18n.changeLanguage('ja');
|
|
139
|
-
expect(i18n.language).toBe('ja');
|
|
140
|
-
expect(i18n.t('common.save')).toBe('保存');
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
describe('getDirection', () => {
|
|
145
|
-
it('returns ltr for English', () => {
|
|
146
|
-
expect(getDirection('en')).toBe('ltr');
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('returns ltr for Chinese', () => {
|
|
150
|
-
expect(getDirection('zh')).toBe('ltr');
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('returns rtl for Arabic', () => {
|
|
154
|
-
expect(getDirection('ar')).toBe('rtl');
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('returns ltr for unknown languages', () => {
|
|
158
|
-
expect(getDirection('xx')).toBe('ltr');
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
describe('isRTL', () => {
|
|
163
|
-
it('returns true for Arabic', () => {
|
|
164
|
-
expect(isRTL('ar')).toBe(true);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('returns false for English', () => {
|
|
168
|
-
expect(isRTL('en')).toBe(false);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it('returns false for Chinese', () => {
|
|
172
|
-
expect(isRTL('zh')).toBe(false);
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
describe('RTL_LANGUAGES', () => {
|
|
177
|
-
it('includes Arabic', () => {
|
|
178
|
-
expect(RTL_LANGUAGES).toContain('ar');
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it('includes Hebrew', () => {
|
|
182
|
-
expect(RTL_LANGUAGES).toContain('he');
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
describe('builtInLocales', () => {
|
|
187
|
-
it('has 10 built-in language packs', () => {
|
|
188
|
-
expect(Object.keys(builtInLocales).length).toBe(10);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it('all locales have the same top-level keys', () => {
|
|
192
|
-
const enKeys = Object.keys(builtInLocales.en).sort();
|
|
193
|
-
for (const [lang, locale] of Object.entries(builtInLocales)) {
|
|
194
|
-
const keys = Object.keys(locale).sort();
|
|
195
|
-
expect(keys).toEqual(enKeys);
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('all locales have common section keys matching English', () => {
|
|
200
|
-
const enCommonKeys = Object.keys(builtInLocales.en.common).sort();
|
|
201
|
-
for (const [lang, locale] of Object.entries(builtInLocales)) {
|
|
202
|
-
const keys = Object.keys(locale.common).sort();
|
|
203
|
-
expect(keys).toEqual(enCommonKeys);
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
describe('formatDate', () => {
|
|
209
|
-
it('formats a date with default options', () => {
|
|
210
|
-
const result = formatDate(new Date(2026, 0, 15), { locale: 'en' });
|
|
211
|
-
expect(result).toContain('Jan');
|
|
212
|
-
expect(result).toContain('15');
|
|
213
|
-
expect(result).toContain('2026');
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
it('formats a date with short style', () => {
|
|
217
|
-
const result = formatDate(new Date(2026, 0, 15), { locale: 'en', style: 'short' });
|
|
218
|
-
expect(result).toContain('15');
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it('returns string for invalid dates', () => {
|
|
222
|
-
const result = formatDate('invalid-date');
|
|
223
|
-
expect(result).toBe('invalid-date');
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
describe('formatCurrency', () => {
|
|
228
|
-
it('formats USD by default', () => {
|
|
229
|
-
const result = formatCurrency(1234.56, { locale: 'en' });
|
|
230
|
-
expect(result).toContain('1,234.56');
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it('formats EUR', () => {
|
|
234
|
-
const result = formatCurrency(1234.56, { locale: 'de', currency: 'EUR' });
|
|
235
|
-
expect(result).toContain('1.234,56');
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it('formats CNY', () => {
|
|
239
|
-
const result = formatCurrency(1234.56, { locale: 'zh', currency: 'CNY' });
|
|
240
|
-
expect(result).toContain('1,234.56');
|
|
241
|
-
});
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
describe('formatNumber', () => {
|
|
245
|
-
it('formats a number with default locale', () => {
|
|
246
|
-
const result = formatNumber(1234567.89, { locale: 'en' });
|
|
247
|
-
expect(result).toContain('1,234,567.89');
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
it('formats with compact notation', () => {
|
|
251
|
-
const result = formatNumber(1234567, { locale: 'en', notation: 'compact' });
|
|
252
|
-
expect(result).toContain('M'); // e.g. 1.2M
|
|
253
|
-
});
|
|
254
|
-
});
|
|
255
|
-
});
|