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,598 +1,608 @@
1
- # 动态多语言配置指南
2
-
3
- > **更新时间**: 2025-12-25
4
- > **场景**: 从请求头动态获取语言配置
5
-
6
- ---
7
-
8
- ## 📑 目录
9
-
10
- - [基本原理](#基本原理)
11
- - [方案1: 验证时指定语言(推荐)](#方案1-验证时指定语言推荐)
12
- - [方案2: 临时切换语言](#方案2-临时切换语言)
13
- - [方案3: Express/Koa 中间件](#方案3-expresskoa-中间件)
14
- - [完整示例](#完整示例)
15
- - [最佳实践](#最佳实践)
16
-
17
- ---
18
-
19
- ## 基本原理
20
-
21
- SchemaI-DSL 的 `Validator` 支持在验证时动态指定语言,无需全局切换。
22
-
23
- ### 核心方法
24
-
25
- ```javascript
26
- validator.validate(schema, data, {
27
- locale: 'zh-CN' // 动态指定语言
28
- });
29
- ```
30
-
31
- ---
32
-
33
- ## 方案1: 验证时指定语言(推荐)✅
34
-
35
- 这是**最推荐**的方案,无需修改全局状态,支持并发请求。
36
-
37
- ### 1.1 应用启动时配置(一次性加载所有语言)
38
-
39
- 使用 `dsl.config` 在应用启动时一次性加载所有自定义语言包。
40
-
41
- ```javascript
42
- const { dsl, validate } = require('schema-dsl');
43
- const path = require('path');
44
-
45
- // ========== 应用启动时配置(只执行一次)==========
46
-
47
- // 方式一:传入目录路径(推荐)⭐
48
- // 自动扫描目录下的所有 .js 和 .json 文件
49
- dsl.config({
50
- i18n: path.join(__dirname, 'locales')
51
- });
52
-
53
- // 方式二:直接传入对象
54
- dsl.config({
55
- i18n: {
56
- 'fr-FR': {
57
- 'required': '{{#label}} est requis',
58
- 'string.minLength': '{{#label}} doit contenir au moins {{#limit}} caractères'
59
- },
60
- 'de-DE': {
61
- 'required': '{{#label}} ist erforderlich',
62
- 'string.minLength': '{{#label}} muss mindestens {{#limit}} Zeichen lang sein'
63
- }
64
- }
65
- });
66
-
67
- // 说明:
68
- // - 只在应用启动时执行一次
69
- // - 自动与系统内置语言包合并(用户自定义的优先)
70
- // - 运行时无需重新加载,直接切换
71
- ```
72
-
73
- ### 1.2 运行时直接切换语言(无需重新加载)
74
-
75
- ```javascript
76
- const { dsl, validate } = require('schema-dsl');
77
-
78
- // 定义 Schema
79
- const schema = dsl({
80
- username: 'string:3-32!',
81
- email: 'email!'
82
- });
83
-
84
- // 测试数据
85
- const data = { username: 'ab', email: 'invalid' };
86
-
87
- // ========== 运行时直接切换语言 ==========
88
-
89
- // 使用中文
90
- const result1 = validate(schema, data, { locale: 'zh-CN' });
91
- // 错误: "username长度不能少于3个字符"
92
-
93
- // 使用法语
94
- const result2 = validate(schema, data, { locale: 'fr-FR' });
95
- // 错误: "username doit contenir au moins 3 caractères"
96
-
97
- // 使用德语
98
- const result3 = validate(schema, data, { locale: 'de-DE' });
99
- // 错误: "username muss mindestens 3 Zeichen lang sein"
100
-
101
- // 说明:
102
- // - 无需重新加载语言包
103
- // - 每次验证可以使用不同语言
104
- // - 支持高并发(无全局状态修改)
105
- ```
106
-
107
- ### 1.3 从请求头获取语言(实际应用场景)
108
-
109
- ```javascript
110
- const express = require('express');
111
- const { dsl, validate } = require('schema-dsl');
112
- const path = require('path');
113
-
114
- const app = express();
115
-
116
- // ========== 应用启动时配置(只执行一次)==========
117
- dsl.config({
118
- i18n: path.join(__dirname, 'locales')
119
- });
120
-
121
- // 定义 Schema
122
- const userSchema = dsl({
123
- username: 'string:3-32!',
124
- email: 'email!',
125
- password: 'string:8-32!'
126
- });
127
-
128
- // ========== Express 路由 ==========
129
- app.post('/api/user/register', (req, res) => {
130
- // 从请求头获取语言偏好
131
- const locale = req.headers['accept-language'] || 'en-US';
132
-
133
- // 验证数据(直接切换语言,无需重新加载)
134
- const result = validate(userSchema, req.body, { locale });
135
-
136
- if (!result.valid) {
137
- return res.status(400).json({
138
- errors: result.errors // 自动使用用户偏好的语言
139
- });
140
- }
141
-
142
- // 处理成功...
143
- res.json({ message: 'User registered successfully' });
144
- });
145
- ```
146
-
147
- ### 1.3 解析 Accept-Language 头
148
-
149
- ```javascript
150
- /**
151
- * 解析 Accept-Language 头
152
- * @param {string} acceptLanguage - Accept-Language 头的值
153
- * @returns {string} 语言代码
154
- */
155
- function parseAcceptLanguage(acceptLanguage) {
156
- if (!acceptLanguage) return 'en-US';
157
-
158
- // Accept-Language 格式: zh-CN,zh;q=0.9,en;q=0.8
159
- const languages = acceptLanguage.split(',').map(lang => {
160
- const [code, qValue] = lang.trim().split(';');
161
- const q = qValue ? parseFloat(qValue.split('=')[1]) : 1.0;
162
- return { code: code.trim(), q };
163
- });
164
-
165
- // 按权重排序
166
- languages.sort((a, b) => b.q - a.q);
167
-
168
- // 映射到支持的语言
169
- const supportedLocales = ['zh-CN', 'en-US', 'ja-JP'];
170
- for (const lang of languages) {
171
- const matched = supportedLocales.find(locale =>
172
- locale.toLowerCase() === lang.code.toLowerCase() ||
173
- locale.split('-')[0] === lang.code.split('-')[0]
174
- );
175
- if (matched) return matched;
176
- }
177
-
178
- return 'en-US'; // 默认语言
179
- }
180
-
181
- // 使用
182
- app.post('/api/user/register', (req, res) => {
183
- const locale = parseAcceptLanguage(req.headers['accept-language']);
184
-
185
- const result = validator.validate(schema, req.body, { locale });
186
-
187
- // ...
188
- });
189
- ```
190
-
191
- ---
192
-
193
- ## 方案2: 临时切换语言
194
-
195
- 适用于少数场景。
196
-
197
- ### 2.1 使用闭包保存原语言
198
-
199
- ```javascript
200
- function validateWithLocale(validator, schema, data, locale) {
201
- const originalLocale = Locale.getLocale();
202
-
203
- try {
204
- Locale.setLocale(locale);
205
- return validator.validate(schema, data);
206
- } finally {
207
- Locale.setLocale(originalLocale); // 恢复原语言
208
- }
209
- }
210
-
211
- // 使用
212
- app.post('/api/user/register', (req, res) => {
213
- const locale = req.headers['accept-language'] || 'en-US';
214
-
215
- const result = validateWithLocale(validator, schema, req.body, locale);
216
-
217
- // ...
218
- });
219
- ```
220
-
221
- ---
222
-
223
- ## 方案3: Express/Koa 中间件
224
-
225
- 封装为中间件,自动处理语言切换。
226
-
227
- ### 3.1 Express 中间件 (推荐)
228
-
229
- 通过中间件一次性配置,后续业务代码无需关心语言参数。
230
-
231
- ```javascript
232
- const { Validator } = require('schema-dsl');
233
- const validator = new Validator();
234
-
235
- const schemaIoMiddleware = (req, res, next) => {
236
- // 1. 自动获取语言
237
- const lang = req.headers['accept-language'] || 'en-US';
238
- // 简单匹配逻辑 (实际可使用 accept-language-parser)
239
- const locale = lang.includes('zh') ? 'zh-CN' :
240
- lang.includes('ja') ? 'ja-JP' :
241
- lang.includes('es') ? 'es-ES' :
242
- lang.includes('fr') ? 'fr-FR' : 'en-US';
243
-
244
- // 2. 挂载绑定了语言的验证方法
245
- req.validate = (schema, data) => {
246
- return validator.validate(schema, data, { locale });
247
- };
248
-
249
- next();
250
- };
251
-
252
- app.use(schemaIoMiddleware);
253
-
254
- // 业务中使用
255
- app.post('/users', (req, res) => {
256
- // 直接调用,自动使用中间件解析的语言
257
- const result = req.validate(userSchema, req.body);
258
-
259
- if (!result.valid) {
260
- return res.status(400).json({ errors: result.errors });
261
- }
262
-
263
- // ...
264
- });
265
- ```
266
-
267
- 完整示例请参考 `examples/middleware-usage.js`。
268
-
269
- ### 3.2 Koa 中间件
270
-
271
- ```javascript
272
- const { Locale } = require('schema-dsl');
273
-
274
- /**
275
- * Koa 语言中间件
276
- */
277
- function localeMiddleware() {
278
- return async (ctx, next) => {
279
- // 解析语言
280
- const locale = parseAcceptLanguage(ctx.headers['accept-language']);
281
-
282
- // 保存到上下文
283
- ctx.locale = locale;
284
-
285
- // 创建验证辅助函数
286
- ctx.validate = function(schema, data) {
287
- const { Validator } = require('schema-dsl');
288
- const validator = new Validator();
289
- return validator.validate(schema, data, { locale: ctx.locale });
290
- };
291
-
292
- await next();
293
- };
294
- }
295
-
296
- // 应用中间件
297
- app.use(localeMiddleware());
298
-
299
- // 使用
300
- router.post('/api/user/register', async (ctx) => {
301
- // 自动使用请求的语言
302
- const result = ctx.validate(userSchema, ctx.request.body);
303
-
304
- if (!result.valid) {
305
- ctx.status = 400;
306
- ctx.body = { errors: result.errors };
307
- return;
308
- }
309
-
310
- // ...
311
- });
312
- ```
313
-
314
- ---
315
-
316
- ## 完整示例
317
-
318
- ### Express 完整示例
319
-
320
- ```javascript
321
- const express = require('express');
322
- const { dsl, Validator, Locale } = require('schema-dsl');
323
-
324
- const app = express();
325
- app.use(express.json());
326
-
327
- // ========== 1. 初始化语言包 ==========
328
-
329
- Locale.addLocale('zh-CN', {
330
- 'required': '{{#label}}不能为空',
331
- 'min': '{{#label}}至少{{#limit}}个字符',
332
- 'max': '{{#label}}最多{{#limit}}个字符',
333
- 'pattern': '{{#label}}格式不正确',
334
- 'format': '请输入有效的{{#label}}'
335
- });
336
-
337
- Locale.addLocale('en-US', {
338
- 'required': '{{#label}} is required',
339
- 'min': '{{#label}} must be at least {{#limit}} characters',
340
- 'max': '{{#label}} must be at most {{#limit}} characters',
341
- 'pattern': '{{#label}} format is invalid',
342
- 'format': 'Please enter a valid {{#label}}'
343
- });
344
-
345
- // ========== 2. 工具函数 ==========
346
-
347
- function parseAcceptLanguage(acceptLanguage) {
348
- if (!acceptLanguage) return 'en-US';
349
-
350
- const languages = acceptLanguage.split(',').map(lang => {
351
- const [code, qValue] = lang.trim().split(';');
352
- const q = qValue ? parseFloat(qValue.split('=')[1]) : 1.0;
353
- return { code: code.trim(), q };
354
- });
355
-
356
- languages.sort((a, b) => b.q - a.q);
357
-
358
- const supportedLocales = ['zh-CN', 'en-US'];
359
- for (const lang of languages) {
360
- const matched = supportedLocales.find(locale =>
361
- locale.toLowerCase() === lang.code.toLowerCase()
362
- );
363
- if (matched) return matched;
364
- }
365
-
366
- return 'en-US';
367
- }
368
-
369
- // ========== 3. 中间件 ==========
370
-
371
- function localeMiddleware(req, res, next) {
372
- req.locale = parseAcceptLanguage(req.headers['accept-language']);
373
-
374
- req.validate = function(schema, data) {
375
- const validator = new Validator();
376
- return validator.validate(schema, data, { locale: req.locale });
377
- };
378
-
379
- next();
380
- }
381
-
382
- app.use(localeMiddleware);
383
-
384
- // ========== 4. 定义Schema ==========
385
-
386
- const userSchema = dsl({
387
- username: 'string:3-32!'.label('用户名'),
388
- email: 'email!'.label('邮箱地址'),
389
- password: 'string:8-64!'
390
- .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/)
391
- .label('密码')
392
- .messages({
393
- 'pattern': '密码必须包含大小写字母和数字'
394
- }),
395
- age: 'number:18-120'.label('年龄')
396
- });
397
-
398
- // ========== 5. API 路由 ==========
399
-
400
- app.post('/api/user/register', (req, res) => {
401
- // 验证数据(自动使用请求语言)
402
- const result = req.validate(userSchema, req.body);
403
-
404
- if (!result.valid) {
405
- return res.status(400).json({
406
- success: false,
407
- errors: result.errors,
408
- locale: req.locale // 返回使用的语言
409
- });
410
- }
411
-
412
- // 处理注册逻辑
413
- res.json({
414
- success: true,
415
- message: req.locale === 'zh-CN' ? '注册成功' : 'Registration successful'
416
- });
417
- });
418
-
419
- // ========== 6. 测试 ==========
420
-
421
- app.listen(3000, () => {
422
- console.log('Server running on http://localhost:3000');
423
- console.log('\n测试命令:');
424
- console.log('# 中文错误消息');
425
- console.log('curl -X POST http://localhost:3000/api/user/register \\');
426
- console.log(' -H "Content-Type: application/json" \\');
427
- console.log(' -H "Accept-Language: zh-CN" \\');
428
- console.log(' -d \'{"username":"ab"}\'');
429
- console.log('\n# 英文错误消息');
430
- console.log('curl -X POST http://localhost:3000/api/user/register \\');
431
- console.log(' -H "Content-Type: application/json" \\');
432
- console.log(' -H "Accept-Language: en-US" \\');
433
- console.log(' -d \'{"username":"ab"}\'');
434
- });
435
- ```
436
-
437
- ---
438
-
439
- ## 最佳实践
440
-
441
- ### 1. 语言包集中管理
442
-
443
- ```javascript
444
- // locales/index.js
445
- module.exports = {
446
- 'zh-CN': require('./zh-CN.json'),
447
- 'en-US': require('./en-US.json'),
448
- 'ja-JP': require('./ja-JP.json')
449
- };
450
-
451
- // locales/zh-CN.json
452
- {
453
- "required": "{{#label}}不能为空",
454
- "min": "{{#label}}至少{{#limit}}个字符",
455
- "max": "{{#label}}最多{{#limit}}个字符",
456
- "pattern": "{{#label}}格式不正确",
457
- "format": "请输入有效的{{#label}}"
458
- }
459
-
460
- // 初始化
461
- const locales = require('./locales');
462
- Object.entries(locales).forEach(([locale, messages]) => {
463
- Locale.addLocale(locale, messages);
464
- });
465
- ```
466
-
467
- ### 2. 支持的语言列表
468
-
469
- ```javascript
470
- const SUPPORTED_LOCALES = ['zh-CN', 'en-US', 'ja-JP'];
471
-
472
- function getSupportedLocale(requestLocale) {
473
- return SUPPORTED_LOCALES.includes(requestLocale)
474
- ? requestLocale
475
- : 'en-US';
476
- }
477
- ```
478
-
479
- ### 3. 缓存验证器
480
-
481
- ```javascript
482
- // 为每个语言缓存验证器
483
- const validators = {
484
- 'zh-CN': new Validator(),
485
- 'en-US': new Validator(),
486
- 'ja-JP': new Validator()
487
- };
488
-
489
- function getValidator(locale) {
490
- return validators[locale] || validators['en-US'];
491
- }
492
-
493
- // 使用
494
- const result = getValidator(req.locale).validate(
495
- schema,
496
- data,
497
- { locale: req.locale }
498
- );
499
- ```
500
-
501
- ### 4. 错误响应标准化
502
-
503
- ```javascript
504
- function sendValidationError(res, result, locale) {
505
- res.status(400).json({
506
- success: false,
507
- code: 'VALIDATION_ERROR',
508
- message: locale === 'zh-CN' ? '验证失败' : 'Validation failed',
509
- errors: result.errors,
510
- locale: locale
511
- });
512
- }
513
-
514
- // 使用
515
- if (!result.valid) {
516
- return sendValidationError(res, result, req.locale);
517
- }
518
- ```
519
-
520
- ---
521
-
522
- ## 方案对比
523
-
524
- | 方案 | 优点 | 缺点 | 推荐度 |
525
- |------|------|------|--------|
526
- | **方案1: 验证时指定** | ✅ 无竞态问题<br>✅ 支持并发<br>✅ 代码简洁 | - | ⭐⭐⭐⭐⭐ |
527
- | 方案2: 临时切换 | ✅ 实现简单 | ⚠️ 并发竞态问题 | ⭐⭐⭐ |
528
- | 方案3: 中间件 | ✅ 自动化<br>✅ 统一管理 | - | ⭐⭐⭐⭐⭐ |
529
-
530
- **推荐**: 方案1 + 方案3(中间件封装)
531
-
532
- ---
533
-
534
- ## 常见问题
535
-
536
- ### Q1: 如何处理不支持的语言?
537
-
538
- **A**: 回退到默认语言
539
-
540
- ```javascript
541
- function parseAcceptLanguage(acceptLanguage) {
542
- // ...解析逻辑
543
- return supportedLocale || 'en-US'; // 默认英文
544
- }
545
- ```
546
-
547
- ### Q2: 是否支持动态加载语言包?
548
-
549
- **A**: 支持
550
-
551
- ```javascript
552
- async function loadLocale(locale) {
553
- if (!Locale.getAvailableLocales().includes(locale)) {
554
- const messages = await import(`./locales/${locale}.json`);
555
- Locale.addLocale(locale, messages);
556
- }
557
- }
558
-
559
- // 使用
560
- app.use(async (req, res, next) => {
561
- await loadLocale(req.locale);
562
- next();
563
- });
564
- ```
565
-
566
- ### Q3: 如何自定义某些字段的错误消息?
567
-
568
- **A**: 使用 `.messages()` 方法
569
-
570
- ```javascript
571
- const schema = dsl({
572
- password: 'string:8-64!'
573
- .label('密码')
574
- .messages({
575
- 'required': req.locale === 'zh-CN'
576
- ? '请输入密码'
577
- : 'Please enter password',
578
- 'min': req.locale === 'zh-CN'
579
- ? '密码太短了,至少8个字符'
580
- : 'Password is too short, at least 8 characters'
581
- })
582
- });
583
- ```
584
-
585
- ---
586
-
587
- ## 相关文档
588
-
589
- - [String 扩展](./string-extensions.md#多语言支持)
590
- - [Locale API](./api-reference.md#locale-类)
591
- - [Validator API](./api-reference.md#validator-类)
592
-
593
- ---
594
-
595
- **最后更新**: 2025-12-25
596
- **作者**: SchemaI-DSL Team
597
-
598
-
1
+ # 动态多语言配置指南
2
+
3
+ > **更新时间**: 2025-12-25
4
+ > **场景**: 从请求头动态获取语言配置
5
+
6
+ ---
7
+
8
+ ## 📑 目录
9
+
10
+ - [基本原理](#基本原理)
11
+ - [方案1: 验证时指定语言(推荐)](#方案1-验证时指定语言推荐)
12
+ - [方案2: 临时切换语言](#方案2-临时切换语言)
13
+ - [方案3: Express/Koa 中间件](#方案3-expresskoa-中间件)
14
+ - [完整示例](#完整示例)
15
+ - [最佳实践](#最佳实践)
16
+
17
+ ---
18
+
19
+ ## 基本原理
20
+
21
+ schema-dsl 的 `Validator` 支持在验证时动态指定语言,无需全局切换。
22
+
23
+ ### 核心方法
24
+
25
+ ```javascript
26
+ validator.validate(schema, data, {
27
+ locale: 'zh-CN' // 动态指定语言
28
+ });
29
+ ```
30
+
31
+ ---
32
+
33
+ ## 方案1: 验证时指定语言(推荐)✅
34
+
35
+ 这是**最推荐**的方案,无需修改全局状态,支持并发请求。
36
+
37
+ ### 1.1 应用启动时配置(一次性加载所有语言)
38
+
39
+ 使用 `dsl.config` 在应用启动时一次性加载所有自定义语言包。
40
+
41
+ ```javascript
42
+ const { dsl, validate } = require('schema-dsl');
43
+ const path = require('path');
44
+
45
+ // ========== 应用启动时配置(只执行一次)==========
46
+
47
+ // 方式一:传入目录路径(推荐)⭐
48
+ // Node >=18:自动扫描目录下的 .js(CommonJS)、.cjs、.json、.jsonc、.json5 文件
49
+ dsl.config({
50
+ i18n: path.join(__dirname, 'locales')
51
+ });
52
+
53
+ // 方式二:直接传入对象
54
+ dsl.config({
55
+ i18n: {
56
+ 'fr-FR': {
57
+ 'required': '{{#label}} est requis',
58
+ 'string.minLength': '{{#label}} doit contenir au moins {{#limit}} caractères'
59
+ },
60
+ 'de-DE': {
61
+ 'required': '{{#label}} ist erforderlich',
62
+ 'string.minLength': '{{#label}} muss mindestens {{#limit}} Zeichen lang sein'
63
+ }
64
+ }
65
+ });
66
+
67
+ // 说明:
68
+ // - 只在应用启动时执行一次
69
+ // - 自动与系统内置语言包合并(用户自定义的优先)
70
+ // - 运行时无需重新加载,直接切换
71
+ ```
72
+
73
+ ### 1.2 运行时直接切换语言(无需重新加载)
74
+
75
+ ```javascript
76
+ const { dsl, validate } = require('schema-dsl');
77
+
78
+ // 定义 Schema
79
+ const schema = dsl({
80
+ username: 'string:3-32!',
81
+ email: 'email!'
82
+ });
83
+
84
+ // 测试数据
85
+ const data = { username: 'ab', email: 'invalid' };
86
+
87
+ // ========== 运行时直接切换语言 ==========
88
+
89
+ // 使用中文
90
+ const result1 = validate(schema, data, { locale: 'zh-CN' });
91
+ // 错误: "username长度不能少于3个字符"
92
+
93
+ // 使用法语
94
+ const result2 = validate(schema, data, { locale: 'fr-FR' });
95
+ // 错误: "username doit contenir au moins 3 caractères"
96
+
97
+ // 使用德语
98
+ const result3 = validate(schema, data, { locale: 'de-DE' });
99
+ // 错误: "username muss mindestens 3 Zeichen lang sein"
100
+
101
+ // 说明:
102
+ // - 无需重新加载语言包
103
+ // - 每次验证可以使用不同语言
104
+ // - 支持高并发(无全局状态修改)
105
+ ```
106
+
107
+ ### 1.3 从请求头获取语言(实际应用场景)
108
+
109
+ ```javascript
110
+ const express = require('express');
111
+ const { dsl, validate } = require('schema-dsl');
112
+ const path = require('path');
113
+
114
+ const app = express();
115
+
116
+ // ========== 应用启动时配置(只执行一次)==========
117
+ dsl.config({
118
+ i18n: path.join(__dirname, 'locales')
119
+ });
120
+
121
+ // 定义 Schema
122
+ const userSchema = dsl({
123
+ username: 'string:3-32!',
124
+ email: 'email!',
125
+ password: 'string:8-32!'
126
+ });
127
+
128
+ // ========== Express 路由 ==========
129
+ app.post('/api/user/register', (req, res) => {
130
+ // 从请求头获取语言偏好
131
+ const locale = parseAcceptLanguage(req.headers['accept-language']);
132
+
133
+ // 验证数据(直接切换语言,无需重新加载)
134
+ const result = validate(userSchema, req.body, { locale });
135
+
136
+ if (!result.valid) {
137
+ return res.status(400).json({
138
+ errors: result.errors // 自动使用用户偏好的语言
139
+ });
140
+ }
141
+
142
+ // 处理成功...
143
+ res.json({ message: 'User registered successfully' });
144
+ });
145
+ ```
146
+
147
+ ### 1.3 解析 Accept-Language 头
148
+
149
+ ```javascript
150
+ /**
151
+ * 解析 Accept-Language 头
152
+ * @param {string} acceptLanguage - Accept-Language 头的值
153
+ * @returns {string} 语言代码
154
+ */
155
+ function parseAcceptLanguage(acceptLanguage) {
156
+ if (!acceptLanguage) return 'en-US';
157
+
158
+ // Accept-Language 格式: zh-CN,zh;q=0.9,en;q=0.8
159
+ const languages = acceptLanguage.split(',').map(lang => {
160
+ const [code, qValue] = lang.trim().split(';');
161
+ const q = qValue ? parseFloat(qValue.split('=')[1]) : 1.0;
162
+ return { code: code.trim(), q };
163
+ });
164
+
165
+ // 按权重排序
166
+ languages.sort((a, b) => b.q - a.q);
167
+
168
+ // 映射到支持的语言
169
+ const supportedLocales = ['zh-CN', 'en-US', 'ja-JP'];
170
+ for (const lang of languages) {
171
+ const matched = supportedLocales.find(locale =>
172
+ locale.toLowerCase() === lang.code.toLowerCase() ||
173
+ locale.split('-')[0] === lang.code.split('-')[0]
174
+ );
175
+ if (matched) return matched;
176
+ }
177
+
178
+ return 'en-US'; // 默认语言
179
+ }
180
+
181
+ // 使用
182
+ app.post('/api/user/register', (req, res) => {
183
+ const locale = parseAcceptLanguage(req.headers['accept-language']);
184
+
185
+ const result = validator.validate(schema, req.body, { locale });
186
+
187
+ // ...
188
+ });
189
+ ```
190
+
191
+ ---
192
+
193
+ ## 方案2: 临时切换语言
194
+
195
+ 适用于少数场景。
196
+
197
+ ### 2.1 使用闭包保存原语言
198
+
199
+ ```javascript
200
+ function validateWithLocale(validator, schema, data, locale) {
201
+ const originalLocale = Locale.getLocale();
202
+
203
+ try {
204
+ Locale.setLocale(locale);
205
+ return validator.validate(schema, data);
206
+ } finally {
207
+ Locale.setLocale(originalLocale); // 恢复原语言
208
+ }
209
+ }
210
+
211
+ // 使用
212
+ app.post('/api/user/register', (req, res) => {
213
+ const locale = parseAcceptLanguage(req.headers['accept-language']);
214
+
215
+ const result = validateWithLocale(validator, schema, req.body, locale);
216
+
217
+ // ...
218
+ });
219
+ ```
220
+
221
+ ---
222
+
223
+ ## 方案3: Express/Koa 中间件
224
+
225
+ 封装为中间件,自动处理语言切换。
226
+
227
+ ### 3.1 Express 中间件 (推荐)
228
+
229
+ 通过中间件一次性配置,后续业务代码无需关心语言参数。
230
+
231
+ ```javascript
232
+ const { Validator } = require('schema-dsl');
233
+ const validator = new Validator();
234
+
235
+ const schemaIoMiddleware = (req, res, next) => {
236
+ // 1. 自动获取语言
237
+ const lang = req.headers['accept-language']?.split(',')[0]?.trim() || 'en-US';
238
+ // 简单匹配逻辑 (实际可使用 accept-language-parser)
239
+ const locale = lang.includes('zh') ? 'zh-CN' :
240
+ lang.includes('ja') ? 'ja-JP' :
241
+ lang.includes('es') ? 'es-ES' :
242
+ lang.includes('fr') ? 'fr-FR' : 'en-US';
243
+
244
+ // 2. 挂载绑定了语言的验证方法
245
+ req.validate = (schema, data) => {
246
+ return validator.validate(schema, data, { locale });
247
+ };
248
+
249
+ next();
250
+ };
251
+
252
+ app.use(schemaIoMiddleware);
253
+
254
+ // 业务中使用
255
+ app.post('/users', (req, res) => {
256
+ // 直接调用,自动使用中间件解析的语言
257
+ const result = req.validate(userSchema, req.body);
258
+
259
+ if (!result.valid) {
260
+ return res.status(400).json({ errors: result.errors });
261
+ }
262
+
263
+ // ...
264
+ });
265
+ ```
266
+
267
+ 完整示例请参考 [dynamic-locale.ts](https://github.com/vextjs/schema-dsl/blob/main/examples/docs/dynamic-locale.ts)。
268
+
269
+ ### 3.2 Koa 中间件
270
+
271
+ ```javascript
272
+ const { Locale, Validator } = require('schema-dsl');
273
+
274
+ const validator = new Validator();
275
+
276
+ /**
277
+ * Koa 语言中间件
278
+ */
279
+ function localeMiddleware() {
280
+ return async (ctx, next) => {
281
+ // 解析语言
282
+ const locale = parseAcceptLanguage(ctx.headers['accept-language']);
283
+
284
+ // 保存到上下文
285
+ ctx.locale = locale;
286
+
287
+ // 复用共享 Validator,避免每个请求都重新建立实例和缓存
288
+ ctx.validate = function(schema, data) {
289
+ return validator.validate(schema, data, { locale: ctx.locale });
290
+ };
291
+
292
+ await next();
293
+ };
294
+ }
295
+
296
+ // 应用中间件
297
+ app.use(localeMiddleware());
298
+
299
+ // 使用
300
+ router.post('/api/user/register', async (ctx) => {
301
+ // 自动使用请求的语言
302
+ const result = ctx.validate(userSchema, ctx.request.body);
303
+
304
+ if (!result.valid) {
305
+ ctx.status = 400;
306
+ ctx.body = { errors: result.errors };
307
+ return;
308
+ }
309
+
310
+ // ...
311
+ });
312
+ ```
313
+
314
+ ---
315
+
316
+ ## 完整示例
317
+
318
+ ### Express 完整示例
319
+
320
+ ```javascript
321
+ const express = require('express');
322
+ const { dsl, Validator, Locale } = require('schema-dsl');
323
+
324
+ const app = express();
325
+ app.use(express.json());
326
+
327
+ // ========== 1. 初始化语言包 ==========
328
+
329
+ Locale.addLocale('zh-CN', {
330
+ 'required': '{{#label}}不能为空',
331
+ 'min': '{{#label}}至少{{#limit}}个字符',
332
+ 'max': '{{#label}}最多{{#limit}}个字符',
333
+ 'pattern': '{{#label}}格式不正确',
334
+ 'format': '请输入有效的{{#label}}'
335
+ });
336
+
337
+ Locale.addLocale('en-US', {
338
+ 'required': '{{#label}} is required',
339
+ 'min': '{{#label}} must be at least {{#limit}} characters',
340
+ 'max': '{{#label}} must be at most {{#limit}} characters',
341
+ 'pattern': '{{#label}} format is invalid',
342
+ 'format': 'Please enter a valid {{#label}}'
343
+ });
344
+
345
+ // ========== 2. 工具函数 ==========
346
+
347
+ function parseAcceptLanguage(acceptLanguage) {
348
+ if (!acceptLanguage) return 'en-US';
349
+
350
+ const languages = acceptLanguage.split(',').map(lang => {
351
+ const [code, qValue] = lang.trim().split(';');
352
+ const q = qValue ? parseFloat(qValue.split('=')[1]) : 1.0;
353
+ return { code: code.trim(), q };
354
+ });
355
+
356
+ languages.sort((a, b) => b.q - a.q);
357
+
358
+ const supportedLocales = ['zh-CN', 'en-US'];
359
+ for (const lang of languages) {
360
+ const matched = supportedLocales.find(locale =>
361
+ locale.toLowerCase() === lang.code.toLowerCase()
362
+ );
363
+ if (matched) return matched;
364
+ }
365
+
366
+ return 'en-US';
367
+ }
368
+
369
+ // ========== 3. 中间件 ==========
370
+
371
+ const validator = new Validator();
372
+
373
+ function localeMiddleware(req, res, next) {
374
+ req.locale = parseAcceptLanguage(req.headers['accept-language']);
375
+
376
+ req.validate = function(schema, data) {
377
+ return validator.validate(schema, data, { locale: req.locale });
378
+ };
379
+
380
+ next();
381
+ }
382
+
383
+ app.use(localeMiddleware);
384
+
385
+ // ========== 4. 定义Schema ==========
386
+
387
+ const userSchema = dsl({
388
+ username: 'string:3-32!'.label('用户名'),
389
+ email: 'email!'.label('邮箱地址'),
390
+ password: 'string:8-64!'
391
+ .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/)
392
+ .label('密码')
393
+ .messages({
394
+ 'pattern': '密码必须包含大小写字母和数字'
395
+ }),
396
+ age: 'number:18-120'.label('年龄')
397
+ });
398
+
399
+ // ========== 5. API 路由 ==========
400
+
401
+ app.post('/api/user/register', (req, res) => {
402
+ // 验证数据(自动使用请求语言)
403
+ const result = req.validate(userSchema, req.body);
404
+
405
+ if (!result.valid) {
406
+ return res.status(400).json({
407
+ success: false,
408
+ errors: result.errors,
409
+ locale: req.locale // 返回使用的语言
410
+ });
411
+ }
412
+
413
+ // 处理注册逻辑
414
+ res.json({
415
+ success: true,
416
+ message: req.locale === 'zh-CN' ? '注册成功' : 'Registration successful'
417
+ });
418
+ });
419
+
420
+ // ========== 6. 测试 ==========
421
+
422
+ app.listen(3000, () => {
423
+ console.log('Server running on http://localhost:3000');
424
+ console.log('\n测试命令:');
425
+ console.log('# 中文错误消息');
426
+ console.log('curl -X POST http://localhost:3000/api/user/register \\');
427
+ console.log(' -H "Content-Type: application/json" \\');
428
+ console.log(' -H "Accept-Language: zh-CN" \\');
429
+ console.log(' -d \'{"username":"ab"}\'');
430
+ console.log('\n# 英文错误消息');
431
+ console.log('curl -X POST http://localhost:3000/api/user/register \\');
432
+ console.log(' -H "Content-Type: application/json" \\');
433
+ console.log(' -H "Accept-Language: en-US" \\');
434
+ console.log(' -d \'{"username":"ab"}\'');
435
+ });
436
+ ```
437
+
438
+ ---
439
+
440
+ ## 最佳实践
441
+
442
+ ### 1. 语言包集中管理
443
+
444
+ ```javascript
445
+ // locales/index.js
446
+ module.exports = {
447
+ 'zh-CN': require('./zh-CN.json'),
448
+ 'en-US': require('./en-US.json'),
449
+ 'ja-JP': require('./ja-JP.json')
450
+ };
451
+
452
+ // locales/zh-CN.json
453
+ {
454
+ "required": "{{#label}}不能为空",
455
+ "min": "{{#label}}至少{{#limit}}个字符",
456
+ "max": "{{#label}}最多{{#limit}}个字符",
457
+ "pattern": "{{#label}}格式不正确",
458
+ "format": "请输入有效的{{#label}}"
459
+ }
460
+
461
+ // 初始化
462
+ const locales = require('./locales');
463
+ Object.entries(locales).forEach(([locale, messages]) => {
464
+ Locale.addLocale(locale, messages);
465
+ });
466
+ ```
467
+
468
+ ### 2. 支持的语言列表
469
+
470
+ ```javascript
471
+ const SUPPORTED_LOCALES = ['zh-CN', 'en-US', 'ja-JP'];
472
+
473
+ function getSupportedLocale(requestLocale) {
474
+ return SUPPORTED_LOCALES.includes(requestLocale)
475
+ ? requestLocale
476
+ : 'en-US';
477
+ }
478
+ ```
479
+
480
+ ### 3. 缓存验证器
481
+
482
+ ```javascript
483
+ // 为每个语言缓存验证器
484
+ const validators = {
485
+ 'zh-CN': new Validator(),
486
+ 'en-US': new Validator(),
487
+ 'ja-JP': new Validator()
488
+ };
489
+
490
+ function getValidator(locale) {
491
+ return validators[locale] || validators['en-US'];
492
+ }
493
+
494
+ // 使用
495
+ const result = getValidator(req.locale).validate(
496
+ schema,
497
+ data,
498
+ { locale: req.locale }
499
+ );
500
+ ```
501
+
502
+ ### 4. 错误响应标准化
503
+
504
+ ```javascript
505
+ function sendValidationError(res, result, locale) {
506
+ res.status(400).json({
507
+ success: false,
508
+ code: 'VALIDATION_ERROR',
509
+ message: locale === 'zh-CN' ? '验证失败' : 'Validation failed',
510
+ errors: result.errors,
511
+ locale: locale
512
+ });
513
+ }
514
+
515
+ // 使用
516
+ if (!result.valid) {
517
+ return sendValidationError(res, result, req.locale);
518
+ }
519
+ ```
520
+
521
+ ---
522
+
523
+ ## 方案对比
524
+
525
+ | 方案 | 优点 | 缺点 | 推荐度 |
526
+ |------|------|------|--------|
527
+ | **方案1: 验证时指定** | ✅ 无竞态问题<br>✅ 支持并发<br>✅ 代码简洁 | - | ⭐⭐⭐⭐⭐ |
528
+ | 方案2: 临时切换 | ✅ 实现简单 | ⚠️ 并发竞态问题 | ⭐⭐⭐ |
529
+ | 方案3: 中间件 | ✅ 自动化<br>✅ 统一管理<br>✅ 可复用共享 Validator 缓存 | - | ⭐⭐⭐⭐⭐ |
530
+
531
+ **推荐**: 方案1 + 方案3(中间件封装)
532
+
533
+ ---
534
+
535
+ ## 常见问题
536
+
537
+ ### Q1: 如何处理不支持的语言?
538
+
539
+ **A**: 回退到默认语言
540
+
541
+ 不要直接把原始 `Accept-Language` 头透传给 `locale`;浏览器常见值会带 `q=` 权重,应该先解析再回退。
542
+
543
+ ```javascript
544
+ function parseAcceptLanguage(acceptLanguage) {
545
+ // ...解析逻辑
546
+ return supportedLocale || 'en-US'; // 默认英文
547
+ }
548
+ ```
549
+
550
+ ### Q2: 是否支持动态加载语言包?
551
+
552
+ **A**: 支持
553
+
554
+ ```javascript
555
+ async function loadLocale(locale) {
556
+ if (!Locale.getAvailableLocales().includes(locale)) {
557
+ const messages = await import(`./locales/${locale}.json`);
558
+ Locale.addLocale(locale, messages);
559
+ }
560
+ }
561
+
562
+ // 使用
563
+ app.use(async (req, res, next) => {
564
+ await loadLocale(req.locale);
565
+ next();
566
+ });
567
+ ```
568
+
569
+ ### Q3: 如何自定义某些字段的错误消息?
570
+
571
+ **A**: 使用 `.messages()` 方法
572
+
573
+ ```javascript
574
+ const schema = dsl({
575
+ password: 'string:8-64!'
576
+ .label('密码')
577
+ .messages({
578
+ 'required': req.locale === 'zh-CN'
579
+ ? '请输入密码'
580
+ : 'Please enter password',
581
+ 'min': req.locale === 'zh-CN'
582
+ ? '密码太短了,至少8个字符'
583
+ : 'Password is too short, at least 8 characters'
584
+ })
585
+ });
586
+ ```
587
+
588
+ ---
589
+
590
+ ## 相关文档
591
+
592
+ - [String 扩展](./string-extensions.md#多语言支持)
593
+ - [Locale API](./api-reference.md#locale-类)
594
+ - [Validator API](./api-reference.md#validator-类)
595
+
596
+ ---
597
+
598
+ ## 对应示例文件
599
+
600
+ **示例入口**: [dynamic-locale.ts](https://github.com/vextjs/schema-dsl/blob/main/examples/docs/dynamic-locale.ts)
601
+ **说明**: 覆盖 `Accept-Language` 解析、运行时 locale 选择,以及同一 schema 在不同请求语言下的验证入口。
602
+
603
+ ---
604
+
605
+ **最后更新**: 2026-05-08
606
+ **作者**: schema-dsl Team
607
+
608
+