schema-dsl 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/.eslintignore +10 -0
  2. package/.eslintrc.json +27 -0
  3. package/.github/CODE_OF_CONDUCT.md +45 -0
  4. package/.github/ISSUE_TEMPLATE/bug_report.md +57 -0
  5. package/.github/ISSUE_TEMPLATE/config.yml +11 -0
  6. package/.github/ISSUE_TEMPLATE/feature_request.md +45 -0
  7. package/.github/ISSUE_TEMPLATE/question.md +31 -0
  8. package/.github/PULL_REQUEST_TEMPLATE.md +70 -0
  9. package/.github/SECURITY.md +184 -0
  10. package/.github/workflows/ci.yml +35 -0
  11. package/CHANGELOG.md +633 -0
  12. package/CONTRIBUTING.md +368 -0
  13. package/LICENSE +21 -0
  14. package/README.md +1122 -0
  15. package/STATUS.md +273 -0
  16. package/docs/FEATURE-INDEX.md +521 -0
  17. package/docs/INDEX.md +224 -0
  18. package/docs/api-reference.md +1098 -0
  19. package/docs/best-practices.md +672 -0
  20. package/docs/cache-manager.md +336 -0
  21. package/docs/design-philosophy.md +602 -0
  22. package/docs/dsl-syntax.md +654 -0
  23. package/docs/dynamic-locale.md +552 -0
  24. package/docs/error-handling.md +703 -0
  25. package/docs/export-guide.md +459 -0
  26. package/docs/faq.md +576 -0
  27. package/docs/frontend-i18n-guide.md +290 -0
  28. package/docs/i18n-user-guide.md +488 -0
  29. package/docs/label-vs-description.md +262 -0
  30. package/docs/markdown-exporter.md +398 -0
  31. package/docs/mongodb-exporter.md +279 -0
  32. package/docs/multi-type-support.md +319 -0
  33. package/docs/mysql-exporter.md +257 -0
  34. package/docs/plugin-system.md +542 -0
  35. package/docs/postgresql-exporter.md +290 -0
  36. package/docs/quick-start.md +761 -0
  37. package/docs/schema-helper.md +340 -0
  38. package/docs/schema-utils.md +492 -0
  39. package/docs/string-extensions.md +480 -0
  40. package/docs/troubleshooting.md +471 -0
  41. package/docs/type-converter.md +319 -0
  42. package/docs/type-reference.md +219 -0
  43. package/docs/validate.md +486 -0
  44. package/docs/validation-guide.md +484 -0
  45. package/examples/array-dsl-example.js +227 -0
  46. package/examples/custom-extension.js +85 -0
  47. package/examples/dsl-match-example.js +74 -0
  48. package/examples/dsl-style.js +118 -0
  49. package/examples/dynamic-locale-configuration.js +348 -0
  50. package/examples/dynamic-locale-example.js +287 -0
  51. package/examples/export-demo.js +130 -0
  52. package/examples/i18n-full-demo.js +310 -0
  53. package/examples/i18n-memory-safety.examples.js +268 -0
  54. package/examples/markdown-export.js +71 -0
  55. package/examples/middleware-usage.js +93 -0
  56. package/examples/password-reset/README.md +153 -0
  57. package/examples/password-reset/schema.js +26 -0
  58. package/examples/password-reset/test.js +101 -0
  59. package/examples/plugin-system.examples.js +205 -0
  60. package/examples/simple-example.js +122 -0
  61. package/examples/string-extensions.js +297 -0
  62. package/examples/user-registration/README.md +156 -0
  63. package/examples/user-registration/routes.js +92 -0
  64. package/examples/user-registration/schema.js +150 -0
  65. package/examples/user-registration/server.js +74 -0
  66. package/index.d.ts +1999 -0
  67. package/index.js +270 -0
  68. package/index.mjs +30 -0
  69. package/lib/adapters/DslAdapter.js +653 -0
  70. package/lib/adapters/index.js +20 -0
  71. package/lib/config/constants.js +286 -0
  72. package/lib/config/patterns/creditCard.js +9 -0
  73. package/lib/config/patterns/idCard.js +9 -0
  74. package/lib/config/patterns/index.js +8 -0
  75. package/lib/config/patterns/licensePlate.js +4 -0
  76. package/lib/config/patterns/passport.js +4 -0
  77. package/lib/config/patterns/phone.js +9 -0
  78. package/lib/config/patterns/postalCode.js +5 -0
  79. package/lib/core/CacheManager.js +376 -0
  80. package/lib/core/DslBuilder.js +740 -0
  81. package/lib/core/ErrorCodes.js +233 -0
  82. package/lib/core/ErrorFormatter.js +342 -0
  83. package/lib/core/JSONSchemaCore.js +347 -0
  84. package/lib/core/Locale.js +119 -0
  85. package/lib/core/MessageTemplate.js +89 -0
  86. package/lib/core/PluginManager.js +448 -0
  87. package/lib/core/StringExtensions.js +209 -0
  88. package/lib/core/Validator.js +316 -0
  89. package/lib/exporters/MarkdownExporter.js +420 -0
  90. package/lib/exporters/MongoDBExporter.js +162 -0
  91. package/lib/exporters/MySQLExporter.js +212 -0
  92. package/lib/exporters/PostgreSQLExporter.js +289 -0
  93. package/lib/exporters/index.js +24 -0
  94. package/lib/locales/en-US.js +65 -0
  95. package/lib/locales/es-ES.js +66 -0
  96. package/lib/locales/fr-FR.js +66 -0
  97. package/lib/locales/index.js +8 -0
  98. package/lib/locales/ja-JP.js +66 -0
  99. package/lib/locales/zh-CN.js +93 -0
  100. package/lib/utils/LRUCache.js +174 -0
  101. package/lib/utils/SchemaHelper.js +240 -0
  102. package/lib/utils/SchemaUtils.js +313 -0
  103. package/lib/utils/TypeConverter.js +245 -0
  104. package/lib/utils/index.js +13 -0
  105. package/lib/validators/CustomKeywords.js +203 -0
  106. package/lib/validators/index.js +11 -0
  107. package/package.json +70 -0
  108. package/plugins/custom-format.js +101 -0
  109. package/plugins/custom-validator.js +200 -0
