schema-dsl 1.0.7 → 1.0.9

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.
package/docs/i18n.md CHANGED
@@ -1,7 +1,19 @@
1
1
  # 多语言配置指南
2
2
 
3
- **版本**: v1.0.1
4
- **最后更新**: 2025-12-31
3
+ **版本**: v1.0.9
4
+ **最后更新**: 2026-01-04
5
+
6
+ ---
7
+
8
+ ## 📚 多语言文档导航
9
+
10
+ 根据你的需求选择合适的文档:
11
+
12
+ - 🚀 **新手快速上手**: [i18n.md](./i18n.md)(当前文档)- 5分钟学会基础用法
13
+ - 📖 **完整使用指南**: [i18n-user-guide.md](./i18n-user-guide.md) - 详细的用户指南和最佳实践
14
+ - 🎯 **添加新语言**: [add-custom-locale.md](./add-custom-locale.md) - 添加自定义语言包教程
15
+ - 🔄 **动态切换语言**: [dynamic-locale.md](./dynamic-locale.md) - API开发中的动态语言切换
16
+ - 🌐 **前端集成**: [frontend-i18n-guide.md](./frontend-i18n-guide.md) - 前后端分离项目集成指南
5
17
 
6
18
  ---
7
19
 
@@ -9,10 +21,61 @@
9
21
 
10
22
  schema-dsl 支持完整的多语言功能,允许你自定义字段标签和错误消息的翻译。
11
23
 
24
+ **v1.0.9 新增**:
25
+ - ✅ 支持参数化语言切换(无需修改全局状态)
26
+ - ✅ 支持自定义错误消息(三种格式)
27
+ - ✅ TypeScript 类型定义完整
28
+
12
29
  ---
13
30
 
14
31
  ## 🚀 快速开始
15
32
 
33
+ ### 方式1:验证时动态指定语言(推荐)⭐
34
+
35
+ ```javascript
36
+ const { dsl, validate } = require('schema-dsl');
37
+
38
+ const schema = dsl({ username: 'string:3-32!' });
39
+
40
+ // 使用中文错误消息
41
+ const result = validate(schema, { username: 'ab' }, { locale: 'zh-CN' });
42
+ // message: "username长度不能少于3个字符"
43
+
44
+ // 使用英文错误消息
45
+ const result2 = validate(schema, { username: 'ab' }, { locale: 'en-US' });
46
+ // message: "username length must be at least 3"
47
+ ```
48
+
49
+ ### 方式2:使用全局配置(向后兼容)
50
+
51
+ ```javascript
52
+ const { Locale } = require('schema-dsl');
53
+
54
+ // 设置默认语言
55
+ Locale.setLocale('zh-CN');
56
+
57
+ // 后续验证会使用中文错误消息
58
+ const result = validate(schema, data);
59
+ ```
60
+
61
+ ---
62
+
63
+ ## 📝 内置语言支持
64
+
65
+ schema-dsl 内置了以下语言包:
66
+
67
+ | 语言代码 | 语言名称 | 支持状态 |
68
+ |---------|---------|---------|
69
+ | `zh-CN` | 简体中文 | ✅ 完整支持 |
70
+ | `en-US` | 英语(美国)| ✅ 完整支持 |
71
+ | `ja-JP` | 日语 | ✅ 完整支持 |
72
+ | `es-ES` | 西班牙语 | ✅ 完整支持 |
73
+ | `fr-FR` | 法语 | ✅ 完整支持 |
74
+
75
+ ---
76
+
77
+ ## 🎯 高级用法
78
+
16
79
  ### 基础配置
17
80
 
