@lhy-meta-server/meta-i18n 0.0.0-stage → 0.0.1

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 (56) hide show
  1. package/README.md +1239 -3
  2. package/dist/context/language-context.d.ts +29 -0
  3. package/dist/context/language-context.d.ts.map +1 -0
  4. package/dist/core/errors.d.ts +42 -0
  5. package/dist/core/errors.d.ts.map +1 -0
  6. package/dist/core/event-emitter.d.ts +61 -0
  7. package/dist/core/event-emitter.d.ts.map +1 -0
  8. package/dist/core/i18n.d.ts +343 -0
  9. package/dist/core/i18n.d.ts.map +1 -0
  10. package/dist/core/translator.d.ts +146 -0
  11. package/dist/core/translator.d.ts.map +1 -0
  12. package/dist/core/types.d.ts +518 -0
  13. package/dist/core/types.d.ts.map +1 -0
  14. package/dist/format/builtin-formatters.d.ts +6 -0
  15. package/dist/format/builtin-formatters.d.ts.map +1 -0
  16. package/dist/format/formatter-registry.d.ts +51 -0
  17. package/dist/format/formatter-registry.d.ts.map +1 -0
  18. package/dist/http/index.d.ts +12 -0
  19. package/dist/http/index.d.ts.map +1 -0
  20. package/dist/http/index.js +176 -0
  21. package/dist/http/index.js.map +11 -0
  22. package/dist/http/language-detector.d.ts +67 -0
  23. package/dist/http/language-detector.d.ts.map +1 -0
  24. package/dist/http/middleware.d.ts +79 -0
  25. package/dist/http/middleware.d.ts.map +1 -0
  26. package/dist/index-2y70nz2z.js +46 -0
  27. package/dist/index-2y70nz2z.js.map +10 -0
  28. package/dist/index.d.ts +21 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +1233 -0
  31. package/dist/index.js.map +23 -0
  32. package/dist/interpolation/interpolator.d.ts +111 -0
  33. package/dist/interpolation/interpolator.d.ts.map +1 -0
  34. package/dist/interpolation/path.d.ts +43 -0
  35. package/dist/interpolation/path.d.ts.map +1 -0
  36. package/dist/language/language-utils.d.ts +95 -0
  37. package/dist/language/language-utils.d.ts.map +1 -0
  38. package/dist/loaders/fs-backend.d.ts +41 -0
  39. package/dist/loaders/fs-backend.d.ts.map +1 -0
  40. package/dist/loaders/index.d.ts +12 -0
  41. package/dist/loaders/index.d.ts.map +1 -0
  42. package/dist/loaders/index.js +105 -0
  43. package/dist/loaders/index.js.map +11 -0
  44. package/dist/loaders/memory-backend.d.ts +42 -0
  45. package/dist/loaders/memory-backend.d.ts.map +1 -0
  46. package/dist/plural/plural-resolver.d.ts +49 -0
  47. package/dist/plural/plural-resolver.d.ts.map +1 -0
  48. package/dist/resource/resource-loader.d.ts +69 -0
  49. package/dist/resource/resource-loader.d.ts.map +1 -0
  50. package/dist/resource/resource-store.d.ts +93 -0
  51. package/dist/resource/resource-store.d.ts.map +1 -0
  52. package/dist/utils/escape.d.ts +20 -0
  53. package/dist/utils/escape.d.ts.map +1 -0
  54. package/dist/utils/object.d.ts +39 -0
  55. package/dist/utils/object.d.ts.map +1 -0
  56. package/package.json +65 -6
