schema-dsl 2.0.0 → 2.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 (145) hide show
  1. package/CHANGELOG.md +130 -113
  2. package/LICENSE +21 -21
  3. package/README.md +628 -628
  4. package/dist/{DslBuilder-DkLaOo9Q.d.ts → DslBuilder-BIgQOAXp.d.ts} +2 -0
  5. package/dist/{DslBuilder-DQDN0ZxZ.d.cts → DslBuilder-CjHTucNQ.d.cts} +2 -0
  6. package/dist/{Validator-hFWKGxir.d.ts → Validator-CllRdrY0.d.ts} +1 -1
  7. package/dist/{Validator-C7GsVQOH.d.cts → Validator-D6okG9tr.d.cts} +1 -1
  8. package/dist/index.cjs +75 -29
  9. package/dist/index.d.cts +10 -4
  10. package/dist/index.d.ts +10 -4
  11. package/dist/index.js +75 -29
  12. package/dist/plugins/custom-format.cjs +33 -17
  13. package/dist/plugins/custom-format.d.cts +1 -1
  14. package/dist/plugins/custom-format.d.ts +1 -1
  15. package/dist/plugins/custom-format.js +33 -17
  16. package/dist/plugins/custom-type-example.cjs +33 -17
  17. package/dist/plugins/custom-type-example.d.cts +1 -1
  18. package/dist/plugins/custom-type-example.d.ts +1 -1
  19. package/dist/plugins/custom-type-example.js +33 -17
  20. package/dist/plugins/custom-validator.cjs +0 -2
  21. package/dist/plugins/custom-validator.d.cts +1 -1
  22. package/dist/plugins/custom-validator.d.ts +1 -1
  23. package/dist/plugins/custom-validator.js +0 -2
  24. package/docs/FEATURE-INDEX.md +553 -553
  25. package/docs/add-custom-locale.md +496 -496
  26. package/docs/add-keyword.md +24 -24
  27. package/docs/api-reference.md +1047 -1047
  28. package/docs/api.md +13 -13
  29. package/docs/best-practices-project-structure.md +417 -417
  30. package/docs/best-practices.md +712 -712
  31. package/docs/cache-manager.md +344 -344
  32. package/docs/compile.md +45 -45
  33. package/docs/conditional-api.md +1307 -1307
  34. package/docs/custom-extensions-guide.md +339 -339
  35. package/docs/design-philosophy.md +606 -606
  36. package/docs/doc-index.md +324 -324
  37. package/docs/dsl-syntax.md +714 -714
  38. package/docs/dynamic-locale.md +608 -608
  39. package/docs/enum.md +482 -482
  40. package/docs/error-handling.md +1975 -1975
  41. package/docs/export-guide.md +501 -501
  42. package/docs/export-limitations.md +567 -567
  43. package/docs/faq.md +596 -596
  44. package/docs/frontend-i18n-guide.md +307 -307
  45. package/docs/i18n-user-guide.md +487 -487
  46. package/docs/i18n.md +476 -476
  47. package/docs/index.md +48 -48
  48. package/docs/json-schema-basics.md +40 -40
  49. package/docs/label-vs-description.md +271 -271
  50. package/docs/markdown-exporter.md +406 -406
  51. package/docs/mongodb-exporter.md +302 -302
  52. package/docs/multi-language.md +26 -26
  53. package/docs/multi-type-support.md +322 -322
  54. package/docs/mysql-exporter.md +280 -280
  55. package/docs/number-operators.md +449 -449
  56. package/docs/optional-marker-guide.md +326 -326
  57. package/docs/performance-guide.md +49 -49
  58. package/docs/plugin-system.md +381 -381
  59. package/docs/plugin-type-registration.md +34 -34
  60. package/docs/postgresql-exporter.md +311 -311
  61. package/docs/public/favicon.svg +4 -4
  62. package/docs/quick-start.md +435 -435
  63. package/docs/runtime-locale-support.md +532 -532
  64. package/docs/schema-helper.md +345 -345
  65. package/docs/schema-utils-advanced-issues.md +23 -23
  66. package/docs/schema-utils-best-practices.md +20 -20
  67. package/docs/schema-utils-chaining.md +150 -150
  68. package/docs/schema-utils.md +524 -524
  69. package/docs/security-checklist.md +20 -20
  70. package/docs/string-extensions.md +488 -488
  71. package/docs/troubleshooting.md +486 -486
  72. package/docs/type-converter.md +310 -310
  73. package/docs/type-reference.md +242 -242
  74. package/docs/typescript-guide.md +584 -584
  75. package/docs/union-type-guide.md +157 -157
  76. package/docs/union-types.md +284 -284
  77. package/docs/validate-async.md +491 -491
  78. package/docs/validate-batch.md +49 -49
  79. package/docs/validate-dsl-object-support.md +578 -578
  80. package/docs/validate.md +506 -506
  81. package/docs/validation-guide.md +502 -502
  82. package/docs/validator.md +39 -39
  83. package/package.json +131 -131
  84. package/plugins/custom-format.cjs +8 -8
  85. package/plugins/custom-type-example.cjs +8 -8
  86. package/plugins/custom-validator.cjs +8 -8
  87. package/src/adapters/DslAdapter.ts +111 -111
  88. package/src/adapters/index.ts +1 -1
  89. package/src/config/constants.ts +83 -83
  90. package/src/config/index.ts +2 -2
  91. package/src/config/patterns.ts +77 -77
  92. package/src/core/CacheManager.ts +169 -159
  93. package/src/core/ConditionalBuilder.ts +382 -382
  94. package/src/core/ConditionalRuntime.ts +27 -27
  95. package/src/core/ConditionalValidator.ts +254 -254
  96. package/src/core/DslBuilder.ts +687 -677
  97. package/src/core/ErrorCodes.ts +38 -38
  98. package/src/core/ErrorFormatter.ts +271 -271
  99. package/src/core/JSONSchemaCore.ts +65 -65
  100. package/src/core/Locale.ts +187 -187
  101. package/src/core/MessageTemplate.ts +42 -42
  102. package/src/core/ObjectDslBuilder.ts +64 -64
  103. package/src/core/PluginManager.ts +326 -326
  104. package/src/core/StringExtensions.ts +140 -140
  105. package/src/core/TemplateEngine.ts +44 -44
  106. package/src/core/Validator.ts +448 -448
  107. package/src/errors/I18nError.ts +159 -159
  108. package/src/errors/ValidationError.ts +105 -105
  109. package/src/exporters/BaseExporter.ts +60 -60
  110. package/src/exporters/MarkdownExporter.ts +305 -305
  111. package/src/exporters/MongoDBExporter.ts +126 -126
  112. package/src/exporters/MySQLExporter.ts +156 -155
  113. package/src/exporters/PostgreSQLExporter.ts +222 -222
  114. package/src/exporters/index.ts +18 -18
  115. package/src/index.ts +651 -633
  116. package/src/locales/en-US.ts +160 -160
  117. package/src/locales/es-ES.ts +160 -160
  118. package/src/locales/fr-FR.ts +160 -160
  119. package/src/locales/index.ts +103 -103
  120. package/src/locales/ja-JP.ts +160 -160
  121. package/src/locales/types.ts +156 -156
  122. package/src/locales/zh-CN.ts +160 -160
  123. package/src/parser/ConstraintParser.ts +101 -101
  124. package/src/parser/DslParser.ts +470 -470
  125. package/src/parser/SchemaCompiler.ts +66 -66
  126. package/src/parser/TypeRegistry.ts +250 -250
  127. package/src/parser/index.ts +6 -6
  128. package/src/plugins/custom-format.ts +124 -126
  129. package/src/plugins/custom-type-example.ts +106 -108
  130. package/src/plugins/custom-validator.ts +138 -140
  131. package/src/types/conditional.ts +28 -28
  132. package/src/types/config.ts +59 -59
  133. package/src/types/dsl.ts +131 -131
  134. package/src/types/error.ts +60 -60
  135. package/src/types/index.ts +17 -17
  136. package/src/types/infer.ts +127 -127
  137. package/src/types/plugin.ts +58 -58
  138. package/src/types/safe-regex.d.ts +9 -9
  139. package/src/types/schema.ts +66 -66
  140. package/src/types/validate.ts +71 -71
  141. package/src/utils/SchemaHelper.ts +196 -196
  142. package/src/utils/SchemaUtils.ts +365 -346
  143. package/src/utils/TypeConverter.ts +215 -215
  144. package/src/utils/index.ts +10 -10
  145. package/src/validators/CustomKeywords.ts +477 -477