18
81
  ```javascript
package/index.d.ts CHANGED
@@ -112,6 +112,35 @@ declare module 'schema-dsl' {
112
112
  field?: string;
113
113
  }
114
114
 
115
+ /**
116
+ * 验证选项
117
+ *
118
+ * @description validate() 和 Validator.validate() 的配置选项
119
+ *
120
+ * @example
121
+ * ```typescript
122
+ * const options: ValidateOptions = {
123
+ * format: true,
124
+ * locale: 'zh-CN',
125
+ * messages: {
126
+ * min: '至少需要 {{#limit}} 个字符'
127
+ * }
128
+ * };
129
+ *
130
+ * const result = validate(schema, data, options);
131
+ * ```
132
+ */
133
+ export interface ValidateOptions {
134
+ /** 是否格式化错误(默认true) */
135
+ format?: boolean;
136
+ /** 动态指定语言(如 'zh-CN', 'en-US', 'ja-JP', 'es-ES', 'fr-FR') */
137
+ locale?: string;
138
+ /** 自定义错误消息 */
139
+ messages?: ErrorMessages;
140
+ /** 扩展选项 */
141
+ [key: string]: any;
142
+ }
143
+
115
144
  /**
116
145
  * 错误消息对象
117
146
  *
@@ -1257,9 +1286,27 @@ declare module 'schema-dsl' {
1257
1286
  * 验证数据
1258
1287
  * @param schema - JSON Schema对象
1259
1288
  * @param data - 要验证的数据
1289
+ * @param options - 验证选项
1260
1290
  * @returns 验证结果
1291
+ *
1292
+ * @example
1293
+ * ```typescript
1294
+ * const validator = new Validator();
1295
+ *
1296
+ * // 使用默认语言
1297
+ * const result1 = validator.validate(schema, data);
1298
+ *
1299
+ * // 动态指定语言
1300
+ * const result2 = validator.validate(schema, data, { locale: 'zh-CN' });
1301
+ *
1302
+ * // 自定义错误消息
1303
+ * const result3 = validator.validate(schema, data, {
1304
+ * locale: 'zh-CN',
1305
+ * messages: { min: '至少{{#limit}}个字符' }
1306
+ * });
1307
+ * ```
1261
1308
  */
1262
- validate<T = any>(schema: JSONSchema, data: any): ValidationResult<T>;
1309
+ validate<T = any>(schema: JSONSchema, data: any, options?: ValidateOptions): ValidationResult<T>;
1263
1310
 
1264
1311
  /**
1265
1312
  * 获取底层ajv实例
@@ -1300,14 +1347,25 @@ declare module 'schema-dsl' {
1300
1347
  * import { dsl, validate } from 'schema-dsl';
1301
1348
  *
1302
1349
  * const schema = dsl({ email: 'email!' });
1303
- * const result = validate(schema, { email: 'test@example.com' });
1304
- *
1305
- * if (result.valid) {
1350
+ *
1351
+ * // 基本验证
1352
+ * const result1 = validate(schema, { email: 'test@example.com' });
1353
+ *
1354
+ * // 指定语言
1355
+ * const result2 = validate(schema, { email: 'invalid' }, { locale: 'zh-CN' });
1356
+ *
1357
+ * if (result2.valid) {
1306
1358
  * console.log('验证通过');
1359
+ * } else {
1360
+ * console.log('错误:', result2.errors); // 中文错误消息
1307
1361
  * }
1308
1362
  * ```
1309
1363
  */
1310
- export function validate<T = any>(schema: JSONSchema | SchemaIO, data: any): ValidationResult<T>;
1364
+ export function validate<T = any>(
1365
+ schema: JSONSchema | SchemaIO,
1366
+ data: any,
1367
+ options?: ValidateOptions
1368
+ ): ValidationResult<T>;
1311
1369
 
1312
1370
  /**
1313
1371
  * 便捷异步验证方法(推荐)
package/index.js CHANGED
@@ -128,16 +128,25 @@ function getDefaultValidator() {
128
128
  * 便捷验证方法(使用默认Validator)
129
129
  * @param {Object} schema - JSON Schema对象
130
130
  * @param {*} data - 待验证数据
131
+ * @param {Object} [options] - 验证选项
132
+ * @param {boolean} [options.format=true] - 是否格式化错误
133
+ * @param {string} [options.locale] - 动态指定语言(如 'zh-CN', 'en-US')
134
+ * @param {Object} [options.messages] - 自定义错误消息
131
135
  * @returns {Object} 验证结果
132
136
  *
133
137
  * @example
134
138
  * const { validate, dsl } = require('schema-dsl');
135
139
  *
136
140
  * const schema = dsl({ email: 'email!' });
137
- * const result = validate(schema, { email: 'test@example.com' });
141
+ *
142
+ * // 基本验证
143
+ * const result1 = validate(schema, { email: 'test@example.com' });
144
+ *
145
+ * // 指定语言
146
+ * const result2 = validate(schema, { email: 'invalid' }, { locale: 'zh-CN' });
138
147
  */