package/README.md CHANGED
@@ -1,3 +1,1239 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # @lhy-meta-server/meta-i18n
2
+
3
+ 面向 Bun 设计、兼容 Node.js 的服务端国际化插件,支持深层属性插值、语言回退、命名空间、复数、上下文、格式化、异步资源加载和请求级语言隔离。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ bun add @lhy-meta-server/meta-i18n
9
+ ```
10
+
11
+ ```bash
12
+ npm install @lhy-meta-server/meta-i18n
13
+ ```
14
+
15
+ 要求 Bun >= 1.1 或 Node.js >= 18.18,仅支持 ESM。
16
+
17
+ ## 导入入口
18
+
19
+ ```ts
20
+ import { createI18n } from '@lhy-meta-server/meta-i18n'
21
+ import { withI18n } from '@lhy-meta-server/meta-i18n/http'
22
+ import { createFsBackend } from '@lhy-meta-server/meta-i18n/loaders'
23
+ ```
24
+
25
+ | 入口 | 内容 |
26
+ | --- | --- |
27
+ | `@lhy-meta-server/meta-i18n` | 核心实例、类型、格式化器和工具函数 |
28
+ | `@lhy-meta-server/meta-i18n/http` | 请求语言检测和 Web 标准 HTTP 包装器 |
29
+ | `@lhy-meta-server/meta-i18n/loaders` | 文件系统、内存和懒加载资源后端 |
30
+
31
+ ## 快速开始
32
+
33
+ ```ts
34
+ import { createI18n } from '@lhy-meta-server/meta-i18n'
35
+
36
+ const i18n = createI18n({
37
+ lng: 'zh-CN',
38
+ fallbackLng: 'en',
39
+ supportedLngs: ['zh-CN', 'en'],
40
+ resources: {
41
+ 'zh-CN': {
42
+ translation: {
43
+ welcome: '欢迎,{{user.profile.name}}!',
44
+ item_one: '{{count}} 件商品',
45
+ item_other: '{{count}} 件商品',
46
+ },
47
+ },
48
+ en: {
49
+ translation: {
50
+ welcome: 'Welcome, {{user.profile.name}}!',
51
+ item_one: '{{count}} item',
52
+ item_other: '{{count}} items',
53
+ },
54
+ },
55
+ },
56
+ })
57
+
58
+ await i18n.init()
59
+
60
+ i18n.t('welcome', { user: { profile: { name: 'Tom' } } })
61
+ // 欢迎,Tom!
62
+
63
+ i18n.t('item', { lng: 'en', count: 3 })
64
+ // 3 items
65
+ ```
66
+ ## 创建实例
67
+
68
+ `createI18n(options)` 返回一个独立的 `I18n` 实例。内联资源在实例创建后即可使用;配置异步资源后端或插件初始化钩子时,应先等待 `init()`。
69
+
70
+ ```ts
71
+ const i18n = createI18n(options)
72
+ await i18n.init()
73
+ ```
74
+
75
+ `init()` 是幂等的,多次调用会复用同一个初始化任务。
76
+
77
+ 也可以直接创建类实例:
78
+
79
+ ```ts
80
+ import { I18n } from '@lhy-meta-server/meta-i18n'
81
+
82
+ const i18n = new I18n(options)
83
+ ```
84
+
85
+ ## 翻译
86
+
87
+ ### 基础翻译
88
+
89
+ ```ts
90
+ i18n.t('home.title')
91
+ i18n.translate('home.title')
92
+ ```
93
+
94
+ `t` 已绑定到实例,可以安全解构:
95
+
96
+ ```ts
97
+ const { t } = i18n
98
+ t('home.title')
99
+ ```
100
+
101
+ 可以提供多个候选键,插件会依次查找,返回第一个存在的翻译:
102
+
103
+ ```ts
104
+ i18n.t(['error.network.timeout', 'error.network.default', 'error.default'])
105
+ ```
106
+
107
+ ### 默认值
108
+
109
+ ```ts
110
+ i18n.t('unknown.key', { defaultValue: '默认内容' })
111
+ i18n.t('unknown.key', { defaultValue: '你好,{{name}}', name: 'Tom' })
112
+ ```
113
+
114
+ 没有 `defaultValue` 时,缺失键默认返回键本身。
115
+
116
+ ### 检查键是否存在
117
+
118
+ ```ts
119
+ i18n.exists('home.title')
120
+ i18n.exists('item', { lng: 'en', count: 2 })
121
+ i18n.exists('friend', { context: 'male' })
122
+ ```
123
+
124
+ `exists()` 会应用语言回退、命名空间、上下文和复数规则。
125
+
126
+ ## 插值
127
+
128
+ ### 深层属性
129
+
130
+ 插值支持任意深度的对象和数组属性:
131
+
132
+ ```json
133
+ {
134
+ "profile": "姓名:{{user.profile.name}}",
135
+ "firstItem": "商品:{{order.items[0].title}}",
136
+ "lastItem": "最后一项:{{order.items[-1].title}}",
137
+ "specialKey": "值:{{data[\"key.with.dot\"]}}"
138
+ }
139
+ ```
140
+
141
+ ```ts
142
+ const result = i18n.t('profile', {
143
+ user: {
144
+ profile: {
145
+ name: 'Tom',
146
+ },
147
+ },
148
+ })
149
+ ```
150
+
151
+ 支持的路径语法:
152
+
153
+ | 语法 | 示例 | 说明 |
154
+ | --- | --- | --- |
155
+ | 点号属性 | `user.profile.name` | 深层对象属性 |
156
+ | 数组下标 | `items[0].title` | 数组元素 |
157
+ | 点号下标 | `items.0.title` | 数组元素的简写形式 |
158
+ | 负数下标 | `items[-1].title` | 从数组末尾读取 |
159
+ | 引号键名 | `data["a.b"]` | 属性名包含点号或空格 |
160
+ | `Map` 键 | `settings.theme` | `settings` 可以是 `Map` |
161
+
162
+ 路径中间值为 `null`、`undefined` 或属性不存在时,会按缺失插值处理,不会抛出异常。
163
+
164
+ 如果插值对象本身含有名为 `a.b` 的扁平属性,该属性优先于深层路径:
165
+
166
+ ```ts
167
+ i18n.t('key', {
168
+ values: {
169
+ 'a.b': '扁平值',
170
+ a: { b: '嵌套值' },
171
+ },
172
+ })
173
+ ```
174
+
175
+ ### 插值变量
176
+
177
+ 变量可以直接放在翻译选项顶层:
178
+
179
+ ```ts
180
+ i18n.t('welcome', {
181
+ user: { profile: { name: 'Tom' } },
182
+ })
183
+ ```
184
+
185
+ 也可以放在 `values` 中。`values` 适用于变量名与 `lng`、`ns`、`count` 等保留选项冲突的场景,并且同名变量优先于顶层变量:
186
+
187
+ ```ts
188
+ i18n.t('message', {
189
+ lng: 'zh-CN',
190
+ values: {
191
+ lng: 'zh-CN 也是模板变量',
192
+ },
193
+ })
194
+ ```
195
+
196
+ ### HTML 转义
197
+
198
+ 插值默认启用 HTML 转义:
199
+
200
+ ```ts
201
+ i18n.t('welcome', { name: '<script>alert(1)</script>' })
202
+ // &lt;script&gt;alert(1)&lt;/script&gt;
203
+ ```
204
+
205
+ 单个插值使用 `{{- path}}` 跳过转义:
206
+
207
+ ```json
208
+ {
209
+ "trustedHtml": "内容:{{- html}}"
210
+ }
211
+ ```
212
+
213
+ 关闭单次调用的转义:
214
+
215
+ ```ts
216
+ i18n.t('welcome', {
217
+ name: '<b>Tom</b>',
218
+ interpolation: { escapeValue: false },
219
+ })
220
+ ```
221
+
222
+ 关闭整个实例的转义:
223
+
224
+ ```ts
225
+ createI18n({
226
+ interpolation: {
227
+ escapeValue: false,
228
+ },
229
+ })
230
+ ```
231
+
232
+ 只有在纯文本或可信内容场景中才应关闭转义。
233
+
234
+ ### 缺失插值
235
+
236
+ ```ts
237
+ createI18n({
238
+ interpolation: {
239
+ missingValue: 'keep',
240
+ },
241
+ })
242
+ ```
243
+
244
+ | 值 | 行为 |
245
+ | --- | --- |
246
+ | `'empty'` | 替换为空字符串,默认行为 |
247
+ | `'keep'` | 保留原始 `{{path}}` |
248
+ | `(path, key) => string` | 使用回调返回值 |
249
+
250
+ ```ts
251
+ createI18n({
252
+ interpolation: {
253
+ missingValue: (path, key) => `[missing ${key}:${path}]`,
254
+ },
255
+ })
256
+ ```
257
+
258
+ 单次翻译也可以覆盖:
259
+
260
+ ```ts
261
+ i18n.t('welcome', {
262
+ interpolation: { missingValue: 'keep' },
263
+ })
264
+ ```
265
+
266
+ ### 自定义插值标记
267
+
268
+ ```ts
269
+ const i18n = createI18n({
270
+ interpolation: {
271
+ prefix: '${',
272
+ suffix: '}',
273
+ },
274
+ resources: {
275
+ en: {
276
+ translation: {
277
+ welcome: 'Hello ${user.name}',
278
+ },
279
+ },
280
+ },
281
+ })
282
+ ```
283
+
284
+ ### 独立读取深层属性
285
+
286
+ ```ts
287
+ import { getByPath, parsePath } from '@lhy-meta-server/meta-i18n'
288
+
289
+ getByPath({ a: { b: [{ c: 1 }] } }, 'a.b[0].c')
290
+ // 1
291
+
292
+ parsePath('a.b[0].c')
293
+ // ['a', 'b', 0, 'c']
294
+ ```
295
+ ## 命名空间
296
+
297
+ 资源按“语言 → 命名空间 → 翻译键”组织:
298
+
299
+ ```ts
300
+ const i18n = createI18n({
301
+ lng: 'zh-CN',
302
+ ns: ['common', 'user'],
303
+ defaultNS: 'common',
304
+ fallbackNS: 'common',
305
+ resources: {
306
+ 'zh-CN': {
307
+ common: {
308
+ button: {
309
+ ok: '确定',
310
+ cancel: '取消',
311
+ },
312
+ },
313
+ user: {
314
+ title: '用户中心',
315
+ },
316
+ },
317
+ },
318
+ })
319
+ ```
320
+
321
+ ```ts
322
+ i18n.t('button.ok')
323
+ i18n.t('user:title')
324
+ i18n.t('title', { ns: 'user' })
325
+ i18n.t('title', { ns: ['user', 'common'] })
326
+ ```
327
+
328
+ 命名空间分隔符默认为 `:`,可修改或关闭:
329
+
330
+ ```ts
331
+ createI18n({ nsSeparator: '::' })
332
+ createI18n({ nsSeparator: false })
333
+ ```
334
+
335
+ ## 复数
336
+
337
+ 复数规则基于 `Intl.PluralRules` 和 CLDR。资源键使用 `_zero`、`_one`、`_two`、`_few`、`_many`、`_other` 后缀:
338
+
339
+ ```json
340
+ {
341
+ "item_zero": "没有商品",
342
+ "item_one": "{{count}} item",
343
+ "item_other": "{{count}} items"
344
+ }
345
+ ```
346
+
347
+ ```ts
348
+ i18n.t('item', { lng: 'en', count: 0 })
349
+ i18n.t('item', { lng: 'en', count: 1 })
350
+ i18n.t('item', { lng: 'en', count: 5 })
351
+ ```
352
+
353
+ 精确数量资源会优先匹配:
354
+
355
+ ```json
356
+ {
357
+ "member_0": "暂无成员",
358
+ "member_1": "只有你",
359
+ "member_other": "共有 {{count}} 位成员"
360
+ }
361
+ ```
362
+
363
+ 序数使用 `_ordinal_*` 后缀:
364
+
365
+ ```json
366
+ {
367
+ "place_ordinal_one": "{{count}}st",
368
+ "place_ordinal_two": "{{count}}nd",
369
+ "place_ordinal_few": "{{count}}rd",
370
+ "place_ordinal_other": "{{count}}th"
371
+ }
372
+ ```
373
+
374
+ ```ts
375
+ i18n.t('place', { lng: 'en', count: 2, ordinal: true })
376
+ // 2nd
377
+ ```
378
+
379
+ 后缀分隔符可以通过 `pluralSeparator` 修改。
380
+
381
+ ## 上下文
382
+
383
+ 使用上下文选择同一个键的不同变体:
384
+
385
+ ```json
386
+ {
387
+ "friend": "一位朋友",
388
+ "friend_male": "一位男性朋友",
389
+ "friend_female": "一位女性朋友"
390
+ }
391
+ ```
392
+
393
+ ```ts
394
+ i18n.t('friend', { context: 'male' })
395
+ i18n.t('friend', { context: 'female' })
396
+ i18n.t('friend', { context: 'unknown' })
397
+ // 未找到 friend_unknown 时回退到 friend
398
+ ```
399
+
400
+ 上下文和复数可以组合:
401
+
402
+ ```json
403
+ {
404
+ "guest_male_one": "{{count}} 位男宾",
405
+ "guest_male_other": "{{count}} 位男宾",
406
+ "guest_female_one": "{{count}} 位女宾",
407
+ "guest_female_other": "{{count}} 位女宾"
408
+ }
409
+ ```
410
+
411
+ 上下文分隔符可以通过 `contextSeparator` 修改。
412
+
413
+ ## 嵌套翻译
414
+
415
+ 使用 `$t(key)` 在翻译文本中引用其他翻译:
416
+
417
+ ```json
418
+ {
419
+ "brand": "Meta",
420
+ "welcome": "欢迎使用 $t(brand)",
421
+ "summary": "状态:$t(status.ready)",
422
+ "cart": "你有 $t(item, {\"count\": 3})"
423
+ }
424
+ ```
425
+
426
+ 嵌套翻译继承父级语言、命名空间和插值变量。嵌套选项必须是合法 JSON。
427
+
428
+ ```ts
429
+ createI18n({
430
+ interpolation: {
431
+ nesting: true,
432
+ maxNestingDepth: 10,
433
+ },
434
+ })
435
+ ```
436
+
437
+ 最大深度用于阻止循环引用。达到限制后,未展开的 `$t(...)` 会保留在结果中。
438
+
439
+ ## 格式化
440
+
441
+ 格式化器名称放在插值路径之后,用逗号分隔。推荐资源模板只声明“如何格式化”,具体参数使用标准 `Intl` 选项从代码传入:
442
+
443
+ ```json
444
+ {
445
+ "amount": "金额:{{price, currency}}",
446
+ "number": "数量:{{count, number}}",
447
+ "percent": "进度:{{progress, percent}}",
448
+ "date": "日期:{{createdAt, datetime}}",
449
+ "relative": "{{days, relativetime}}",
450
+ "users": "用户:{{names, list}}",
451
+ "upper": "{{name, uppercase}}"
452
+ }
453
+ ```
454
+
455
+ ```ts
456
+ i18n.t('amount', {
457
+ price: 99,
458
+ formatParams: {
459
+ price: {
460
+ currency: 'CNY',
461
+ currencyDisplay: 'symbol',
462
+ },
463
+ },
464
+ })
465
+ ```
466
+
467
+ `formatParams` 的键是插值路径,值直接使用对应的 ECMA-402 `Intl` 选项。资源模板只声明格式化器名称,不解析或保存业务参数;货币、时区等选项由代码统一提供。
468
+
469
+ ### 直接格式化
470
+
471
+ 不需要翻译模板时,可以调用 `format()`。选项同样遵循标准 `Intl`:
472
+
473
+ ```ts
474
+ i18n.format(12345.67, 'number', {
475
+ minimumFractionDigits: 2,
476
+ })
477
+
478
+ i18n.format(99, 'currency', {
479
+ currency: 'CNY',
480
+ currencyDisplay: 'symbol',
481
+ })
482
+
483
+ i18n.format(new Date(), 'datetime', {
484
+ dateStyle: 'long',
485
+ timeStyle: 'short',
486
+ timeZone: 'Asia/Shanghai',
487
+ })
488
+
489
+ i18n.format(-1, 'relativetime', {
490
+ unit: 'day',
491
+ numeric: 'auto',
492
+ })
493
+ ```
494
+
495
+ 第四个参数可以覆盖格式化语言:
496
+
497
+ ```ts
498
+ i18n.format(99, 'currency', { currency: 'EUR' }, 'de-DE')
499
+ ```
500
+
501
+ 多个字符串格式化器会从左到右执行:
502
+
503
+ ```json
504
+ {
505
+ "name": "{{name, lowercase, capitalize}}"
506
+ }
507
+ ```
508
+
509
+ | 格式化器 | 关键选项 | 底层标准 |
510
+ | --- | --- | --- |
511
+ | `number` | `minimumFractionDigits`、`notation`、`useGrouping` | `Intl.NumberFormat` |
512
+ | `currency` | `currency`、`currencyDisplay`、`currencySign` | `Intl.NumberFormat` |
513
+ | `percent` | `minimumFractionDigits`、`maximumFractionDigits` | `Intl.NumberFormat` |
514
+ | `datetime` / `date` | `dateStyle`、`timeStyle`、`timeZone` | `Intl.DateTimeFormat` |
515
+ | `relativetime` | `unit`、`numeric`、`style` | `Intl.RelativeTimeFormat` |
516
+ | `list` | `type`、`style` | `Intl.ListFormat` |
517
+ | `uppercase` | 无 | `toLocaleUpperCase` |
518
+ | `lowercase` | 无 | `toLocaleLowerCase` |
519
+ | `capitalize` | 无 | 本地化大小写转换 |
520
+
521
+
522
+ ### 自定义格式化器
523
+
524
+ 初始化时注册:
525
+
526
+ ```ts
527
+ const i18n = createI18n({
528
+ formatters: {
529
+ mask(value, _lng, options) {
530
+ const text = String(value)
531
+ const char = String(options.value ?? '*')
532
+ return char.repeat(Math.max(0, text.length - 4)) + text.slice(-4)
533
+ },
534
+ },
535
+ })
536
+ ```
537
+
538
+ 运行时注册:
539
+
540
+ ```ts
541
+ i18n.addFormatter('trim', (value) => String(value).trim())
542
+ ```
543
+
544
+ 资源中使用:
545
+
546
+ ```json
547
+ {
548
+ "phone": "手机号:{{phone, mask(*)}}"
549
+ }
550
+ ```
551
+ ## 语言与回退
552
+
553
+ ### 切换语言
554
+
555
+ ```ts
556
+ await i18n.changeLanguage('en')
557
+ i18n.language
558
+ // en
559
+ ```
560
+
561
+ `changeLanguage()` 会按需加载目标语言资源并修改实例默认语言。服务端并发请求不应通过它切换请求语言,请使用请求作用域。
562
+
563
+ ### 固定语言翻译函数
564
+
565
+ ```ts
566
+ const t = i18n.getFixedT('zh-CN', 'common', 'button')
567
+
568
+ t('ok')
569
+ // 等价于 i18n.t('button.ok', { lng: 'zh-CN', ns: 'common' })
570
+ ```
571
+
572
+ 参数依次为固定语言、固定命名空间和键前缀,均可省略。调用固定函数时显式传入的 `lng` 或 `ns` 仍可覆盖固定值。
573
+
574
+ ### 请求级语言作用域
575
+
576
+ ```ts
577
+ const result = await i18n.runWithLanguage('en', async () => {
578
+ await loadUser()
579
+ return i18n.t('welcome')
580
+ })
581
+ ```
582
+
583
+ 语言会沿异步调用链传递,多个并发作用域互不影响。作用域结束后实例默认语言不变。
584
+
585
+ ### 语言规范化
586
+
587
+ ```ts
588
+ i18n.resolveLanguage('zh_cn')
589
+ // zh-CN
590
+
591
+ i18n.resolveLanguage('EN-us')
592
+ // en-US 或配置中匹配到的受支持语言
593
+ ```
594
+
595
+ 配置 `supportedLngs` 后,语言按以下顺序匹配:
596
+
597
+ 1. 完整语言标识。
598
+ 2. 主语言标识,例如 `zh-TW` 匹配 `zh`。
599
+ 3. 同主语言的受支持地区,例如 `en-AU` 匹配 `en-US`。
600
+ 4. 第一个回退语言。
601
+
602
+ ### 回退语言
603
+
604
+ 单个回退语言:
605
+
606
+ ```ts
607
+ createI18n({ fallbackLng: 'en' })
608
+ ```
609
+
610
+ 多个回退语言:
611
+
612
+ ```ts
613
+ createI18n({ fallbackLng: ['zh-CN', 'en'] })
614
+ ```
615
+
616
+ 按请求语言配置:
617
+
618
+ ```ts
619
+ createI18n({
620
+ fallbackLng: {
621
+ 'zh-TW': ['zh-CN'],
622
+ 'en-AU': ['en-GB', 'en'],
623
+ default: ['en'],
624
+ },
625
+ })
626
+ ```
627
+
628
+ 动态回退:
629
+
630
+ ```ts
631
+ createI18n({
632
+ fallbackLng: (lng) => (lng.startsWith('zh-') ? ['zh-CN', 'en'] : ['en']),
633
+ })
634
+ ```
635
+
636
+ 关闭回退:
637
+
638
+ ```ts
639
+ createI18n({ fallbackLng: false })
640
+ ```
641
+
642
+ 默认情况下,`zh-Hant-TW` 会形成 `zh-Hant-TW → zh-Hant → zh → fallbackLng` 的查找链。设置 `loadCurrentOnly: true` 后不再追加主语言变体。
643
+
644
+ ## 资源
645
+
646
+ 资源结构为语言、命名空间和资源树三层:
647
+
648
+ ```ts
649
+ const resources = {
650
+ en: {
651
+ common: {
652
+ button: {
653
+ ok: 'OK',
654
+ },
655
+ },
656
+ },
657
+ 'zh-CN': {
658
+ common: {
659
+ button: {
660
+ ok: '确定',
661
+ },
662
+ },
663
+ },
664
+ }
665
+ ```
666
+
667
+ 资源树也可以使用扁平键,或嵌套与扁平混用:
668
+
669
+ ```ts
670
+ {
671
+ 'button.ok': '确定',
672
+ dialog: {
673
+ 'button.cancel': '取消',
674
+ },
675
+ }
676
+ ```
677
+
678
+ ### 文件系统后端
679
+
680
+ 默认文件路径为 `<baseDir>/<lng>/<ns>.json`:
681
+
682
+ ```ts
683
+ import { createI18n } from '@lhy-meta-server/meta-i18n'
684
+ import { createFsBackend } from '@lhy-meta-server/meta-i18n/loaders'
685
+
686
+ const i18n = createI18n({
687
+ lng: 'zh-CN',
688
+ fallbackLng: 'en',
689
+ supportedLngs: ['zh-CN', 'en'],
690
+ ns: ['common', 'user'],
691
+ defaultNS: 'common',
692
+ preload: ['en'],
693
+ backend: createFsBackend({
694
+ baseDir: './locales',
695
+ pathTemplate: '{{lng}}/{{ns}}.json',
696
+ }),
697
+ })
698
+
699
+ await i18n.init()
700
+ ```
701
+
702
+ 目录示例:
703
+
704
+ ```text
705
+ locales/
706
+ ├── en/
707
+ │ ├── common.json
708
+ │ └── user.json
709
+ └── zh-CN/
710
+ ├── common.json
711
+ └── user.json
712
+ ```
713
+
714
+ 自定义解析器可用于 YAML、JSON5 等格式:
715
+
716
+ ```ts
717
+ createFsBackend({
718
+ baseDir: './locales',
719
+ pathTemplate: '{{lng}}/{{ns}}.yaml',
720
+ parse: (content, filePath) => parseYaml(content),
721
+ })
722
+ ```
723
+
724
+ 文件后端会校验语言与命名空间,并同时检查规范路径和真实路径,拒绝 `../` 与符号链接造成的目录逃逸。
725
+
726
+ ### 内存后端
727
+
728
+ ```ts
729
+ import { createMemoryBackend } from '@lhy-meta-server/meta-i18n/loaders'
730
+
731
+ const backend = createMemoryBackend(resources)
732
+ const i18n = createI18n({ backend })
733
+ ```
734
+
735
+ 直接配置 `resources` 通常更简单;内存后端主要用于组合后端和统一加载流程。
736
+
737
+ ### 懒加载后端
738
+
739
+ ```ts
740
+ import { createLazyBackend } from '@lhy-meta-server/meta-i18n/loaders'
741
+
742
+ const backend = createLazyBackend({
743
+ en: {
744
+ common: () => import('./locales/en/common.json', { with: { type: 'json' } }),
745
+ },
746
+ 'zh-CN': {
747
+ common: () => import('./locales/zh-CN/common.json', { with: { type: 'json' } }),
748
+ },
749
+ })
750
+ ```
751
+
752
+ 懒加载函数可返回资源树,或返回包含 `default` 资源树的模块。
753
+
754
+ ### 组合后端
755
+
756
+ ```ts
757
+ import {
758
+ chainBackends,
759
+ createLazyBackend,
760
+ createMemoryBackend,
761
+ } from '@lhy-meta-server/meta-i18n/loaders'
762
+
763
+ const backend = chainBackends(
764
+ remoteBackend,
765
+ createLazyBackend(lazyResources),
766
+ createMemoryBackend(fallbackResources),
767
+ )
768
+ ```
769
+
770
+ 后端按传入顺序读取,返回第一个非 `undefined` 的资源。
771
+
772
+ ### 自定义后端
773
+
774
+ ```ts
775
+ import type { ResourceBackend } from '@lhy-meta-server/meta-i18n'
776
+
777
+ const backend: ResourceBackend = {
778
+ async read(lng, ns) {
779
+ const value = await redis.get(`i18n:${lng}:${ns}`)
780
+ return value ? JSON.parse(value) : undefined
781
+ },
782
+ }
783
+ ```
784
+
785
+ `read(lng, ns)` 应返回资源树或 `undefined`,也可以同步返回。抛出的异常会被转换为 `I18nError` 并触发 `failedLoading` 事件。默认的 `loadFailureMode: 'throw'` 会同时让当前加载操作失败,避免生产请求静默返回错误语言。
786
+
787
+ ### 动态加载资源
788
+
789
+ ```ts
790
+ await i18n.loadLanguages('ja')
791
+ await i18n.loadLanguages(['ja', 'ko'])
792
+ await i18n.loadNamespaces('admin')
793
+ await i18n.loadNamespaces(['admin', 'report'])
794
+ ```
795
+
796
+ 加载语言时会同时加载其语言回退链以及当前登记的全部命名空间。并发加载相同“语言 + 命名空间”时只调用一次后端。
797
+
798
+ 重新加载资源:
799
+
800
+ ```ts
801
+ await i18n.reloadResources()
802
+ await i18n.reloadResources('en')
803
+ await i18n.reloadResources(['en', 'zh-CN'], ['common', 'user'])
804
+ ```
805
+
806
+ ### 运行时修改资源
807
+
808
+ ```ts
809
+ i18n.addResourceBundle(
810
+ 'zh-CN',
811
+ 'common',
812
+ {
813
+ button: {
814
+ save: '保存',
815
+ },
816
+ },
817
+ true,
818
+ true,
819
+ )
820
+ ```
821
+
822
+ `addResourceBundle` 最后两个参数分别表示是否深度合并和是否覆盖已有值,默认均为 `true`。
823
+
824
+ ```ts
825
+ i18n.addResource('zh-CN', 'common', 'button.delete', '删除')
826
+
827
+ i18n.getResourceBundle('zh-CN', 'common') // 返回与内部状态隔离的资源快照
828
+ i18n.hasResourceBundle('zh-CN', 'common')
829
+ i18n.removeResourceBundle('zh-CN', 'common')
830
+
831
+ i18n.toJSON()
832
+ ```
833
+ ## HTTP 集成
834
+
835
+ HTTP 子入口基于 Web 标准的 `Request` 和 `Response`,适用于 Bun.serve、Elysia、Hono 以及其他兼容 Fetch API 的框架。
836
+
837
+ ### Bun.serve
838
+
839
+ ```ts
840
+ import { createI18n } from '@lhy-meta-server/meta-i18n'
841
+ import { withI18n } from '@lhy-meta-server/meta-i18n/http'
842
+
843
+ const i18n = createI18n(options)
844
+ await i18n.init()
845
+
846
+ Bun.serve({
847
+ port: 3000,
848
+ fetch: withI18n(i18n, (request, { lng, t }) => {
849
+ return Response.json({
850
+ language: lng,
851
+ message: t('welcome', {
852
+ user: { profile: { name: 'Tom' } },
853
+ }),
854
+ })
855
+ }),
856
+ })
857
+ ```
858
+
859
+ `withI18n()` 会执行以下操作:
860
+
861
+ 1. 从请求中检测语言。
862
+ 2. 按需加载目标语言资源。
863
+ 3. 在请求级语言作用域中调用处理函数。
864
+ 4. 默认向响应添加 `Content-Language`。
865
+ 5. 根据启用的检测来源合并 `Vary`,防止 CDN 或反向代理混用不同语言响应。
866
+
867
+ Cookie、语言请求头和 `Accept-Language` 会加入 `Vary`;查询参数已属于 URL 缓存键,无需加入。使用自定义检测器时默认写入 `Vary: *`。如应用已自行构造精确缓存键,可以设置 `setVary: false` 后自行管理响应头。
868
+
869
+ 请求处理函数内直接调用 `i18n.t()`,也会读取当前请求语言:
870
+
871
+ ```ts
872
+ const handler = withI18n(i18n, async () => {
873
+ await loadData()
874
+ return new Response(i18n.t('welcome'))
875
+ })
876
+ ```
877
+
878
+ ### 框架上下文
879
+
880
+ ```ts
881
+ import { createRequestContext } from '@lhy-meta-server/meta-i18n/http'
882
+
883
+ const context = createRequestContext(i18n, request)
884
+
885
+ context.lng
886
+ context.t('welcome')
887
+ ```
888
+
889
+ Elysia 示例:
890
+
891
+ ```ts
892
+ new Elysia()
893
+ .derive(({ request }) => createRequestContext(i18n, request))
894
+ .get('/', ({ t }) => t('welcome'))
895
+ ```
896
+
897
+ `createRequestContext()` 只检测和绑定语言,不会加载资源。使用异步资源时,应提前预加载,或者先调用 `loadLanguages(context.lng)`。
898
+
899
+ ### 语言检测
900
+
901
+ 默认检测顺序:
902
+
903
+ 1. 查询参数 `language` 或 `lng`
904
+ 2. Cookie `language` 或 `lng`
905
+ 3. 请求头 `x-language`
906
+ 4. `Accept-Language`
907
+
908
+ 查询参数名和 Cookie 名不区分大小写,因此 `?LANGUAGE=zh-CN`、`?Lng=zh-CN`、`Language=zh-CN` Cookie 都可以识别。若同一来源同时存在两个关键字,按请求中出现的先后顺序取第一个非空值。
909
+
910
+ ```ts
911
+ withI18n(i18n, handler, {
912
+ order: ['query', 'cookie', 'header', 'accept-language'],
913
+ languageKeys: ['locale', 'lang'],
914
+ headerName: 'x-language',
915
+ setContentLanguage: true,
916
+ })
917
+ ```
918
+
919
+ `languageKeys` 同时作用于查询参数和 Cookie,并且始终不区分大小写。传入空数组可以关闭这两种来源的内置关键字读取。
920
+
921
+ 支持自定义检测器,可从会话、JWT、数据库映射或其他位置读取语言:
922
+
923
+ ```ts
924
+ withI18n(i18n, handler, {
925
+ order: [
926
+ (request) => {
927
+ const token = request.headers.get('authorization')
928
+ return token ? readLanguageFromToken(token) : undefined
929
+ },
930
+ 'query',
931
+ 'cookie',
932
+ 'accept-language',
933
+ ],
934
+ })
935
+ ```
936
+
937
+ 检测器可返回单个语言、按优先级排列的语言数组或 `undefined`。
938
+
939
+ 单独创建检测函数:
940
+
941
+ ```ts
942
+ import {
943
+ createLanguageDetector,
944
+ parseAcceptLanguage,
945
+ } from '@lhy-meta-server/meta-i18n/http'
946
+
947
+ const detect = createLanguageDetector(i18n, {
948
+ order: ['accept-language'],
949
+ })
950
+
951
+ const lng = detect(request)
952
+
953
+ parseAcceptLanguage('zh-CN,zh;q=0.9,en;q=0.8')
954
+ // ['zh-CN', 'zh', 'en']
955
+ ```
956
+
957
+ ## 插件
958
+
959
+ 插件可注册格式化器、订阅事件、异步初始化和后处理翻译结果:
960
+
961
+ ```ts
962
+ import type { I18nPlugin } from '@lhy-meta-server/meta-i18n'
963
+
964
+ const debugPlugin: I18nPlugin = {
965
+ name: 'debug',
966
+
967
+ setup(i18n) {
968
+ i18n.addFormatter('debug', (value) => `[${String(value)}]`)
969
+ i18n.on('missingKey', ({ namespace, key }) => {
970
+ console.warn(`Missing translation: ${namespace}:${key}`)
971
+ })
972
+ },
973
+
974
+ async init(i18n) {
975
+ await preparePlugin()
976
+ },
977
+
978
+ postProcess(value, context) {
979
+ return context.missing ? `⚠ ${value}` : value
980
+ },
981
+ }
982
+ ```
983
+
984
+ 初始化时注册:
985
+
986
+ ```ts
987
+ const i18n = createI18n({
988
+ plugins: [debugPlugin],
989
+ })
990
+ ```
991
+
992
+ 运行时注册:
993
+
994
+ ```ts
995
+ i18n.use(debugPlugin)
996
+ ```
997
+
998
+ 同名插件只注册一次。带 `init` 钩子的插件必须在 `i18n.init()` 前注册;实例初始化完成后只能注册不需要初始化的插件,否则会抛出 `INVALID_OPTION`。钩子执行顺序:
999
+
1000
+ | 钩子 | 时机 |
1001
+ | --- | --- |
1002
+ | `setup(i18n)` | 调用 `use()` 时同步执行 |
1003
+ | `init(i18n)` | `init()` 加载资源前按注册顺序等待执行 |
1004
+ | `postProcess(value, context)` | 每次翻译完成后按注册顺序执行 |
1005
+
1006
+ ## 事件
1007
+
1008
+ ```ts
1009
+ const unsubscribe = i18n.on('languageChanged', (lng) => {
1010
+ console.log(`Language changed to ${lng}`)
1011
+ })
1012
+
1013
+ unsubscribe()
1014
+ ```
1015
+
1016
+ 一次性监听:
1017
+
1018
+ ```ts
1019
+ i18n.once('initialized', (options) => {
1020
+ console.log(options)
1021
+ })
1022
+ ```
1023
+
1024
+ 移除监听器:
1025
+
1026
+ ```ts
1027
+ i18n.off('languageChanged', listener)
1028
+ i18n.off('languageChanged')
1029
+ ```
1030
+
1031
+ | 事件 | 回调参数 | 触发时机 |
1032
+ | --- | --- | --- |
1033
+ | `initialized` | `options` | 初始化完成 |
1034
+ | `languageChanged` | `lng` | 实例默认语言发生变化 |
1035
+ | `loaded` | `{ [lng]: ns[] }` | 一批资源加载成功 |
1036
+ | `failedLoading` | `lng, ns, error` | 资源加载失败 |
1037
+ | `added` | `lng, ns` | 添加资源 |
1038
+ | `removed` | `lng, ns` | 移除资源 |
1039
+ | `missingKey` | `info` | 翻译键缺失 |
1040
+
1041
+ 事件监听器抛出的异常不会中断其他监听器和翻译流程。
1042
+
1043
+ ## 缺失键处理
1044
+
1045
+ ```ts
1046
+ const i18n = createI18n({
1047
+ missingKeyHandler(info) {
1048
+ reporter.add({
1049
+ language: info.languages[0],
1050
+ namespace: info.namespace,
1051
+ key: info.key,
1052
+ defaultValue: info.defaultValue,
1053
+ })
1054
+ },
1055
+
1056
+ parseMissingKeyHandler(key, defaultValue) {
1057
+ return defaultValue ?? `[[${key}]]`
1058
+ },
1059
+ })
1060
+ ```
1061
+
1062
+ `missingKeyHandler` 用于记录或上报缺失键;`parseMissingKeyHandler` 用于生成最终返回值。默认行为是返回 `defaultValue`,未提供时返回键。
1063
+
1064
+ 空字符串默认被视为有效翻译。若希望空字符串继续执行回退查找:
1065
+
1066
+ ```ts
1067
+ createI18n({ returnEmptyString: false })
1068
+ ```
1069
+ ## 克隆实例
1070
+
1071
+ ```ts
1072
+ const cloned = i18n.cloneInstance({
1073
+ lng: 'en',
1074
+ })
1075
+
1076
+ await cloned.init()
1077
+ ```
1078
+
1079
+ 克隆实例复制当前资源和插件,并允许覆盖配置。克隆后的默认语言、事件监听和初始化状态相互独立。
1080
+
1081
+ ## 配置
1082
+
1083
+ ### I18nOptions
1084
+
1085
+ | 选项 | 类型 | 默认值 | 说明 |
1086
+ | --- | --- | --- | --- |
1087
+ | `lng` | `string` | 第一个回退语言或 `'en'` | 实例初始语言 |
1088
+ | `fallbackLng` | `string \| string[] \| Record<string, string[]> \| function \| false` | `'en'` | 回退语言配置 |
1089
+ | `supportedLngs` | `string[]` | 未限制 | 允许使用的语言 |
1090
+ | `loadCurrentOnly` | `boolean` | `false` | 只使用完整语言标识,不追加主语言 |
1091
+ | `ns` | `string \| string[]` | `[defaultNS]` | 需要加载的命名空间 |
1092
+ | `defaultNS` | `string` | `'translation'` | 默认命名空间 |
1093
+ | `fallbackNS` | `string \| string[]` | `[]` | 当前命名空间缺失时尝试的命名空间 |
1094
+ | `resources` | `Resources` | `undefined` | 内联资源 |
1095
+ | `backend` | `ResourceBackend` | `undefined` | 异步资源后端 |
1096
+ | `preload` | `string[]` | `[]` | 初始化时额外加载的语言 |
1097
+ | `loadFailureMode` | `'throw' \| 'fallback'` | `'throw'` | 加载失败时抛错,或触发事件后继续回退 |
1098
+ | `keySeparator` | `string \| false` | `'.'` | 翻译键层级分隔符 |
1099
+ | `nsSeparator` | `string \| false` | `':'` | 命名空间分隔符 |
1100
+ | `pluralSeparator` | `string` | `'_'` | 复数后缀分隔符 |
1101
+ | `contextSeparator` | `string` | `'_'` | 上下文后缀分隔符 |
1102
+ | `returnEmptyString` | `boolean` | `true` | 是否将空字符串视为有效翻译 |
1103
+ | `interpolation` | `InterpolationOptions` | 见下表 | 插值配置 |
1104
+ | `formatters` | `Record<string, Formatter>` | `{}` | 自定义格式化器 |
1105
+ | `missingKeyHandler` | `(info) => void` | `undefined` | 缺失键通知函数 |
1106
+ | `parseMissingKeyHandler` | `(key, defaultValue) => string` | 默认值或键 | 缺失键返回值转换 |
1107
+ | `plugins` | `I18nPlugin[]` | `[]` | 初始化时注册的插件 |
1108
+
1109
+ ### InterpolationOptions
1110
+
1111
+ | 选项 | 类型 | 默认值 | 说明 |
1112
+ | --- | --- | --- | --- |
1113
+ | `prefix` | `string` | `'{{'` | 插值起始标记 |
1114
+ | `suffix` | `string` | `'}}'` | 插值结束标记 |
1115
+ | `escapeValue` | `boolean` | `true` | 是否转义插值结果 |
1116
+ | `escape` | `(value) => string` | HTML 转义 | 自定义转义函数 |
1117
+ | `formatSeparator` | `string` | `','` | 插值路径和格式化器之间的分隔符 |
1118
+ | `missingValue` | `'empty' \| 'keep' \| function` | `'empty'` | 插值变量缺失时的行为 |
1119
+ | `nesting` | `boolean` | `true` | 是否启用 `$t()` 嵌套翻译 |
1120
+ | `maxNestingDepth` | `number` | `10` | 最大嵌套深度 |
1121
+
1122
+ ### TranslateOptions
1123
+
1124
+ | 选项 | 类型 | 说明 |
1125
+ | --- | --- | --- |
1126
+ | `lng` | `string` | 覆盖本次翻译语言 |
1127
+ | `ns` | `string \| string[]` | 覆盖本次翻译命名空间 |
1128
+ | `defaultValue` | `string` | 键缺失时使用的文本,支持插值 |
1129
+ | `count` | `number` | 复数计数,同时作为 `{{count}}` 变量 |
1130
+ | `ordinal` | `boolean` | 使用序数复数规则 |
1131
+ | `context` | `string` | 上下文后缀 |
1132
+ | `values` | `Record<string, unknown>` | 显式插值变量,优先于顶层变量 |
1133
+ | `formatParams` | `Record<string, Record<string, unknown>>` | 按插值路径覆盖格式化参数 |
1134
+ | `interpolation.escapeValue` | `boolean` | 覆盖本次转义设置 |
1135
+ | `interpolation.missingValue` | `'empty' \| 'keep' \| function` | 覆盖本次缺失插值策略 |
1136
+ | 其他字段 | `unknown` | 作为插值变量 |
1137
+
1138
+ ### FsBackendOptions
1139
+
1140
+ | 选项 | 类型 | 默认值 | 说明 |
1141
+ | --- | --- | --- | --- |
1142
+ | `baseDir` | `string` | 必填 | 资源根目录 |
1143
+ | `pathTemplate` | `string` | `'{{lng}}/{{ns}}.json'` | 相对文件路径模板 |
1144
+ | `parse` | `(content, filePath) => unknown` | `JSON.parse` | 文件内容解析函数 |
1145
+
1146
+ ### LanguageDetectorOptions
1147
+
1148
+ | 选项 | 类型 | 默认值 | 说明 |
1149
+ | --- | --- | --- | --- |
1150
+ | `order` | `(DetectionSource \| CustomDetector)[]` | `['query', 'cookie', 'header', 'accept-language']` | 检测顺序,可插入自定义检测函数 |
1151
+ | `languageKeys` | `string[]` | `['language', 'lng']` | 查询参数与 Cookie 共用的关键字,不区分大小写 |
1152
+ | `headerName` | `string` | `'x-language'` | 自定义语言请求头 |
1153
+
1154
+ `languageKeys` 可以改为任意关键字或设为空数组。需要从会话、JWT 等位置读取语言时,在 `order` 中传入 `CustomDetector`。
1155
+
1156
+ `withI18n()` 还支持 `setContentLanguage` 和 `setVary`,两者默认均为 `true`。
1157
+
1158
+ ## API
1159
+
1160
+ ### 实例属性
1161
+
1162
+ | 属性 | 类型 | 说明 |
1163
+ | --- | --- | --- |
1164
+ | `language` | `string` | 当前作用域语言或实例默认语言 |
1165
+ | `languages` | `readonly string[]` | 当前语言的完整查找链 |
1166
+ | `isInitialized` | `boolean` | 是否完成初始化 |
1167
+ | `options` | `Readonly<I18nOptions>` | 实例配置 |
1168
+ | `t` | `TFunction` | 已绑定实例的翻译函数 |
1169
+ | `store` | `ResourceStore` | 资源存储 |
1170
+ | `formatters` | `FormatterRegistry` | 格式化器注册表 |
1171
+ | `languageUtils` | `LanguageUtils` | 语言处理工具 |
1172
+
1173
+ ### 实例方法
1174
+
1175
+ | 方法 | 返回值 | 说明 |
1176
+ | --- | --- | --- |
1177
+ | `init()` | `Promise<this>` | 初始化插件并加载资源 |
1178
+ | `t(keys, options?)` | `string` | 翻译 |
1179
+ | `translate(keys, options?)` | `string` | `t` 的方法形式 |
1180
+ | `exists(keys, options?)` | `boolean` | 判断翻译是否存在 |
1181
+ | `getFixedT(lng?, ns?, keyPrefix?)` | `TFunction` | 创建固定上下文翻译函数 |
1182
+ | `changeLanguage(lng)` | `Promise<TFunction>` | 切换实例默认语言 |
1183
+ | `runWithLanguage(lng, operation)` | `Result` | 在请求级语言作用域运行函数 |
1184
+ | `resolveLanguage(lng)` | `string` | 规范化并匹配受支持语言 |
1185
+ | `loadLanguages(lngs)` | `Promise<void>` | 加载语言资源 |
1186
+ | `loadNamespaces(ns)` | `Promise<void>` | 登记并加载命名空间 |
1187
+ | `reloadResources(lngs?, ns?)` | `Promise<void>` | 强制重新加载资源 |
1188
+ | `addResourceBundle(lng, ns, resources, deep?, overwrite?)` | `this` | 添加资源包 |
1189
+ | `addResource(lng, ns, key, value)` | `this` | 添加单个翻译 |
1190
+ | `getResourceBundle(lng, ns)` | `ResourceTree \| undefined` | 获取与内部状态隔离的资源快照 |
1191
+ | `hasResourceBundle(lng, ns)` | `boolean` | 判断资源包是否存在 |
1192
+ | `removeResourceBundle(lng, ns)` | `this` | 删除资源包 |
1193
+ | `toJSON()` | `Resources` | 导出资源快照 |
1194
+ | `format(value, formatter, options?, lng?)` | `string` | 使用标准 `Intl` 选项直接格式化值 |
1195
+ | `addFormatter(name, formatter)` | `void` | 注册格式化器 |
1196
+ | `use(plugin)` | `this` | 注册插件 |
1197
+ | `on(event, listener)` | `() => void` | 订阅事件 |
1198
+ | `once(event, listener)` | `() => void` | 订阅一次性事件 |
1199
+ | `off(event, listener?)` | `void` | 取消事件订阅 |
1200
+ | `cloneInstance(overrides?)` | `I18n` | 克隆实例 |
1201
+
1202
+ ## 错误处理
1203
+
1204
+ ```ts
1205
+ import { I18nError } from '@lhy-meta-server/meta-i18n'
1206
+
1207
+ try {
1208
+ i18n.t('price', { value: 10 })
1209
+ } catch (error) {
1210
+ if (error instanceof I18nError) {
1211
+ console.error(error.code, error.message, error.cause)
1212
+ }
1213
+ }
1214
+ ```
1215
+
1216
+ | 错误码 | 说明 |
1217
+ | --- | --- |
1218
+ | `INVALID_OPTION` | 初始化配置无效 |
1219
+ | `INVALID_PATH` | 直接调用 `parsePath()` 或 `getByPath()` 时路径语法无效 |
1220
+ | `FORMATTER_NOT_FOUND` | 模板使用了未注册的格式化器 |
1221
+ | `FORMAT_FAILED` | 格式化值或 `Intl` 选项无效 |
1222
+ | `INVALID_RESOURCE_IDENTIFIER` | 语言或命名空间含非法字符 |
1223
+ | `LOAD_FAILED` | 资源后端读取失败 |
1224
+
1225
+ 资源加载错误始终触发 `failedLoading`。默认 `loadFailureMode: 'throw'` 还会使 `init()`、资源加载方法和 HTTP 包装器拒绝当前操作;初始化失败后可以再次调用 `init()` 重试:
1226
+
1227
+ ```ts
1228
+ i18n.on('failedLoading', (lng, ns, error) => {
1229
+ console.error(`Failed to load ${lng}/${ns}`, error)
1230
+ })
1231
+ ```
1232
+
1233
+ 如果业务明确允许资源加载失败后继续使用已有资源或键名,可以配置:
1234
+
1235
+ ```ts
1236
+ const i18n = createI18n({
1237
+ loadFailureMode: 'fallback',
1238
+ })
1239
+ ```