@@ -1,532 +1,532 @@
1
- # 运行时多语言支持 - schema-dsl
2
-
3
- **版本**: v1.1.8+
4
- **更新日期**: 2026-01-30
5
-
6
- ---
7
-
8
- ## 📋 概述
9
-
10
- schema-dsl 的 `dsl.error` 和 `I18nError` 支持**运行时指定语言**,无需修改全局语言设置。
11
-
12
- 这对于 **API 开发**特别有用,可以根据每个请求的语言偏好(如 `Accept-Language` 请求头)动态返回对应语言的错误消息。
13
-
14
- ### 🆕 智能参数识别(v1.1.8)
15
-
16
- **v1.1.8 新增**:支持简化语法,从4个参数减少到2个参数
17
-
18
- ```javascript
19
- // ✅ 新增:简化语法(推荐)
20
- dsl.error.throw('account.notFound', 'zh-CN');
21
- dsl.error.throw('account.notFound', 'zh-CN', 404);
22
-
23
- // ✅ 标准语法(完全兼容)
24
- dsl.error.throw('account.notFound', {}, 404, 'zh-CN');
25
- ```
26
-
27
- **智能识别规则**:
28
- - 第2个参数是 `string` → 识别为语言参数
29
- - 第2个参数是 `object` → 识别为参数对象
30
- - 第2个参数是 `null/undefined/数组` → 使用默认值
31
-
32
- ### 🎨 支持的模板语法(v1.1.4+)
33
-
34
- schema-dsl 现在支持**多种模板语法格式**,提供更好的兼容性:
35
-
36
- | 语法格式 | 示例 | 说明 | 版本 |
37
- |---------|------|------|------|
38
- | `{{#variable}}` | `余额{{#balance}}元` | 井号格式(现有) | v1.0.0+ |
39
- | `{{variable}}` | `余额{{balance}}元` | 无井号格式(新增) | v1.1.4+ |
40
- | `{variable}` | `余额{balance}元` | 单花括号(新增) | v1.1.4+ |
41
- | 混合格式 | `{{#user}}在{date}购买{{product}}` | 可混用多种格式 | v1.1.4+ |
42
-
43
- **示例**:
44
- ```javascript
45
- // 所有格式都支持
46
- Locale.addLocale('zh-CN', {
47
- 'msg1': '余额不足,当前{{#balance}}元', // {{#}} 格式
48
- 'msg2': '用户{{name}}已登录', // {{}} 格式
49
- 'msg3': '订单{orderId}已支付', // {} 格式
50
- 'msg4': '{{#user}}在{date}购买了{{product}}' // 混合格式
51
- });
52
- ```
53
-
54
- **向后兼容**:
55
- - ✅ 现有的 `{{#variable}}` 格式完全兼容
56
- - ✅ 所有单元测试通过
57
- - ✅ 无破坏性变更
58
-
59
- ---
60
-
61
- ## 🎯 三种使用方式
62
-
63
- ### 方式 1: 简化语法(v1.1.8 推荐)⭐
64
-
65
- ```javascript
66
- const { dsl, Locale } = require('schema-dsl');
67
-
68
- // 配置语言包
69
- Locale.addLocale('zh-CN', {
70
- 'account.notFound': {
71
- code: 40001,
72
- message: '账户不存在'
73
- }
74
- });
75
-
76
- Locale.addLocale('en-US', {
77
- 'account.notFound': {
78
- code: 40001,
79
- message: 'Account not found'
80
- }
81
- });
82
-
83
- // ✅ 简化语法:直接传语言参数
84
- const error1 = dsl.error.create('account.notFound', 'zh-CN');
85
- console.log(error1.message); // "账户不存在"
86
-
87
- const error2 = dsl.error.create('account.notFound', 'en-US');
88
- console.log(error2.message); // "Account not found"
89
- ```
90
-
91
- **适用场景**:
92
- - 不需要参数插值
93
- - API 开发中最常见
94
- - 代码最简洁
95
-
96
- ### 方式 2: 全局语言设置(传统方式)
97
-
98
- ```javascript
99
- const { dsl, Locale } = require('schema-dsl');
100
-
101
- // 设置全局语言
102
- Locale.setLocale('zh-CN');
103
-
104
- // 后续所有错误都使用中文
105
- const error1 = dsl.error.create('account.notFound');
106
- console.log(error1.message); // "账户不存在"
107
-
108
- const error2 = dsl.error.create('user.noPermission');
109
- console.log(error2.message); // "没有管理员权限"
110
- ```
111
-
112
- **适用场景**:
113
- - 单一语言的应用
114
- - 不需要动态切换语言
115
- - 简单的错误处理
116
-
117
- ---
118
-
119
- ### 方式 2: 运行时指定语言(推荐用于 API)⭐
120
-
121
- ```javascript
122
- const { dsl, Locale } = require('schema-dsl');
123
-
124
- // 全局保持默认语言
125
- Locale.setLocale('zh-CN');
126
-
127
- // 每次调用时指定语言
128
- const error1 = dsl.error.create('account.notFound', {}, 404, 'zh-CN');
129
- console.log(error1.message); // "账户不存在"
130
-
131
- const error2 = dsl.error.create('account.notFound', {}, 404, 'en-US');
132
- console.log(error2.message); // "Account not found"
133
-
134
- const error3 = dsl.error.create('account.notFound', {}, 404, 'ja-JP');
135
- console.log(error3.message); // "account.notFound"(日语未翻译)
136
- ```
137
-
138
- **适用场景**:
139
- - 多语言 API
140
- - 根据请求头动态返回多语言错误
141
- - 同一请求中需要多种语言
142
- - 微服务架构中的错误传递
143
-
144
- ---
145
-
146
- ## 🔧 API 参数
147
-
148
- ### dsl.error.create()
149
-
150
- ```typescript
151
- dsl.error.create(
152
- code: string, // 错误代码(如 'account.notFound')
153
- params?: object, // 参数插值(如 { balance: 50 })
154
- statusCode?: number, // HTTP 状态码(默认 400)
155
- locale?: string // 🆕 运行时语言(如 'en-US')
156
- ): I18nError
157
- ```
158
-
159
- ### dsl.error.throw()
160
-
161
- ```typescript
162
- dsl.error.throw(
163
- code: string,
164
- params?: object,
165
- statusCode?: number,
166
- locale?: string // 🆕 运行时语言
167
- ): never
168
- ```
169
-
170
- ### dsl.error.assert()
171
-
172
- ```typescript
173
- dsl.error.assert(
174
- condition: any,
175
- code: string,
176
- params?: object,
177
- statusCode?: number,
178
- locale?: string // 🆕 运行时语言
179
- ): void
180
- ```
181
-
182
- ---
183
-
184
- ## 💡 实际应用场景
185
-
186
- ### 场景 1: Express/Koa 中根据请求头返回多语言错误
187
-
188
- ```javascript
189
- const { dsl } = require('schema-dsl');
190
-
191
- function getRequestLocale(acceptLanguage) {
192
- return acceptLanguage?.split(',')[0]?.trim() || 'zh-CN';
193
- }
194
-
195
- // Express 中间件
196
- app.get('/api/account/:id', async (req, res, next) => {
197
- try {
198
- const account = await getAccount(req.params.id);
199
-
200
- // 根据请求头获取语言
201
- const locale = getRequestLocale(req.headers['accept-language']);
202
-
203
- // 使用运行时语言抛出错误
204
- dsl.error.assert(account, 'account.notFound', {}, 404, locale);
205
-
206
- res.json(account);
207
- } catch (error) {
208
- if (error instanceof I18nError) {
209
- return res.status(error.statusCode).json(error.toJSON());
210
- }
211
- next(error);
212
- }
213
- });
214
-
215
- // 请求示例
216
- // 中文客户端: Accept-Language: zh-CN
217
- // 响应: { "code": "account.notFound", "message": "账户不存在", ... }
218
-
219
- // 英文客户端: Accept-Language: en-US
220
- // 响应: { "code": "account.notFound", "message": "Account not found", ... }
221
- ```
222
-
223
- ---
224
-
225
- ### 场景 2: 微服务架构中的错误传递
226
-
227
- ```javascript
228
- const { dsl } = require('schema-dsl');
229
-
230
- // 服务 A: 用户服务
231
- async function getUserService(userId, locale) {
232
- const user = await db.findUser(userId);
233
-
234
- // 传递 locale 到错误
235
- dsl.error.assert(user, 'user.notFound', { userId }, 404, locale);
236
-
237
- return user;
238
- }
239
-
240
- // 服务 B: API 网关
241
- app.get('/api/users/:id', async (req, res) => {
242
- try {
243
- const locale = getRequestLocale(req.headers['accept-language']);
244
-
245
- // 调用用户服务,传递 locale
246
- const user = await getUserService(req.params.id, locale);
247
-
248
- res.json(user);
249
- } catch (error) {
250
- // 错误已经是正确的语言
251
- res.status(error.statusCode).json(error.toJSON());
252
- }
253
- });
254
- ```
255
-
256
- ---
257
-
258
- ### 场景 3: 同一请求中使用多种语言
259
-
260
- ```javascript
261
- const { dsl } = require('schema-dsl');
262
-
263
- // 批量验证,为不同用户返回不同语言的错误
264
- async function batchValidateAccounts(requests) {
265
- const results = [];
266
-
267
- for (const req of requests) {
268
- try {
269
- const account = await getAccount(req.accountId);
270
-
271
- // 每个用户使用各自的语言偏好
272
- dsl.error.assert(
273
- account.balance >= req.amount,
274
- 'account.insufficientBalance',
275
- { balance: account.balance, required: req.amount },
276
- 400,
277
- req.locale // 每个用户的语言偏好
278
- );
279
-
280
- results.push({ success: true, accountId: req.accountId });
281
- } catch (error) {
282
- results.push({
283
- success: false,
284
- accountId: req.accountId,
285
- error: error.toJSON() // 错误已经是对应用户的语言
286
- });
287
- }
288
- }
289
-
290
- return results;
291
- }
292
-
293
- // 调用示例
294
- const results = await batchValidateAccounts([
295
- { accountId: '001', amount: 100, locale: 'zh-CN' }, // 中文用户
296
- { accountId: '002', amount: 200, locale: 'en-US' }, // 英文用户
297
- { accountId: '003', amount: 300, locale: 'ja-JP' } // 日文用户
298
- ]);
299
-
300
- // 结果:每个用户收到对应语言的错误消息
301
- ```
302
-
303
- ---
304
-
305
- ### 场景 4: GraphQL Resolver 中的多语言错误
306
-
307
- ```javascript
308
- const { dsl } = require('schema-dsl');
309
-
310
- const resolvers = {
311
- Query: {
312
- account: async (_, { id }, context) => {
313
- // 从 context 获取用户语言偏好
314
- const locale = context.user?.locale || 'zh-CN';
315
-
316
- const account = await getAccount(id);
317
-
318
- // 使用运行时语言
319
- dsl.error.assert(account, 'account.notFound', {}, 404, locale);
320
-
321
- return account;
322
- }
323
- }
324
- };
325
- ```
326
-
327
- ---
328
-
329
- ## 🔍 运行时语言 vs 全局语言
330
-
331
- ### 对比表
332
-
333
- | 特性 | 全局语言 | 运行时语言 |
334
- |------|---------|-----------|
335
- | 设置方式 | `Locale.setLocale('zh-CN')` | `dsl.error.create(..., locale)` |
336
- | 影响范围 | 全局所有错误 | 仅当前错误 |
337
- | 是否改变全局状态 | ✅ 是 | ❌ 否 |
338
- | 适用场景 | 单一语言应用 | 多语言 API |
339
- | 并发安全 | ⚠️ 需注意 | ✅ 完全安全 |
340
- | 推荐用于 | 简单应用 | API/微服务 |
341
-
342
- ### 并发安全性
343
-
344
- **全局语言**(不推荐用于多语言 API):
345
-
346
- ```javascript
347
- // ❌ 并发不安全
348
- app.get('/api/account/:id', async (req, res) => {
349
- // 修改全局状态
350
- Locale.setLocale(req.headers['accept-language']?.split(',')[0]?.trim() || 'zh-CN');
351
-
352
- // 如果同时有多个请求,语言会互相干扰
353
- const error = dsl.error.create('account.notFound');
354
- // 错误消息可能是错误的语言!
355
- });
356
- ```
357
-
358
- **运行时语言**(推荐):
359
-
360
- ```javascript
361
- // ✅ 并发安全
362
- app.get('/api/account/:id', async (req, res) => {
363
- const locale = req.headers['accept-language']?.split(',')[0]?.trim() || 'zh-CN';
364
-
365
- // 不修改全局状态,每个请求独立
366
- const error = dsl.error.create('account.notFound', {}, 404, locale);
367
- // 错误消息始终是正确的语言
368
- });
369
- ```
370
-
371
- ---
372
-
373
- ## 📊 测试验证
374
-
375
- ### 运行时语言测试
376
-
377
- ```javascript
378
- const { dsl, Locale } = require('schema-dsl');
379
-
380
- // 设置全局为中文
381
- Locale.setLocale('zh-CN');
382
-
383
- // 测试1: 运行时指定不同语言
384
- const error1 = dsl.error.create('account.notFound', {}, 404, 'zh-CN');
385
- const error2 = dsl.error.create('account.notFound', {}, 404, 'en-US');
386
- const error3 = dsl.error.create('account.notFound', {}, 404, 'ja-JP');
387
-
388
- console.log(error1.message); // "账户不存在"
389
- console.log(error2.message); // "Account not found"
390
- console.log(error3.message); // "account.notFound"
391
-
392
- // 测试2: 验证全局语言未被改变
393
- const currentLocale = Locale.getLocale();
394
- console.log(currentLocale); // "zh-CN"
395
-
396
- const error4 = dsl.error.create('user.noPermission'); // 不指定locale
397
- console.log(error4.message); // "没有管理员权限"(使用全局语言)
398
- ```
399
-
400
- ### 带参数的运行时语言
401
-
402
- ```javascript
403
- const error1 = dsl.error.create(
404
- 'account.insufficientBalance',
405
- { balance: 50, required: 100 },
406
- 400,
407
- 'zh-CN'
408
- );
409
- console.log(error1.message); // "余额不足,当前余额50,需要100"
410
-
411
- const error2 = dsl.error.create(
412
- 'account.insufficientBalance',
413
- { balance: 50, required: 100 },
414
- 400,
415
- 'en-US'
416
- );
417
- console.log(error2.message); // "Insufficient balance, current: 50, required: 100"
418
- ```
419
-
420
- ---
421
-
422
- ## 🎯 最佳实践
423
-
424
- ### 1. API 开发中始终使用运行时语言
425
-
426
- ```javascript
427
- // ✅ 推荐
428
- app.get('/api/account/:id', async (req, res) => {
429
- const locale = req.headers['accept-language']?.split(',')[0]?.trim() || 'zh-CN';
430
-
431
- try {
432
- const account = await getAccount(req.params.id);
433
- dsl.error.assert(account, 'account.notFound', {}, 404, locale);
434
- res.json(account);
435
- } catch (error) {
436
- res.status(error.statusCode).json(error.toJSON());
437
- }
438
- });
439
-
440
- // ❌ 不推荐
441
- app.get('/api/account/:id', async (req, res) => {
442
- Locale.setLocale(req.headers['accept-language']?.split(',')[0]?.trim() || 'zh-CN'); // 并发不安全
443
- // ...
444
- });
445
- ```
446
-
447
- ### 2. 统一封装语言获取逻辑
448
-
449
- ```javascript
450
- // 工具函数
451
- function getUserLocale(req) {
452
- return req.user?.locale ||
453
- req.headers['accept-language']?.split(',')[0]?.trim() ||
454
- 'zh-CN';
455
- }
456
-
457
- // 在业务代码中使用
458
- app.get('/api/account/:id', async (req, res) => {
459
- const locale = getUserLocale(req);
460
-
461
- try {
462
- const account = await getAccount(req.params.id);
463
- dsl.error.assert(account, 'account.notFound', {}, 404, locale);
464
- res.json(account);
465
- } catch (error) {
466
- res.status(error.statusCode).json(error.toJSON());
467
- }
468
- });
469
- ```
470
-
471
- ### 3. 在微服务间传递 locale
472
-
473
- ```javascript
474
- // 服务 A: 底层服务
475
- async function getUser(userId, options = {}) {
476
- const user = await db.findUser(userId);
477
-
478
- dsl.error.assert(
479
- user,
480
- 'user.notFound',
481
- { userId },
482
- 404,
483
- options.locale // 接收 locale 参数
484
- );
485
-
486
- return user;
487
- }
488
-
489
- // 服务 B: API 网关
490
- app.get('/api/users/:id', async (req, res) => {
491
- const locale = getUserLocale(req);
492
-
493
- try {
494
- const user = await getUser(req.params.id, { locale });
495
- res.json(user);
496
- } catch (error) {
497
- res.status(error.statusCode).json(error.toJSON());
498
- }
499
- });
500
- ```
501
-
502
- ---
503
-
504
- ## 📝 向后兼容
505
-
506
- ✅ **完全向后兼容**
507
-
508
- - 现有代码无需修改
509
- - `locale` 参数为可选参数
510
- - 不传 `locale` 时使用全局语言
511
- - 相关单元测试已覆盖
512
-
513
- ---
514
-
515
- ## 🔗 相关文档
516
-
517
- - [多语言配置指南](./i18n.md)
518
- - [错误处理完整指南](./error-handling.md)
519
- - [I18nError API 参考](./api-reference.md)
520
-
521
- ---
522
-
523
- ## 对应示例文件
524
-
525
- **示例入口**: [runtime-locale-support.ts](https://github.com/vextjs/schema-dsl/blob/main/examples/docs/runtime-locale-support.ts)
526
- **说明**: 覆盖运行时指定 locale 创建错误对象、参数插值,以及“局部语言切换不污染全局状态”的关键行为。
527
-
528
- ---
529
-
530
- **最后更新**: 2026-05-08
531
- **作者**: schema-dsl Team
532
-
1
+ # 运行时多语言支持 - schema-dsl
2
+
3
+ **版本**: v1.1.8+
4
+ **更新日期**: 2026-01-30
5
+
6
+ ---
7
+
8
+ ## 📋 概述
9
+
10
+ schema-dsl 的 `dsl.error` 和 `I18nError` 支持**运行时指定语言**,无需修改全局语言设置。
11
+
12
+ 这对于 **API 开发**特别有用,可以根据每个请求的语言偏好(如 `Accept-Language` 请求头)动态返回对应语言的错误消息。
13
+
14
+ ### 🆕 智能参数识别(v1.1.8)
15
+
16
+ **v1.1.8 新增**:支持简化语法,从4个参数减少到2个参数
17
+
18
+ ```javascript
19
+ // ✅ 新增:简化语法(推荐)
20
+ dsl.error.throw('account.notFound', 'zh-CN');
21
+ dsl.error.throw('account.notFound', 'zh-CN', 404);
22
+
23
+ // ✅ 标准语法(完全兼容)
24
+ dsl.error.throw('account.notFound', {}, 404, 'zh-CN');
25
+ ```
26
+
27
+ **智能识别规则**:
28
+ - 第2个参数是 `string` → 识别为语言参数
29
+ - 第2个参数是 `object` → 识别为参数对象
30
+ - 第2个参数是 `null/undefined/数组` → 使用默认值
31
+
32
+ ### 🎨 支持的模板语法(v1.1.4+)
33
+
34
+ schema-dsl 现在支持**多种模板语法格式**,提供更好的兼容性:
35
+
36
+ | 语法格式 | 示例 | 说明 | 版本 |
37
+ |---------|------|------|------|
38
+ | `{{#variable}}` | `余额{{#balance}}元` | 井号格式(现有) | v1.0.0+ |
39
+ | `{{variable}}` | `余额{{balance}}元` | 无井号格式(新增) | v1.1.4+ |
40
+ | `{variable}` | `余额{balance}元` | 单花括号(新增) | v1.1.4+ |
41
+ | 混合格式 | `{{#user}}在{date}购买{{product}}` | 可混用多种格式 | v1.1.4+ |
42
+
43
+ **示例**:
44
+ ```javascript
45
+ // 所有格式都支持
46
+ Locale.addLocale('zh-CN', {
47
+ 'msg1': '余额不足,当前{{#balance}}元', // {{#}} 格式
48
+ 'msg2': '用户{{name}}已登录', // {{}} 格式
49
+ 'msg3': '订单{orderId}已支付', // {} 格式
50
+ 'msg4': '{{#user}}在{date}购买了{{product}}' // 混合格式
51
+ });
52
+ ```
53
+
54
+ **向后兼容**:
55
+ - ✅ 现有的 `{{#variable}}` 格式完全兼容
56
+ - ✅ 所有单元测试通过
57
+ - ✅ 无破坏性变更
58
+
59
+ ---
60
+
61
+ ## 🎯 三种使用方式
62
+
63
+ ### 方式 1: 简化语法(v1.1.8 推荐)⭐
64
+
65
+ ```javascript
66
+ const { dsl, Locale } = require('schema-dsl');
67
+
68
+ // 配置语言包
69
+ Locale.addLocale('zh-CN', {
70
+ 'account.notFound': {
71
+ code: 40001,
72
+ message: '账户不存在'
73
+ }
74
+ });
75
+
76
+ Locale.addLocale('en-US', {
77
+ 'account.notFound': {
78
+ code: 40001,
79
+ message: 'Account not found'
80
+ }
81
+ });
82
+
83
+ // ✅ 简化语法:直接传语言参数
84
+ const error1 = dsl.error.create('account.notFound', 'zh-CN');
85
+ console.log(error1.message); // "账户不存在"
86
+
87
+ const error2 = dsl.error.create('account.notFound', 'en-US');
88
+ console.log(error2.message); // "Account not found"
89
+ ```
90
+
91
+ **适用场景**:
92
+ - 不需要参数插值
93
+ - API 开发中最常见
94
+ - 代码最简洁
95
+
96
+ ### 方式 2: 全局语言设置(传统方式)
97
+
98
+ ```javascript
99
+ const { dsl, Locale } = require('schema-dsl');
100
+
101
+ // 设置全局语言
102
+ Locale.setLocale('zh-CN');
103
+
104
+ // 后续所有错误都使用中文
105
+ const error1 = dsl.error.create('account.notFound');
106
+ console.log(error1.message); // "账户不存在"
107
+
108
+ const error2 = dsl.error.create('user.noPermission');
109
+ console.log(error2.message); // "没有管理员权限"
110
+ ```
111
+
112
+ **适用场景**:
113
+ - 单一语言的应用
114
+ - 不需要动态切换语言
115
+ - 简单的错误处理
116
+
117
+ ---
118
+
119
+ ### 方式 2: 运行时指定语言(推荐用于 API)⭐
120
+
121
+ ```javascript
122
+ const { dsl, Locale } = require('schema-dsl');
123
+
124
+ // 全局保持默认语言
125
+ Locale.setLocale('zh-CN');
126
+
127
+ // 每次调用时指定语言
128
+ const error1 = dsl.error.create('account.notFound', {}, 404, 'zh-CN');
129
+ console.log(error1.message); // "账户不存在"
130
+
131
+ const error2 = dsl.error.create('account.notFound', {}, 404, 'en-US');
132
+ console.log(error2.message); // "Account not found"
133
+
134
+ const error3 = dsl.error.create('account.notFound', {}, 404, 'ja-JP');
135
+ console.log(error3.message); // "account.notFound"(日语未翻译)
136
+ ```
137
+
138
+ **适用场景**:
139
+ - 多语言 API
140
+ - 根据请求头动态返回多语言错误
141
+ - 同一请求中需要多种语言
142
+ - 微服务架构中的错误传递
143
+
144
+ ---
145
+
146
+ ## 🔧 API 参数
147
+
148
+ ### dsl.error.create()
149
+
150
+ ```typescript
151
+ dsl.error.create(
152
+ code: string, // 错误代码(如 'account.notFound')
153
+ params?: object, // 参数插值(如 { balance: 50 })
154
+ statusCode?: number, // HTTP 状态码(默认 400)
155
+ locale?: string // 🆕 运行时语言(如 'en-US')
156
+ ): I18nError
157
+ ```
158
+
159
+ ### dsl.error.throw()
160
+
161
+ ```typescript
162
+ dsl.error.throw(
163
+ code: string,
164
+ params?: object,
165
+ statusCode?: number,
166
+ locale?: string // 🆕 运行时语言
167
+ ): never
168
+ ```
169
+
170
+ ### dsl.error.assert()
171
+
172
+ ```typescript
173
+ dsl.error.assert(
174
+ condition: any,
175
+ code: string,
176
+ params?: object,
177
+ statusCode?: number,
178
+ locale?: string // 🆕 运行时语言
179
+ ): void
180
+ ```
181
+
182
+ ---
183
+
184
+ ## 💡 实际应用场景
185
+
186
+ ### 场景 1: Express/Koa 中根据请求头返回多语言错误
187
+
188
+ ```javascript
189
+ const { dsl } = require('schema-dsl');
190
+
191
+ function getRequestLocale(acceptLanguage) {
192
+ return acceptLanguage?.split(',')[0]?.trim() || 'zh-CN';
193
+ }
194
+
195
+ // Express 中间件
196
+ app.get('/api/account/:id', async (req, res, next) => {
197
+ try {
198
+ const account = await getAccount(req.params.id);
199
+
200
+ // 根据请求头获取语言
201
+ const locale = getRequestLocale(req.headers['accept-language']);
202
+
203
+ // 使用运行时语言抛出错误
204
+ dsl.error.assert(account, 'account.notFound', {}, 404, locale);
205
+
206
+ res.json(account);
207
+ } catch (error) {
208
+ if (error instanceof I18nError) {
209
+ return res.status(error.statusCode).json(error.toJSON());
210
+ }
211
+ next(error);
212
+ }
213
+ });
214
+
215
+ // 请求示例
216
+ // 中文客户端: Accept-Language: zh-CN
217
+ // 响应: { "code": "account.notFound", "message": "账户不存在", ... }
218
+
219
+ // 英文客户端: Accept-Language: en-US
220
+ // 响应: { "code": "account.notFound", "message": "Account not found", ... }
221
+ ```
222
+
223
+ ---
224
+
225
+ ### 场景 2: 微服务架构中的错误传递
226
+
227
+ ```javascript
228
+ const { dsl } = require('schema-dsl');
229
+
230
+ // 服务 A: 用户服务
231
+ async function getUserService(userId, locale) {
232
+ const user = await db.findUser(userId);
233
+
234
+ // 传递 locale 到错误
235
+ dsl.error.assert(user, 'user.notFound', { userId }, 404, locale);
236
+
237
+ return user;
238
+ }
239
+
240
+ // 服务 B: API 网关
241
+ app.get('/api/users/:id', async (req, res) => {
242
+ try {
243
+ const locale = getRequestLocale(req.headers['accept-language']);
244
+
245
+ // 调用用户服务,传递 locale
246
+ const user = await getUserService(req.params.id, locale);
247
+
248
+ res.json(user);
249
+ } catch (error) {
250
+ // 错误已经是正确的语言
251
+ res.status(error.statusCode).json(error.toJSON());
252
+ }
253
+ });
254
+ ```
255
+
256
+ ---
257
+
258
+ ### 场景 3: 同一请求中使用多种语言
259
+
260
+ ```javascript
261
+ const { dsl } = require('schema-dsl');
262
+
263
+ // 批量验证,为不同用户返回不同语言的错误
264
+ async function batchValidateAccounts(requests) {
265
+ const results = [];
266
+
267
+ for (const req of requests) {
268
+ try {
269
+ const account = await getAccount(req.accountId);
270
+
271
+ // 每个用户使用各自的语言偏好
272
+ dsl.error.assert(
273
+ account.balance >= req.amount,
274
+ 'account.insufficientBalance',
275
+ { balance: account.balance, required: req.amount },
276
+ 400,
277
+ req.locale // 每个用户的语言偏好
278
+ );
279
+
280
+ results.push({ success: true, accountId: req.accountId });
281
+ } catch (error) {
282
+ results.push({
283
+ success: false,
284
+ accountId: req.accountId,
285
+ error: error.toJSON() // 错误已经是对应用户的语言
286
+ });
287
+ }
288
+ }
289
+
290
+ return results;
291
+ }
292
+
293
+ // 调用示例
294
+ const results = await batchValidateAccounts([
295
+ { accountId: '001', amount: 100, locale: 'zh-CN' }, // 中文用户
296
+ { accountId: '002', amount: 200, locale: 'en-US' }, // 英文用户
297
+ { accountId: '003', amount: 300, locale: 'ja-JP' } // 日文用户
298
+ ]);
299
+
300
+ // 结果:每个用户收到对应语言的错误消息
301
+ ```
302
+
303
+ ---
304
+
305
+ ### 场景 4: GraphQL Resolver 中的多语言错误
306
+
307
+ ```javascript
308
+ const { dsl } = require('schema-dsl');
309
+
310
+ const resolvers = {
311
+ Query: {
312
+ account: async (_, { id }, context) => {
313
+ // 从 context 获取用户语言偏好
314
+ const locale = context.user?.locale || 'zh-CN';
315
+
316
+ const account = await getAccount(id);
317
+
318
+ // 使用运行时语言
319
+ dsl.error.assert(account, 'account.notFound', {}, 404, locale);
320
+
321
+ return account;
322
+ }
323
+ }
324
+ };
325
+ ```
326
+
327
+ ---
328
+
329
+ ## 🔍 运行时语言 vs 全局语言
330
+
331
+ ### 对比表
332
+
333
+ | 特性 | 全局语言 | 运行时语言 |
334
+ |------|---------|-----------|
335
+ | 设置方式 | `Locale.setLocale('zh-CN')` | `dsl.error.create(..., locale)` |
336
+ | 影响范围 | 全局所有错误 | 仅当前错误 |
337
+ | 是否改变全局状态 | ✅ 是 | ❌ 否 |
338
+ | 适用场景 | 单一语言应用 | 多语言 API |
339
+ | 并发安全 | ⚠️ 需注意 | ✅ 完全安全 |
340
+ | 推荐用于 | 简单应用 | API/微服务 |
341
+
342
+ ### 并发安全性
343
+
344
+ **全局语言**(不推荐用于多语言 API):
345
+
346
+ ```javascript
347
+ // ❌ 并发不安全
348
+ app.get('/api/account/:id', async (req, res) => {
349
+ // 修改全局状态
350
+ Locale.setLocale(req.headers['accept-language']?.split(',')[0]?.trim() || 'zh-CN');
351
+
352
+ // 如果同时有多个请求,语言会互相干扰
353
+ const error = dsl.error.create('account.notFound');
354
+ // 错误消息可能是错误的语言!
355
+ });
356
+ ```
357
+
358
+ **运行时语言**(推荐):
359
+
360
+ ```javascript
361
+ // ✅ 并发安全
362
+ app.get('/api/account/:id', async (req, res) => {
363
+ const locale = req.headers['accept-language']?.split(',')[0]?.trim() || 'zh-CN';
364
+
365
+ // 不修改全局状态,每个请求独立
366
+ const error = dsl.error.create('account.notFound', {}, 404, locale);
367
+ // 错误消息始终是正确的语言
368
+ });
369
+ ```
370
+
371
+ ---
372
+
373
+ ## 📊 测试验证
374
+
375
+ ### 运行时语言测试
376
+
377
+ ```javascript
378
+ const { dsl, Locale } = require('schema-dsl');
379
+
380
+ // 设置全局为中文
381
+ Locale.setLocale('zh-CN');
382
+
383
+ // 测试1: 运行时指定不同语言
384
+ const error1 = dsl.error.create('account.notFound', {}, 404, 'zh-CN');
385
+ const error2 = dsl.error.create('account.notFound', {}, 404, 'en-US');
386
+ const error3 = dsl.error.create('account.notFound', {}, 404, 'ja-JP');
387
+
388
+ console.log(error1.message); // "账户不存在"
389
+ console.log(error2.message); // "Account not found"
390
+ console.log(error3.message); // "account.notFound"
391
+
392
+ // 测试2: 验证全局语言未被改变
393
+ const currentLocale = Locale.getLocale();
394
+ console.log(currentLocale); // "zh-CN"
395
+
396
+ const error4 = dsl.error.create('user.noPermission'); // 不指定locale
397
+ console.log(error4.message); // "没有管理员权限"(使用全局语言)
398
+ ```
399
+
400
+ ### 带参数的运行时语言
401
+
402
+ ```javascript
403
+ const error1 = dsl.error.create(
404
+ 'account.insufficientBalance',
405
+ { balance: 50, required: 100 },
406
+ 400,
407
+ 'zh-CN'
408
+ );
409
+ console.log(error1.message); // "余额不足,当前余额50,需要100"
410
+
411
+ const error2 = dsl.error.create(
412
+ 'account.insufficientBalance',
413
+ { balance: 50, required: 100 },
414
+ 400,
415
+ 'en-US'
416
+ );
417
+ console.log(error2.message); // "Insufficient balance, current: 50, required: 100"
418
+ ```
419
+
420
+ ---
421
+
422
+ ## 🎯 最佳实践
423
+
424
+ ### 1. API 开发中始终使用运行时语言
425
+
426
+ ```javascript
427
+ // ✅ 推荐
428
+ app.get('/api/account/:id', async (req, res) => {
429
+ const locale = req.headers['accept-language']?.split(',')[0]?.trim() || 'zh-CN';
430
+
431
+ try {
432
+ const account = await getAccount(req.params.id);
433
+ dsl.error.assert(account, 'account.notFound', {}, 404, locale);
434
+ res.json(account);
435
+ } catch (error) {
436
+ res.status(error.statusCode).json(error.toJSON());
437
+ }
438
+ });
439
+
440
+ // ❌ 不推荐
441
+ app.get('/api/account/:id', async (req, res) => {
442
+ Locale.setLocale(req.headers['accept-language']?.split(',')[0]?.trim() || 'zh-CN'); // 并发不安全
443
+ // ...
444
+ });
445
+ ```
446
+
447
+ ### 2. 统一封装语言获取逻辑
448
+
449
+ ```javascript
450
+ // 工具函数
451
+ function getUserLocale(req) {
452
+ return req.user?.locale ||
453
+ req.headers['accept-language']?.split(',')[0]?.trim() ||
454
+ 'zh-CN';
455
+ }
456
+
457
+ // 在业务代码中使用
458
+ app.get('/api/account/:id', async (req, res) => {
459
+ const locale = getUserLocale(req);
460
+
461
+ try {
462
+ const account = await getAccount(req.params.id);
463
+ dsl.error.assert(account, 'account.notFound', {}, 404, locale);
464
+ res.json(account);
465
+ } catch (error) {
466
+ res.status(error.statusCode).json(error.toJSON());
467
+ }
468
+ });
469
+ ```
470
+
471
+ ### 3. 在微服务间传递 locale
472
+
473
+ ```javascript
474
+ // 服务 A: 底层服务
475
+ async function getUser(userId, options = {}) {
476
+ const user = await db.findUser(userId);
477
+
478
+ dsl.error.assert(
479
+ user,
480
+ 'user.notFound',
481
+ { userId },
482
+ 404,
483
+ options.locale // 接收 locale 参数
484
+ );
485
+
486
+ return user;
487
+ }
488
+
489
+ // 服务 B: API 网关
490
+ app.get('/api/users/:id', async (req, res) => {
491
+ const locale = getUserLocale(req);
492
+
493
+ try {
494
+ const user = await getUser(req.params.id, { locale });
495
+ res.json(user);
496
+ } catch (error) {
497
+ res.status(error.statusCode).json(error.toJSON());
498
+ }
499
+ });
500
+ ```
501
+
502
+ ---
503
+
504
+ ## 📝 向后兼容
505
+
506
+ ✅ **完全向后兼容**
507
+
508
+ - 现有代码无需修改
509
+ - `locale` 参数为可选参数
510
+ - 不传 `locale` 时使用全局语言
511
+ - 相关单元测试已覆盖
512
+
513
+ ---
514
+
515
+ ## 🔗 相关文档
516
+
517
+ - [多语言配置指南](./i18n.md)
518
+ - [错误处理完整指南](./error-handling.md)
519
+ - [I18nError API 参考](./api-reference.md)
520
+
521
+ ---
522
+
523
+ ## 对应示例文件
524
+
525
+ **示例入口**: [runtime-locale-support.ts](https://github.com/vextjs/schema-dsl/blob/main/examples/docs/runtime-locale-support.ts)
526
+ **说明**: 覆盖运行时指定 locale 创建错误对象、参数插值,以及“局部语言切换不污染全局状态”的关键行为。
527
+
528
+ ---
529
+
530
+ **最后更新**: 2026-05-08
531
+ **作者**: schema-dsl Team
532
+