@modern-js/main-doc 3.1.4 → 3.2.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/docs/en/components/international/init-options-desc.mdx +1 -1
- package/docs/en/components/international/install-command.mdx +4 -17
- package/docs/en/components/international/instance-code.mdx +4 -14
- package/docs/en/components/international/introduce.mdx +4 -1
- package/docs/en/configure/app/source/enable-async-pre-entry.mdx +30 -0
- package/docs/en/configure/app/tools/dev-server.mdx +0 -4
- package/docs/en/guides/advanced-features/international/_meta.json +0 -1
- package/docs/en/guides/advanced-features/international/advanced.mdx +48 -109
- package/docs/en/guides/advanced-features/international/api.mdx +125 -290
- package/docs/en/guides/advanced-features/international/best-practices.mdx +203 -48
- package/docs/en/guides/advanced-features/international/configuration.mdx +108 -315
- package/docs/en/guides/advanced-features/international/locale-detection.mdx +62 -208
- package/docs/en/guides/advanced-features/international/quick-start.mdx +41 -55
- package/docs/en/guides/advanced-features/international/resource-loading.mdx +63 -322
- package/docs/en/guides/advanced-features/international/routing.mdx +60 -138
- package/docs/en/guides/advanced-features/international.mdx +19 -27
- package/docs/en/guides/basic-features/alias.mdx +1 -1
- package/docs/en/guides/basic-features/html.mdx +2 -2
- package/docs/en/guides/basic-features/static-assets.mdx +1 -2
- package/docs/en/guides/concept/entries.mdx +2 -2
- package/docs/zh/components/international/init-options-desc.mdx +1 -1
- package/docs/zh/components/international/install-command.mdx +4 -16
- package/docs/zh/components/international/instance-code.mdx +4 -14
- package/docs/zh/components/international/introduce.mdx +5 -2
- package/docs/zh/configure/app/source/enable-async-pre-entry.mdx +77 -0
- package/docs/zh/configure/app/tools/dev-server.mdx +0 -4
- package/docs/zh/guides/advanced-features/bff/function.mdx +2 -2
- package/docs/zh/guides/advanced-features/international/_meta.json +0 -1
- package/docs/zh/guides/advanced-features/international/advanced.mdx +48 -109
- package/docs/zh/guides/advanced-features/international/api.mdx +126 -292
- package/docs/zh/guides/advanced-features/international/best-practices.mdx +204 -49
- package/docs/zh/guides/advanced-features/international/configuration.mdx +105 -318
- package/docs/zh/guides/advanced-features/international/locale-detection.mdx +62 -236
- package/docs/zh/guides/advanced-features/international/quick-start.mdx +40 -54
- package/docs/zh/guides/advanced-features/international/resource-loading.mdx +62 -324
- package/docs/zh/guides/advanced-features/international/routing.mdx +58 -136
- package/docs/zh/guides/advanced-features/international.mdx +19 -26
- package/docs/zh/guides/basic-features/alias.mdx +1 -1
- package/docs/zh/guides/basic-features/html.mdx +2 -2
- package/docs/zh/guides/basic-features/static-assets.mdx +1 -2
- package/docs/zh/guides/concept/entries.mdx +2 -2
- package/package.json +5 -4
- package/rspress.config.ts +45 -26
- package/docs/en/components/rspackPrecautions.mdx +0 -6
- package/docs/en/guides/advanced-features/international/basic.mdx +0 -417
- package/docs/zh/components/rspackPrecautions.mdx +0 -6
- package/docs/zh/guides/advanced-features/international/basic.mdx +0 -416
|
@@ -6,23 +6,26 @@ title: 最佳实践
|
|
|
6
6
|
|
|
7
7
|
## 资源文件组织
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
命名空间(Namespace)用于按业务模块拆分翻译文件,默认命名空间为 `translation`。按模块拆分可以减少初始加载体积,只在用到时按需加载对应命名空间。
|
|
10
|
+
|
|
11
|
+
推荐目录结构:
|
|
10
12
|
|
|
11
13
|
```
|
|
12
14
|
locales/
|
|
13
15
|
├── en/
|
|
14
|
-
│ ├── translation.json
|
|
15
|
-
│ ├── common.json
|
|
16
|
-
│ └── errors.json
|
|
16
|
+
│ ├── translation.json ← 默认命名空间,放通用文案
|
|
17
|
+
│ ├── common.json ← 按钮、标签等公共 UI 文案
|
|
18
|
+
│ └── errors.json ← 错误信息
|
|
17
19
|
└── zh/
|
|
18
20
|
├── translation.json
|
|
19
21
|
├── common.json
|
|
20
22
|
└── errors.json
|
|
21
23
|
```
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
声明多个命名空间:
|
|
24
26
|
|
|
25
27
|
```ts
|
|
28
|
+
// src/modern.runtime.ts
|
|
26
29
|
export default defineRuntimeConfig({
|
|
27
30
|
i18n: {
|
|
28
31
|
initOptions: {
|
|
@@ -33,25 +36,178 @@ export default defineRuntimeConfig({
|
|
|
33
36
|
});
|
|
34
37
|
```
|
|
35
38
|
|
|
36
|
-
|
|
39
|
+
在组件中使用指定命名空间:
|
|
37
40
|
|
|
38
41
|
```tsx
|
|
39
42
|
import { useTranslation } from 'react-i18next';
|
|
40
43
|
|
|
41
|
-
|
|
44
|
+
// 指定单个命名空间
|
|
45
|
+
function MyButton() {
|
|
42
46
|
const { t } = useTranslation('common');
|
|
47
|
+
return <button>{t('submit')}</button>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 同时使用多个命名空间,用 namespace:key 格式访问
|
|
51
|
+
function Dashboard() {
|
|
52
|
+
const { t } = useTranslation(['dashboard', 'common']);
|
|
53
|
+
return (
|
|
54
|
+
<header>
|
|
55
|
+
<h1>{t('dashboard:title')}</h1>
|
|
56
|
+
<button>{t('common:button.refresh')}</button>
|
|
57
|
+
</header>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
43
60
|
|
|
44
|
-
|
|
61
|
+
// keyPrefix 可以省略重复的前缀
|
|
62
|
+
function ButtonGroup() {
|
|
63
|
+
const { t } = useTranslation('common', { keyPrefix: 'button' });
|
|
64
|
+
return (
|
|
65
|
+
<>
|
|
66
|
+
<button>{t('submit')}</button> {/* → common:button.submit */}
|
|
67
|
+
<button>{t('cancel')}</button> {/* → common:button.cancel */}
|
|
68
|
+
</>
|
|
69
|
+
);
|
|
45
70
|
}
|
|
46
71
|
```
|
|
47
72
|
|
|
48
|
-
##
|
|
73
|
+
## 翻译键命名规范
|
|
74
|
+
|
|
75
|
+
翻译键的命名质量直接影响后续维护成本,建议:
|
|
76
|
+
|
|
77
|
+
- **用语义化词语**,避免缩写:`button.submit` 优于 `btn.sbm`
|
|
78
|
+
- **按模块划分前缀**:`dashboard.table.header`、`auth.login.title`
|
|
79
|
+
- **不要用完整中文文案当键名**:键名应是稳定的标识符,而不是翻译内容本身
|
|
80
|
+
- **用点号表示层级**:与 JSON 嵌套结构一一对应
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
// ✅ 推荐
|
|
84
|
+
{
|
|
85
|
+
"page": {
|
|
86
|
+
"title": "用户设置",
|
|
87
|
+
"description": "管理你的账号信息"
|
|
88
|
+
},
|
|
89
|
+
"form": {
|
|
90
|
+
"username": { "label": "用户名", "placeholder": "请输入用户名" },
|
|
91
|
+
"submit": "保存更改"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
t('page.title')
|
|
98
|
+
t('form.username.label')
|
|
99
|
+
t('form.submit', { defaultValue: '保存' }) // defaultValue 防止键缺失时显示 key 字符串
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## 复数
|
|
103
|
+
|
|
104
|
+
i18next 根据 `count` 参数自动选择对应的复数形式。**注意:i18next v21 之后默认使用 JSON v4 格式**,复数后缀改为 CLDR 标准(`_zero`、`_one`、`_other` 等),旧的 `_plural` 写法已废弃。
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
// locales/en/translation.json(JSON v4 格式)
|
|
108
|
+
{
|
|
109
|
+
"item_zero": "No items",
|
|
110
|
+
"item_one": "{{count}} item",
|
|
111
|
+
"item_other": "{{count}} items"
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
// locales/zh/translation.json
|
|
117
|
+
{
|
|
118
|
+
"item_zero": "没有条目",
|
|
119
|
+
"item_other": "{{count}} 个条目"
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
t('item', { count: 0 }) // → "没有条目" / "No items"
|
|
125
|
+
t('item', { count: 1 }) // → "没有条目" / "1 item"(中文通常只有一种形式)
|
|
126
|
+
t('item', { count: 5 }) // → "5 个条目" / "5 items"
|
|
127
|
+
```
|
|
49
128
|
|
|
50
|
-
|
|
129
|
+
不同语言的复数规则不同(英语有单复数,俄语有 one/few/many 等多种形式),i18next 会根据语言代码自动匹配 CLDR 规则。
|
|
51
130
|
|
|
52
|
-
|
|
131
|
+
:::tip
|
|
132
|
+
如果项目使用了旧版 i18next 或显式配置了 `compatibilityJSON: 'v3'`,才需要使用 `_plural` 写法。新项目统一使用 v4 格式。
|
|
133
|
+
:::
|
|
53
134
|
|
|
54
|
-
|
|
135
|
+
## 嵌套键
|
|
136
|
+
|
|
137
|
+
嵌套结构可以直观反映 UI 层级,用点号访问:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"modal": {
|
|
142
|
+
"confirm": {
|
|
143
|
+
"title": "确认删除",
|
|
144
|
+
"message": "此操作不可撤销,是否继续?",
|
|
145
|
+
"actions": {
|
|
146
|
+
"ok": "确认",
|
|
147
|
+
"cancel": "取消"
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
t('modal.confirm.title')
|
|
156
|
+
t('modal.confirm.actions.ok')
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
嵌套层级不建议超过 4 层,过深的结构会让键名变得冗长难维护。
|
|
160
|
+
|
|
161
|
+
## 格式化插值
|
|
162
|
+
|
|
163
|
+
通过 `interpolation.format` 函数统一处理数字、日期、货币等格式化,避免在每个组件里各自调用 `Intl` API:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
// src/modern.runtime.ts
|
|
167
|
+
export default defineRuntimeConfig({
|
|
168
|
+
i18n: {
|
|
169
|
+
initOptions: {
|
|
170
|
+
interpolation: {
|
|
171
|
+
escapeValue: false, // React 已默认转义文本,关闭避免重复转义
|
|
172
|
+
format(value, format, lng) {
|
|
173
|
+
if (format === 'currency') {
|
|
174
|
+
return new Intl.NumberFormat(lng, {
|
|
175
|
+
style: 'currency',
|
|
176
|
+
currency: lng === 'zh' ? 'CNY' : 'USD',
|
|
177
|
+
}).format(Number(value));
|
|
178
|
+
}
|
|
179
|
+
if (format === 'date') {
|
|
180
|
+
return new Intl.DateTimeFormat(lng, { dateStyle: 'medium' }).format(
|
|
181
|
+
value instanceof Date ? value : new Date(value),
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
return value;
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
翻译文件中用 `, format` 指定格式化类型:
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
{
|
|
196
|
+
"price": "当前价格:{{value, currency}}",
|
|
197
|
+
"expiry": "到期日:{{date, date}}"
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
t('price', { value: 99.5 }) // → "当前价格:¥99.50"(zh)/ "$99.50"(en)
|
|
203
|
+
t('expiry', { date: new Date() }) // → "到期日:2025年5月12日"(zh)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## 错误处理
|
|
207
|
+
|
|
208
|
+
### 加载状态处理
|
|
209
|
+
|
|
210
|
+
使用自定义后端时,翻译资源需要异步加载,推荐用 `isResourcesReady` 处理加载中状态:
|
|
55
211
|
|
|
56
212
|
```tsx
|
|
57
213
|
import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
@@ -61,75 +217,74 @@ function MyComponent() {
|
|
|
61
217
|
const { isResourcesReady } = useModernI18n();
|
|
62
218
|
const { t } = useTranslation();
|
|
63
219
|
|
|
64
|
-
// 检查资源是否已加载并准备好
|
|
65
220
|
if (!isResourcesReady) {
|
|
66
|
-
return <div
|
|
221
|
+
return <div>加载翻译中...</div>;
|
|
67
222
|
}
|
|
68
223
|
|
|
69
|
-
return <div>{t('content', { defaultValue: '
|
|
224
|
+
return <div>{t('content', { defaultValue: '默认内容' })}</div>;
|
|
70
225
|
}
|
|
71
226
|
```
|
|
72
227
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
对于简单场景,也可以检查 i18next 的初始化状态:
|
|
228
|
+
静态资源(HTTP/FS 后端)加载较快,通常不需要处理加载状态;如需检查,可以用 `i18n.isInitialized`:
|
|
76
229
|
|
|
77
230
|
```tsx
|
|
78
|
-
|
|
231
|
+
const { t, i18n } = useTranslation();
|
|
232
|
+
if (!i18n.isInitialized) return null;
|
|
233
|
+
```
|
|
79
234
|
|
|
80
|
-
|
|
81
|
-
const { t, i18n } = useTranslation();
|
|
235
|
+
### 翻译键缺失处理
|
|
82
236
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
237
|
+
通过 `defaultValue` 提供兜底文案,防止界面显示 key 字符串:
|
|
238
|
+
|
|
239
|
+
```tsx
|
|
240
|
+
t('missing.key', { defaultValue: '默认文案' })
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
开发环境可以开启 `saveMissing`,将缺失的键输出到控制台方便排查:
|
|
87
244
|
|
|
88
|
-
|
|
245
|
+
```ts
|
|
246
|
+
initOptions: {
|
|
247
|
+
fallbackLng: 'en',
|
|
248
|
+
saveMissing: true, // 仅建议开发环境开启
|
|
89
249
|
}
|
|
90
250
|
```
|
|
91
251
|
|
|
92
|
-
|
|
93
|
-
对于 SDK 后端场景,`isResourcesReady` 更准确,因为它会检查所有必需的资源是否实际已加载,而不仅仅是检查实例是否已初始化。
|
|
252
|
+
### 网络失败与资源 404
|
|
94
253
|
|
|
95
|
-
|
|
254
|
+
翻译文件加载失败时,i18next 会回退到 `fallbackLng` 对应的资源;若仍失败,`t()` 直接返回 key 字符串。建议:
|
|
255
|
+
|
|
256
|
+
- 生产环境确保 `fallbackLng`(通常是 `en`)对应的翻译文件完整且稳定
|
|
257
|
+
- 使用链式后端时,本地文件作为兜底,减少对远程服务的依赖
|
|
96
258
|
|
|
97
259
|
## 类型安全
|
|
98
260
|
|
|
99
|
-
|
|
261
|
+
通过扩展 react-i18next 的类型定义,让 TypeScript 检查翻译键是否存在并提供自动补全:
|
|
100
262
|
|
|
101
263
|
```ts
|
|
102
264
|
// types/i18n.d.ts
|
|
103
265
|
import 'react-i18next';
|
|
266
|
+
import type translation from '../locales/en/translation.json';
|
|
267
|
+
import type common from '../locales/en/common.json';
|
|
104
268
|
|
|
105
269
|
declare module 'react-i18next' {
|
|
106
270
|
interface CustomTypeOptions {
|
|
107
271
|
defaultNS: 'translation';
|
|
108
272
|
resources: {
|
|
109
|
-
translation:
|
|
110
|
-
|
|
111
|
-
world: string;
|
|
112
|
-
welcome: string;
|
|
113
|
-
};
|
|
114
|
-
common: {
|
|
115
|
-
submit: string;
|
|
116
|
-
cancel: string;
|
|
117
|
-
};
|
|
273
|
+
translation: typeof translation;
|
|
274
|
+
common: typeof common;
|
|
118
275
|
};
|
|
119
276
|
}
|
|
120
277
|
}
|
|
121
278
|
```
|
|
122
279
|
|
|
123
|
-
|
|
280
|
+
直接引用 JSON 文件的类型,比手写 interface 更不容易与实际翻译文件漂移:
|
|
124
281
|
|
|
125
282
|
```tsx
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const { t } = useTranslation();
|
|
130
|
-
|
|
131
|
-
// TypeScript 会检查键是否存在
|
|
132
|
-
return <div>{t('hello')}</div>; // ✅ 类型安全
|
|
133
|
-
// return <div>{t('invalid')}</div>; // ❌ TypeScript 错误
|
|
134
|
-
}
|
|
283
|
+
const { t } = useTranslation();
|
|
284
|
+
t('welcome'); // ✅ TypeScript 自动补全
|
|
285
|
+
t('nonExistent'); // ❌ TypeScript 报错
|
|
135
286
|
```
|
|
287
|
+
|
|
288
|
+
:::tip 大型项目
|
|
289
|
+
翻译文件较多时,手动维护类型文件成本较高。可以使用 [i18next-parser](https://github.com/i18next/i18next-parser) 或 [i18next-resources-for-ts](https://github.com/i18next/i18next-resources-for-ts) 从翻译文件自动生成 TypeScript 类型。
|
|
290
|
+
:::
|