139
- function validate(schema, data) {
140
- return getDefaultValidator().validate(schema, data);
148
+ function validate(schema, data, options) {
149
+ return getDefaultValidator().validate(schema, data, options);
141
150
  }
142
151
 
143
152
  // ========== 工具函数 ==========
@@ -38,20 +38,41 @@ class ErrorFormatter {
38
38
  /**
39
39
  * 格式化单个错误或错误数组
40
40
  * @param {Object|Array<Object>} error - 错误对象或错误数组
41
- * @param {string} [locale] - 动态指定语言(可选)
41
+ * @param {string|Object} [localeOrOptions] - 语言代码字符串或选项对象
42
+ * @param {string} [localeOrOptions.locale] - 动态指定语言(可选)
43
+ * @param {Object} [localeOrOptions.messages] - 自定义错误消息(可选)
42
44
  * @returns {string|Array<Object>} 格式化后的错误消息或错误对象数组
43
45
  */
44
- format(error, locale) {
46
+ format(error, localeOrOptions) {
47
+
48
+ // ✅ 支持两种调用方式:
49
+ // 1. format(errors, 'zh-CN') - 旧版兼容
50
+ // 2. format(errors, { locale: 'zh-CN', messages: {...} }) - 新版增强
51
+ let locale;
52
+ let customMessages;
53
+
54
+ if (typeof localeOrOptions === 'string') {
55
+ // 旧版:直接传入语言代码
56
+ locale = localeOrOptions;
57
+ } else if (typeof localeOrOptions === 'object' && localeOrOptions !== null) {
58
+ // 新版:传入选项对象
59
+ locale = localeOrOptions.locale;
60
+ customMessages = localeOrOptions.messages;
61
+ }
62
+
45
63
  // 如果是数组,格式化为详细对象数组
46
64
  if (Array.isArray(error)) {
47
- return this.formatDetailed(error, locale);
65
+ return this.formatDetailed(error, locale, customMessages);
48
66
  }
49
67
 
50
68
  // 获取当前使用的消息模板
51
69
  const messages = locale ? this._loadMessages(locale) : this.messages;
52
70
 
71
+ // 合并自定义消息
72
+ const finalMessages = customMessages ? { ...messages, ...customMessages } : messages;
73
+
53
74
  // 单个错误对象格式化
54
- const template = messages[error.type] || error.message;
75
+ const template = finalMessages[error.type] || error.message;
55
76
  return this._interpolate(template, {
56
77
  ...error,
57
78
  path: error.path || 'value',
@@ -73,16 +94,42 @@ class ErrorFormatter {
73
94
  * 格式化为详细对象
74
95
  * @param {Array<Object>} errors - ajv错误数组
75
96
  * @param {string} [locale] - 动态指定语言(可选)
97
+ * @param {Object} [customMessages] - 自定义错误消息(可选)
76
98
  * @returns {Array<Object>} 详细错误对象数组
77
99
  */
78
- formatDetailed(errors, locale) {
100
+ formatDetailed(errors, locale, customMessages) {
101
+
79
102
  if (!Array.isArray(errors)) {
80
103
  errors = [errors];
81
104
  }
82
105
 
106
+ // 过滤冗余的包装错误(v1.0.7.1 增强)
107
+ // 当存在具体字段错误时,移除通用的包装错误
108
+ const hasConcreteErrors = errors.some(err => {
109
+ const keyword = err.keyword;
110
+ // 包装错误关键字:if, anyOf, oneOf, allOf
111
+ return keyword !== 'if' &&
112
+ keyword !== 'anyOf' &&
113
+ keyword !== 'oneOf' &&
114
+ keyword !== 'error';
115
+ });
116
+
117
+ if (hasConcreteErrors) {
118
+ // 过滤掉包装错误(这些是通用的组合验证错误)
119
+ errors = errors.filter(err => {
120
+ const keyword = err.keyword;
121
+ return keyword !== 'if' &&
122
+ keyword !== 'anyOf' &&
123
+ keyword !== 'oneOf';
124
+ });
125
+ }
126
+
83
127
  // 获取当前使用的消息模板
84
128
  const messages = locale ? this._loadMessages(locale) : this.messages;
85
129
 
130
+ // ✅ 合并参数传入的自定义消息(在整个 map 循环中使用)
131
+ const finalMessages = customMessages ? { ...messages, ...customMessages } : messages;
132
+
86
133
  return errors.map(err => {
87
134
  // 处理 ajv 错误格式
88
135
  const keyword = err.keyword || err.type || 'validation';
@@ -104,22 +151,27 @@ class ErrorFormatter {
104
151
 
105
152
  if (label) {
106
153
  // 如果显式设置了 label,尝试翻译它
107
- if (messages[label]) {
108
- label = messages[label];
154
+ if (finalMessages[label]) {
155
+ label = finalMessages[label];
109
156
  }
110
157
  } else {
111
158
  // 如果没有显式设置 label,尝试自动查找翻译 (label.fieldName)
112
159
  // 将路径分隔符 / 转换为 . (例如 address/city -> address.city)
113
160
  const autoKey = `label.${fieldName.replace(/\//g, '.')}`;
114
- if (messages[autoKey]) {
115
- label = messages[autoKey];
161
+ if (finalMessages[autoKey]) {
162
+ label = finalMessages[autoKey];
116
163
  } else {
117
164
  // 没找到翻译,回退到 fieldName
118
165
  label = fieldName;
119
166
  }
120
167
  }
121
168
 
122
- const customMessages = schema._customMessages || {};
169
+ // Schema 中的自定义消息
170
+ const schemaCustomMessages = schema._customMessages || {};
171
+
172
+ // ✅ 合并优先级:schemaCustomMessages > finalMessages
173
+ // schemaCustomMessages 是字段级的自定义消息,优先级最高
174
+ const mergedMessages = { ...finalMessages, ...schemaCustomMessages };
123
175
 
124
176
  // 关键字映射 (ajv keyword -> schema-dsl 简写)
125
177
  // 支持 min/max 作为 minLength/maxLength 的简写
@@ -139,18 +191,38 @@ class ErrorFormatter {
139
191
  const mappedKeyword = keywordMap[keyword] || keyword;
140
192
  const type = schema.type || 'string';
141
193
 
142
- // 查找自定义消息
143
- // 优先级:
144
- // 1. type.keyword (如 string.pattern)
145
- // 2. type.mappedKeyword (如 string.min)
146
- // 3. mappedKeyword (如 min)
147
- // 4. keyword (如 minLength,向后兼容)
148
- // 5. default
149
- let message = customMessages[`${type}.${keyword}`] ||
150
- customMessages[`${type}.${mappedKeyword}`] ||
151
- customMessages[mappedKeyword] ||
152
- customMessages[keyword] ||
153
- customMessages['default'];
194
+
195
+ // ✅ 优化:如果 schema 中有自定义消息,优先使用
196
+ // 支持三种自定义消息格式:
197
+ // 1. 键引用:_customMessages.pattern = "pattern.objectId" → 查找语言包中的 "pattern.objectId"
198
+ // 2. 模板字符串:_customMessages.min = "{{#label}}必须大于{{#limit}}" → 直接使用并插值
199
+ // 3. 最终消息:_customMessages.pattern = "手机号格式不正确" → 直接使用
200
+ let message;
201
+
202
+ // 1. 首先检查 schema 是否为该 keyword 定义了自定义消息
203
+ let customValue = schemaCustomMessages[keyword] || schemaCustomMessages[mappedKeyword];
204
+
205
+ if (customValue) {
206
+ // 尝试从 mergedMessages 中查找这个键
207
+ const lookupResult = mergedMessages[customValue];
208
+
209
+ if (lookupResult) {
210
+ // 找到了,说明它是一个键引用
211
+ message = lookupResult;
212
+ } else {
213
+ // 没找到,说明它本身就是模板或最终消息,直接使用
214
+ message = customValue;
215
+ }
216
+ }
217
+
218
+ // 2. 如果没有自定义消息,按照通用查找顺序
219
+ if (!message) {
220
+ message = mergedMessages[`${type}.${keyword}`] ||
221
+ mergedMessages[`${type}.${mappedKeyword}`] ||
222
+ mergedMessages[mappedKeyword] ||
223
+ mergedMessages[keyword] ||
224
+ mergedMessages['default'];
225
+ }
154
226
 
155
227
  // 自动查找 format 类型的消息 (例如 format.email)
156
228
  if (!message && mappedKeyword === 'format' && err.params && err.params.format) {
@@ -160,25 +232,22 @@ class ErrorFormatter {
160
232
 
161
233
  const formatKey = `format.${formatName}`;
162
234
 
163
- // 优先查找 customMessages 中的 format.email
164
- if (customMessages[formatKey]) {
165
- message = customMessages[formatKey];
166
- }
167
- // 其次查找全局/语言包中的 format.email
168
- else if (messages[formatKey]) {
169
- message = messages[formatKey];
235
+ // 优先查找 mergedMessages 中的 format.email
236
+ if (mergedMessages[formatKey]) {
237
+ message = mergedMessages[formatKey];
170
238
  }
171
239
  }
172
240
 
173
241
  if (!message) {
174
242
  // 使用默认模板
175
- const template = messages[mappedKeyword] || messages[keyword] || err.message || 'Validation error';
243
+ const template = mergedMessages[mappedKeyword] || mergedMessages[keyword] || err.message || 'Validation error';
176
244
  message = template;
177
245
  } else {
178
246
  // 检查 message 是否为 key (包含点号且无空格,或者是已知的 key)
179
247
  // 如果是 key,尝试从 messages 中查找
180
- if (typeof message === 'string' && (message.includes('.') || messages[message])) {
181
- let translated = messages[message];
248
+ if (typeof message === 'string' && (message.includes('.') || mergedMessages[message])) {
249
+
250
+ let translated = mergedMessages[message];
182
251
 
183
252
  // 尝试回退查找 (例如 pattern.phone.cn -> pattern.phone)
184
253
  if (!translated && message.includes('.')) {
@@ -186,7 +255,7 @@ class ErrorFormatter {
186
255
  while (parts.length > 1 && !translated) {
187
256
  parts.pop();
188
257
  const fallbackKey = parts.join('.');
189
- translated = messages[fallbackKey];
258
+ translated = mergedMessages[fallbackKey];
190
259
  }
191
260
  }
192
261
 
@@ -115,18 +115,7 @@ class Validator {
115
115
  * @returns {Object} 验证结果 { valid: boolean, errors: Array, data: * }
116
116
  */
117
117
  validate(schema, data, options = {}) {
118
- // ✅ 性能优化:提前检查语言切换,避免99%的无效操作
119
- if (options.locale && options.locale !== Locale.getLocale()) {
120
- const originalLocale = Locale.getLocale();
121
- Locale.setLocale(options.locale);
122
- try {
123
- return this._validateInternal(schema, data, options);
124
- } finally {
125
- Locale.setLocale(originalLocale);
126
- }
127
- }
128
-
129
- // 正常流程(无需切换语言)
118
+ // ✅ 优化:直接传递 locale 到内部方法,不再切换全局状态
130
119
  return this._validateInternal(schema, data, options);
131
120
  }
132
121
 
@@ -175,10 +164,13 @@ class Validator {
175
164
  // 执行验证
176
165
  const valid = validate(data);
177
166
 
178
- // 格式化错误
167
+ // ✅ 优化:直接传递 locale 和 messages 到格式化器,不修改全局状态
179
168
  const errors = valid ? [] : (
180
169
  shouldFormat
181
- ? this.errorFormatter.format(validate.errors, locale)
170
+ ? this.errorFormatter.format(validate.errors, {
171
+ locale,
172
+ messages: options.messages
173
+ })
182
174
  : validate.errors
183
175
  );
184
176
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "schema-dsl",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "简洁强大的JSON Schema验证库 - DSL语法 + String扩展 + 便捷validate",
5
5
  "main": "index.js",
6
6
  "exports": {
@@ -57,6 +57,7 @@
57
57
  "ajv-formats": "^2.1.1"
58
58
  },
59
59
  "devDependencies": {
60
+ "benchmark": "^2.1.4",
60
61
  "chai": "^4.5.0",
61
62
  "eslint": "^8.57.1",
62
63
  "joi": "^18.0.2",
@@ -67,4 +68,4 @@
67
68
  "yup": "^1.7.1",
68
69
  "zod": "^4.2.1"
69
70
  }
70
- }
71
+ }