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