@@ -0,0 +1,552 @@
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
+ SchemaIO 的 `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, Validator } = require('schema-dsl');
43
+
44
+ // 方式一:直接传入对象
45
+ dsl.config({
46
+ locales: {
47
+ 'fr-FR': {
48
+ 'required': '{{#label}} est requis',
49
+ 'pattern.phone.cn': 'Numéro de téléphone invalide'
50
+ }
51
+ }
52
+ });
53
+
54
+ // 方式二:传入目录路径
55
+ // 目录下应包含 zh-CN.js, en-US.js 等文件
56
+ dsl.config({
57
+ locales: './locales'
58
+ });
59
+ ```
60
+
61
+ ### 1.2 基础用法
62
+
63
+ ```javascript
64
+ // 定义Schema
65
+ const schema = dsl({
66
+ username: 'string:3-32!'.label('用户名'),
67
+ email: 'email!'.label('邮箱地址')
68
+ });
69
+
70
+ // 创建验证器
71
+ const validator = new Validator();
72
+
73
+ // 验证时动态指定语言
74
+ const result1 = validator.validate(schema, data, { locale: 'zh-CN' });
75
+ const result2 = validator.validate(schema, data, { locale: 'fr-FR' });
76
+ ```
77
+
78
+ ### 1.3 从请求头获取语言
79
+
80
+ ```javascript
81
+ // Express 示例
82
+ app.post('/api/user/register', (req, res) => {
83
+ // 从请求头获取语言
84
+ const locale = req.headers['accept-language'] || 'en-US';
85
+
86
+ // 验证数据
87
+ const result = validator.validate(schema, req.body, {
88
+ locale: locale
89
+ });
90
+
91
+ if (!result.valid) {
92
+ return res.status(400).json({
93
+ errors: result.errors // 自动使用对应语言的错误消息
94
+ });
95
+ }
96
+
97
+ // 处理成功...
98
+ });
99
+ ```
100
+
101
+ ### 1.3 解析 Accept-Language 头
102
+
103
+ ```javascript
104
+ /**
105
+ * 解析 Accept-Language 头
106
+ * @param {string} acceptLanguage - Accept-Language 头的值
107
+ * @returns {string} 语言代码
108
+ */
109
+ function parseAcceptLanguage(acceptLanguage) {
110
+ if (!acceptLanguage) return 'en-US';
111
+
112
+ // Accept-Language 格式: zh-CN,zh;q=0.9,en;q=0.8
113
+ const languages = acceptLanguage.split(',').map(lang => {
114
+ const [code, qValue] = lang.trim().split(';');
115
+ const q = qValue ? parseFloat(qValue.split('=')[1]) : 1.0;
116
+ return { code: code.trim(), q };
117
+ });
118
+
119
+ // 按权重排序
120
+ languages.sort((a, b) => b.q - a.q);
121
+
122
+ // 映射到支持的语言
123
+ const supportedLocales = ['zh-CN', 'en-US', 'ja-JP'];
124
+ for (const lang of languages) {
125
+ const matched = supportedLocales.find(locale =>
126
+ locale.toLowerCase() === lang.code.toLowerCase() ||
127
+ locale.split('-')[0] === lang.code.split('-')[0]
128
+ );
129
+ if (matched) return matched;
130
+ }
131
+
132
+ return 'en-US'; // 默认语言
133
+ }
134
+
135
+ // 使用
136
+ app.post('/api/user/register', (req, res) => {
137
+ const locale = parseAcceptLanguage(req.headers['accept-language']);
138
+
139
+ const result = validator.validate(schema, req.body, { locale });
140
+
141
+ // ...
142
+ });
143
+ ```
144
+
145
+ ---
146
+
147
+ ## 方案2: 临时切换语言
148
+
149
+ 适用于少数场景。
150
+
151
+ ### 2.1 使用闭包保存原语言
152
+
153
+ ```javascript
154
+ function validateWithLocale(validator, schema, data, locale) {
155
+ const originalLocale = Locale.getLocale();
156
+
157
+ try {
158
+ Locale.setLocale(locale);
159
+ return validator.validate(schema, data);
160
+ } finally {
161
+ Locale.setLocale(originalLocale); // 恢复原语言
162
+ }
163
+ }
164
+
165
+ // 使用
166
+ app.post('/api/user/register', (req, res) => {
167
+ const locale = req.headers['accept-language'] || 'en-US';
168
+
169
+ const result = validateWithLocale(validator, schema, req.body, locale);
170
+
171
+ // ...
172
+ });
173
+ ```
174
+
175
+ ---
176
+
177
+ ## 方案3: Express/Koa 中间件
178
+
179
+ 封装为中间件,自动处理语言切换。
180
+
181
+ ### 3.1 Express 中间件 (推荐)
182
+
183
+ 通过中间件一次性配置,后续业务代码无需关心语言参数。
184
+
185
+ ```javascript
186
+ const { Validator } = require('schema-dsl');
187
+ const validator = new Validator();
188
+
189
+ const schemaIoMiddleware = (req, res, next) => {
190
+ // 1. 自动获取语言
191
+ const lang = req.headers['accept-language'] || 'en-US';
192
+ // 简单匹配逻辑 (实际可使用 accept-language-parser)
193
+ const locale = lang.includes('zh') ? 'zh-CN' :
194
+ lang.includes('ja') ? 'ja-JP' :
195
+ lang.includes('es') ? 'es-ES' :
196
+ lang.includes('fr') ? 'fr-FR' : 'en-US';
197
+
198
+ // 2. 挂载绑定了语言的验证方法
199
+ req.validate = (schema, data) => {
200
+ return validator.validate(schema, data, { locale });
201
+ };
202
+
203
+ next();
204
+ };
205
+
206
+ app.use(schemaIoMiddleware);
207
+
208
+ // 业务中使用
209
+ app.post('/users', (req, res) => {
210
+ // 直接调用,自动使用中间件解析的语言
211
+ const result = req.validate(userSchema, req.body);
212
+
213
+ if (!result.valid) {
214
+ return res.status(400).json({ errors: result.errors });
215
+ }
216
+
217
+ // ...
218
+ });
219
+ ```
220
+
221
+ 完整示例请参考 `examples/middleware-usage.js`。
222
+
223
+ ### 3.2 Koa 中间件
224
+
225
+ ```javascript
226
+ const { Locale } = require('schema-dsl');
227
+
228
+ /**
229
+ * Koa 语言中间件
230
+ */
231
+ function localeMiddleware() {
232
+ return async (ctx, next) => {
233
+ // 解析语言
234
+ const locale = parseAcceptLanguage(ctx.headers['accept-language']);
235
+
236
+ // 保存到上下文
237
+ ctx.locale = locale;
238
+
239
+ // 创建验证辅助函数
240
+ ctx.validate = function(schema, data) {
241
+ const { Validator } = require('schema-dsl');
242
+ const validator = new Validator();
243
+ return validator.validate(schema, data, { locale: ctx.locale });
244
+ };
245
+
246
+ await next();
247
+ };
248
+ }
249
+
250
+ // 应用中间件
251
+ app.use(localeMiddleware());
252
+
253
+ // 使用
254
+ router.post('/api/user/register', async (ctx) => {
255
+ // 自动使用请求的语言
256
+ const result = ctx.validate(userSchema, ctx.request.body);
257
+
258
+ if (!result.valid) {
259
+ ctx.status = 400;
260
+ ctx.body = { errors: result.errors };
261
+ return;
262
+ }
263
+
264
+ // ...
265
+ });
266
+ ```
267
+
268
+ ---
269
+
270
+ ## 完整示例
271
+
272
+ ### Express 完整示例
273
+
274
+ ```javascript
275
+ const express = require('express');
276
+ const { dsl, Validator, Locale } = require('schema-dsl');
277
+
278
+ const app = express();
279
+ app.use(express.json());
280
+
281
+ // ========== 1. 初始化语言包 ==========
282
+
283
+ Locale.addLocale('zh-CN', {
284
+ 'required': '{{#label}}不能为空',
285
+ 'min': '{{#label}}至少{{#limit}}个字符',
286
+ 'max': '{{#label}}最多{{#limit}}个字符',
287
+ 'pattern': '{{#label}}格式不正确',
288
+ 'format': '请输入有效的{{#label}}'
289
+ });
290
+
291
+ Locale.addLocale('en-US', {
292
+ 'required': '{{#label}} is required',
293
+ 'min': '{{#label}} must be at least {{#limit}} characters',
294
+ 'max': '{{#label}} must be at most {{#limit}} characters',
295
+ 'pattern': '{{#label}} format is invalid',
296
+ 'format': 'Please enter a valid {{#label}}'
297
+ });
298
+
299
+ // ========== 2. 工具函数 ==========
300
+
301
+ function parseAcceptLanguage(acceptLanguage) {
302
+ if (!acceptLanguage) return 'en-US';
303
+
304
+ const languages = acceptLanguage.split(',').map(lang => {
305
+ const [code, qValue] = lang.trim().split(';');
306
+ const q = qValue ? parseFloat(qValue.split('=')[1]) : 1.0;
307
+ return { code: code.trim(), q };
308
+ });
309
+
310
+ languages.sort((a, b) => b.q - a.q);
311
+
312
+ const supportedLocales = ['zh-CN', 'en-US'];
313
+ for (const lang of languages) {
314
+ const matched = supportedLocales.find(locale =>
315
+ locale.toLowerCase() === lang.code.toLowerCase()
316
+ );
317
+ if (matched) return matched;
318
+ }
319
+
320
+ return 'en-US';
321
+ }
322
+
323
+ // ========== 3. 中间件 ==========
324
+
325
+ function localeMiddleware(req, res, next) {
326
+ req.locale = parseAcceptLanguage(req.headers['accept-language']);
327
+
328
+ req.validate = function(schema, data) {
329
+ const validator = new Validator();
330
+ return validator.validate(schema, data, { locale: req.locale });
331
+ };
332
+
333
+ next();
334
+ }
335
+
336
+ app.use(localeMiddleware);
337
+
338
+ // ========== 4. 定义Schema ==========
339
+
340
+ const userSchema = dsl({
341
+ username: 'string:3-32!'.label('用户名'),
342
+ email: 'email!'.label('邮箱地址'),
343
+ password: 'string:8-64!'
344
+ .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/)
345
+ .label('密码')
346
+ .messages({
347
+ 'pattern': '密码必须包含大小写字母和数字'
348
+ }),
349
+ age: 'number:18-120'.label('年龄')
350
+ });
351
+
352
+ // ========== 5. API 路由 ==========
353
+
354
+ app.post('/api/user/register', (req, res) => {
355
+ // 验证数据(自动使用请求语言)
356
+ const result = req.validate(userSchema, req.body);
357
+
358
+ if (!result.valid) {
359
+ return res.status(400).json({
360
+ success: false,
361
+ errors: result.errors,
362
+ locale: req.locale // 返回使用的语言
363
+ });
364
+ }
365
+
366
+ // 处理注册逻辑
367
+ res.json({
368
+ success: true,
369
+ message: req.locale === 'zh-CN' ? '注册成功' : 'Registration successful'
370
+ });
371
+ });
372
+
373
+ // ========== 6. 测试 ==========
374
+
375
+ app.listen(3000, () => {
376
+ console.log('Server running on http://localhost:3000');
377
+ console.log('\n测试命令:');
378
+ console.log('# 中文错误消息');
379
+ console.log('curl -X POST http://localhost:3000/api/user/register \\');
380
+ console.log(' -H "Content-Type: application/json" \\');
381
+ console.log(' -H "Accept-Language: zh-CN" \\');
382
+ console.log(' -d \'{"username":"ab"}\'');
383
+ console.log('\n# 英文错误消息');
384
+ console.log('curl -X POST http://localhost:3000/api/user/register \\');
385
+ console.log(' -H "Content-Type: application/json" \\');
386
+ console.log(' -H "Accept-Language: en-US" \\');
387
+ console.log(' -d \'{"username":"ab"}\'');
388
+ });
389
+ ```
390
+
391
+ ---
392
+
393
+ ## 最佳实践
394
+
395
+ ### 1. 语言包集中管理
396
+
397
+ ```javascript
398
+ // locales/index.js
399
+ module.exports = {
400
+ 'zh-CN': require('./zh-CN.json'),
401
+ 'en-US': require('./en-US.json'),
402
+ 'ja-JP': require('./ja-JP.json')
403
+ };
404
+
405
+ // locales/zh-CN.json
406
+ {
407
+ "required": "{{#label}}不能为空",
408
+ "min": "{{#label}}至少{{#limit}}个字符",
409
+ "max": "{{#label}}最多{{#limit}}个字符",
410
+ "pattern": "{{#label}}格式不正确",
411
+ "format": "请输入有效的{{#label}}"
412
+ }
413
+
414
+ // 初始化
415
+ const locales = require('./locales');
416
+ Object.entries(locales).forEach(([locale, messages]) => {
417
+ Locale.addLocale(locale, messages);
418
+ });
419
+ ```
420
+
421
+ ### 2. 支持的语言列表
422
+
423
+ ```javascript
424
+ const SUPPORTED_LOCALES = ['zh-CN', 'en-US', 'ja-JP'];
425
+
426
+ function getSupportedLocale(requestLocale) {
427
+ return SUPPORTED_LOCALES.includes(requestLocale)
428
+ ? requestLocale
429
+ : 'en-US';
430
+ }
431
+ ```
432
+
433
+ ### 3. 缓存验证器
434
+
435
+ ```javascript
436
+ // 为每个语言缓存验证器
437
+ const validators = {
438
+ 'zh-CN': new Validator(),
439
+ 'en-US': new Validator(),
440
+ 'ja-JP': new Validator()
441
+ };
442
+
443
+ function getValidator(locale) {
444
+ return validators[locale] || validators['en-US'];
445
+ }
446
+
447
+ // 使用
448
+ const result = getValidator(req.locale).validate(
449
+ schema,
450
+ data,
451
+ { locale: req.locale }
452
+ );
453
+ ```
454
+
455
+ ### 4. 错误响应标准化
456
+
457
+ ```javascript
458
+ function sendValidationError(res, result, locale) {
459
+ res.status(400).json({
460
+ success: false,
461
+ code: 'VALIDATION_ERROR',
462
+ message: locale === 'zh-CN' ? '验证失败' : 'Validation failed',
463
+ errors: result.errors,
464
+ locale: locale
465
+ });
466
+ }
467
+
468
+ // 使用
469
+ if (!result.valid) {
470
+ return sendValidationError(res, result, req.locale);
471
+ }
472
+ ```
473
+
474
+ ---
475
+
476
+ ## 方案对比
477
+
478
+ | 方案 | 优点 | 缺点 | 推荐度 |
479
+ |------|------|------|--------|
480
+ | **方案1: 验证时指定** | ✅ 无竞态问题<br>✅ 支持并发<br>✅ 代码简洁 | - | ⭐⭐⭐⭐⭐ |
481
+ | 方案2: 临时切换 | ✅ 实现简单 | ⚠️ 并发竞态问题 | ⭐⭐⭐ |
482
+ | 方案3: 中间件 | ✅ 自动化<br>✅ 统一管理 | - | ⭐⭐⭐⭐⭐ |
483
+
484
+ **推荐**: 方案1 + 方案3(中间件封装)
485
+
486
+ ---
487
+
488
+ ## 常见问题
489
+
490
+ ### Q1: 如何处理不支持的语言?
491
+
492
+ **A**: 回退到默认语言
493
+
494
+ ```javascript
495
+ function parseAcceptLanguage(acceptLanguage) {
496
+ // ...解析逻辑
497
+ return supportedLocale || 'en-US'; // 默认英文
498
+ }
499
+ ```
500
+
501
+ ### Q2: 是否支持动态加载语言包?
502
+
503
+ **A**: 支持
504
+
505
+ ```javascript
506
+ async function loadLocale(locale) {
507
+ if (!Locale.getAvailableLocales().includes(locale)) {
508
+ const messages = await import(`./locales/${locale}.json`);
509
+ Locale.addLocale(locale, messages);
510
+ }
511
+ }
512
+
513
+ // 使用
514
+ app.use(async (req, res, next) => {
515
+ await loadLocale(req.locale);
516
+ next();
517
+ });
518
+ ```
519
+
520
+ ### Q3: 如何自定义某些字段的错误消息?
521
+
522
+ **A**: 使用 `.messages()` 方法
523
+
524
+ ```javascript
525
+ const schema = dsl({
526
+ password: 'string:8-64!'
527
+ .label('密码')
528
+ .messages({
529
+ 'required': req.locale === 'zh-CN'
530
+ ? '请输入密码'
531
+ : 'Please enter password',
532
+ 'min': req.locale === 'zh-CN'
533
+ ? '密码太短了,至少8个字符'
534
+ : 'Password is too short, at least 8 characters'
535
+ })
536
+ });
537
+ ```
538
+
539
+ ---
540
+
541
+ ## 相关文档
542
+
543
+ - [String 扩展](./string-extensions.md#多语言支持)
544
+ - [Locale API](./api-reference.md#locale-类)
545
+ - [Validator API](./api-reference.md#validator-类)
546
+
547
+ ---
548
+
549
+ **最后更新**: 2025-12-25
550
+ **作者**: SchemaIO Team
551
+
552
+