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,233 @@
1
+ /**
2
+ * 错误码定义
3
+ *
4
+ * 定义所有内置的错误码和默认错误消息
5
+ * v2.0.1: 支持简化格式(无类型前缀)
6
+ *
7
+ * @module lib/core/ErrorCodes
8
+ */
9
+
10
+ const ERROR_CODES = {
11
+ // ========== 字符串类型错误 ==========
12
+ // v2.0.1: 简化格式(推荐,用户友好)
13
+ 'min': {
14
+ code: 'TOO_SHORT',
15
+ message: '{{#label}} length must be at least {{#limit}} characters'
16
+ },
17
+ 'max': {
18
+ code: 'TOO_LONG',
19
+ message: '{{#label}} length must be at most {{#limit}} characters'
20
+ },
21
+ // ajv 实际使用的关键字(映射到简化版本)
22
+ 'minLength': {
23
+ code: 'TOO_SHORT',
24
+ message: '{{#label}} length must be at least {{#limit}} characters'
25
+ },
26
+ 'maxLength': {
27
+ code: 'TOO_LONG',
28
+ message: '{{#label}} length must be at most {{#limit}} characters'
29
+ },
30
+ 'email': {
31
+ code: 'INVALID_EMAIL',
32
+ message: '{{#label}} must be a valid email address'
33
+ },
34
+ 'url': {
35
+ code: 'INVALID_URL',
36
+ message: '{{#label}} must be a valid URL'
37
+ },
38
+ 'pattern': {
39
+ code: 'INVALID_PATTERN',
40
+ message: '{{#label}} format is invalid'
41
+ },
42
+ 'format': {
43
+ code: 'INVALID_FORMAT',
44
+ message: '{{#label}} must match format "{{#format}}"'
45
+ },
46
+ 'required': {
47
+ code: 'REQUIRED',
48
+ message: '{{#label}} is required'
49
+ },
50
+
51
+ // Formats (Standardized)
52
+ 'format.email': {
53
+ code: 'INVALID_EMAIL',
54
+ message: '{{#label}} must be a valid email address'
55
+ },
56
+ 'format.url': {
57
+ code: 'INVALID_URL',
58
+ message: '{{#label}} must be a valid URL'
59
+ },
60
+ 'format.uuid': {
61
+ code: 'INVALID_UUID',
62
+ message: '{{#label}} must be a valid UUID'
63
+ },
64
+ 'format.ipv4': {
65
+ code: 'INVALID_IPV4',
66
+ message: '{{#label}} must be a valid IPv4 address'
67
+ },
68
+ 'format.ipv6': {
69
+ code: 'INVALID_IPV6',
70
+ message: '{{#label}} must be a valid IPv6 address'
71
+ },
72
+ 'string.hostname': {
73
+ code: 'STRING_INVALID_HOSTNAME',
74
+ message: '{{#label}} must be a valid hostname'
75
+ },
76
+ 'string.pattern': {
77
+ code: 'STRING_PATTERN_MISMATCH',
78
+ message: '{{#label}} format is invalid'
79
+ },
80
+ 'string.enum': {
81
+ code: 'STRING_INVALID_ENUM',
82
+ message: '{{#label}} must be one of: {{#valids}}'
83
+ },
84
+
85
+ // ========== 数字类型错误 ==========
86
+ 'number.base': {
87
+ code: 'NUMBER_INVALID_TYPE',
88
+ message: '{{#label}} must be a number'
89
+ },
90
+ 'number.min': {
91
+ code: 'NUMBER_TOO_SMALL',
92
+ message: '{{#label}} must be greater than or equal to {{#limit}}'
93
+ },
94
+ 'number.max': {
95
+ code: 'NUMBER_TOO_LARGE',
96
+ message: '{{#label}} must be less than or equal to {{#limit}}'
97
+ },
98
+ 'number.integer': {
99
+ code: 'NUMBER_NOT_INTEGER',
100
+ message: '{{#label}} must be an integer'
101
+ },
102
+ 'number.positive': {
103
+ code: 'NUMBER_NOT_POSITIVE',
104
+ message: '{{#label}} must be a positive number'
105
+ },
106
+ 'number.negative': {
107
+ code: 'NUMBER_NOT_NEGATIVE',
108
+ message: '{{#label}} must be a negative number'
109
+ },
110
+
111
+ // ========== 布尔类型错误 ==========
112
+ 'boolean.base': {
113
+ code: 'BOOLEAN_INVALID_TYPE',
114
+ message: '{{#label}} must be a boolean'
115
+ },
116
+
117
+ // ========== 对象类型错误 ==========
118
+ 'object.base': {
119
+ code: 'OBJECT_INVALID_TYPE',
120
+ message: '{{#label}} must be an object'
121
+ },
122
+ 'object.min': {
123
+ code: 'OBJECT_TOO_FEW_KEYS',
124
+ message: '{{#label}} must have at least {{#limit}} keys'
125
+ },
126
+ 'object.max': {
127
+ code: 'OBJECT_TOO_MANY_KEYS',
128
+ message: '{{#label}} must have at most {{#limit}} keys'
129
+ },
130
+ 'object.unknown': {
131
+ code: 'OBJECT_UNKNOWN_KEY',
132
+ message: '{{#label}} contains unknown key: {{#key}}'
133
+ },
134
+
135
+ // ========== 数组类型错误 ==========
136
+ 'array.base': {
137
+ code: 'ARRAY_INVALID_TYPE',
138
+ message: '{{#label}} must be an array'
139
+ },
140
+ 'array.min': {
141
+ code: 'ARRAY_TOO_FEW_ITEMS',
142
+ message: '{{#label}} must have at least {{#limit}} items'
143
+ },
144
+ 'array.max': {
145
+ code: 'ARRAY_TOO_MANY_ITEMS',
146
+ message: '{{#label}} must have at most {{#limit}} items'
147
+ },
148
+ 'array.length': {
149
+ code: 'ARRAY_INVALID_LENGTH',
150
+ message: '{{#label}} must have exactly {{#limit}} items'
151
+ },
152
+ 'array.unique': {
153
+ code: 'ARRAY_NOT_UNIQUE',
154
+ message: '{{#label}} must contain unique items'
155
+ },
156
+
157
+ // ========== 日期类型错误 ==========
158
+ 'date.base': {
159
+ code: 'DATE_INVALID_TYPE',
160
+ message: '{{#label}} must be a valid date'
161
+ },
162
+ 'date.min': {
163
+ code: 'DATE_TOO_EARLY',
164
+ message: '{{#label}} must be on or after {{#limit}}'
165
+ },
166
+ 'date.max': {
167
+ code: 'DATE_TOO_LATE',
168
+ message: '{{#label}} must be on or before {{#limit}}'
169
+ },
170
+
171
+ // ========== 通用错误 ==========
172
+ 'type': {
173
+ code: 'INVALID_TYPE',
174
+ message: '{{#label}} must be of type {{#expected}}'
175
+ },
176
+ 'any.required': {
177
+ code: 'FIELD_REQUIRED',
178
+ message: '{{#label}} is required'
179
+ },
180
+ 'any.invalid': {
181
+ code: 'FIELD_INVALID',
182
+ message: '{{#label}} contains an invalid value'
183
+ },
184
+ 'any.only': {
185
+ code: 'FIELD_NOT_MATCH',
186
+ message: '{{#label}} must match {{#valids}}'
187
+ },
188
+ 'any.unknown': {
189
+ code: 'FIELD_UNKNOWN',
190
+ message: '{{#key}} is not allowed'
191
+ },
192
+
193
+ // ========== 自定义验证错误 ==========
194
+ 'custom.validation': {
195
+ code: 'CUSTOM_VALIDATION_FAILED',
196
+ message: 'Validation failed'
197
+ }
198
+ };
199
+
200
+ /**
201
+ * 获取错误信息
202
+ * @param {string} type - 错误类型
203
+ * @returns {Object} 错误信息
204
+ */
205
+ function getErrorInfo(type) {
206
+ const errorInfo = ERROR_CODES[type];
207
+ if (!errorInfo) {
208
+ return {
209
+ code: 'UNKNOWN_ERROR',
210
+ message: 'Unknown validation error'
211
+ };
212
+ }
213
+
214
+ return {
215
+ code: errorInfo.code,
216
+ message: errorInfo.message
217
+ };
218
+ }
219
+
220
+ /**
221
+ * 获取所有错误码
222
+ * @returns {Object} 所有错误码
223
+ */
224
+ function getAllErrorCodes() {
225
+ return ERROR_CODES;
226
+ }
227
+
228
+ module.exports = {
229
+ ERROR_CODES,
230
+ getErrorInfo,
231
+ getAllErrorCodes
232
+ };
233
+
@@ -0,0 +1,342 @@
1
+ // lib/core/ErrorFormatter.js
2
+
3
+ const CONSTANTS = require('../config/constants');
4
+ const defaultLocales = require('../locales');
5
+
6
+ /**
7
+ * 错误格式化器
8
+ * 将验证错误格式化为友好的消息
9
+ *
10
+ * @class ErrorFormatter
11
+ */
12
+ class ErrorFormatter {
13
+ constructor(locale = 'zh-CN') {
14
+ this.locale = locale;
15
+ this.messages = this._loadMessages(locale);
16
+ }
17
+
18
+ /**
19
+ * 加载错误消息模板
20
+ * @private
21
+ * @param {string} locale - 语言环境
22
+ * @returns {Object} 消息模板
23
+ */
24
+ _loadMessages(locale) {
25
+ // 优先使用 Locale 类中注册的语言包
26
+ const Locale = require('./Locale');
27
+ const registered = Locale.locales[locale];
28
+ const defaults = defaultLocales[locale] || defaultLocales['en-US'];
29
+
30
+ if (registered) {
31
+ return { ...defaults, ...registered };
32
+ }
33
+
34
+ // 其次使用默认语言包
35
+ return defaults;
36
+ }
37
+
38
+ /**
39
+ * 格式化单个错误或错误数组
40
+ * @param {Object|Array<Object>} error - 错误对象或错误数组
41
+ * @param {string} [locale] - 动态指定语言(可选)
42
+ * @returns {string|Array<Object>} 格式化后的错误消息或错误对象数组
43
+ */
44
+ format(error, locale) {
45
+ // 如果是数组,格式化为详细对象数组
46
+ if (Array.isArray(error)) {
47
+ return this.formatDetailed(error, locale);
48
+ }
49
+
50
+ // 获取当前使用的消息模板
51
+ const messages = locale ? this._loadMessages(locale) : this.messages;
52
+
53
+ // 单个错误对象格式化
54
+ const template = messages[error.type] || error.message;
55
+ return this._interpolate(template, {
56
+ ...error,
57
+ path: error.path || 'value',
58
+ allowed: Array.isArray(error.allowed) ? error.allowed.join(', ') : error.allowed
59
+ });
60
+ }
61
+
62
+ /**
63
+ * 格式化所有错误
64
+ * @param {Array<Object>} errors - 错误数组
65
+ * @param {string} [locale] - 动态指定语言(可选)
66
+ * @returns {Array<string>} 格式化后的错误消息数组
67
+ */
68
+ formatAll(errors, locale) {
69
+ return errors.map(err => this.format(err, locale));
70
+ }
71
+
72
+ /**
73
+ * 格式化为详细对象
74
+ * @param {Array<Object>} errors - ajv错误数组
75
+ * @param {string} [locale] - 动态指定语言(可选)
76
+ * @returns {Array<Object>} 详细错误对象数组
77
+ */
78
+ formatDetailed(errors, locale) {
79
+ if (!Array.isArray(errors)) {
80
+ errors = [errors];
81
+ }
82
+
83
+ // 获取当前使用的消息模板
84
+ const messages = locale ? this._loadMessages(locale) : this.messages;
85
+
86
+ return errors.map(err => {
87
+ // 处理 ajv 错误格式
88
+ const keyword = err.keyword || err.type || 'validation';
89
+ const instancePath = err.instancePath || err.path || '';
90
+ const fieldName = instancePath.replace(/^\//, '') || (err.params && err.params.missingProperty) || 'value';
91
+
92
+ // 获取 Schema 中的自定义信息
93
+ let schema = err.parentSchema || err.schema || {};
94
+
95
+ // 特殊处理 required 错误
96
+ if (keyword === 'required' && err.params && err.params.missingProperty) {
97
+ const prop = err.params.missingProperty;
98
+ if (schema.properties && schema.properties[prop]) {
99
+ schema = schema.properties[prop];
100
+ }
101
+ }
102
+
103
+ let label = schema._label;
104
+
105
+ if (label) {
106
+ // 如果显式设置了 label,尝试翻译它
107
+ if (messages[label]) {
108
+ label = messages[label];
109
+ }
110
+ } else {
111
+ // 如果没有显式设置 label,尝试自动查找翻译 (label.fieldName)
112
+ // 将路径分隔符 / 转换为 . (例如 address/city -> address.city)
113
+ const autoKey = `label.${fieldName.replace(/\//g, '.')}`;
114
+ if (messages[autoKey]) {
115
+ label = messages[autoKey];
116
+ } else {
117
+ // 没找到翻译,回退到 fieldName
118
+ label = fieldName;
119
+ }
120
+ }
121
+
122
+ const customMessages = schema._customMessages || {};
123
+
124
+ // 关键字映射 (ajv keyword -> schema-dsl 简写)
125
+ // 支持 min/max 作为 minLength/maxLength 的简写
126
+ const keywordMap = {
127
+ 'minLength': 'min',
128
+ 'maxLength': 'max',
129
+ 'minimum': 'min',
130
+ 'maximum': 'max',
131
+ 'minItems': 'min',
132
+ 'maxItems': 'max',
133
+ 'pattern': 'pattern',
134
+ 'format': 'format',
135
+ 'required': 'required',
136
+ 'enum': 'enum'
137
+ };
138
+
139
+ const mappedKeyword = keywordMap[keyword] || keyword;
140
+ const type = schema.type || 'string';
141
+
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'];
154
+
155
+ // 自动查找 format 类型的消息 (例如 format.email)
156
+ if (!message && mappedKeyword === 'format' && err.params && err.params.format) {
157
+ let formatName = err.params.format;
158
+ // 映射 uri -> url
159
+ if (formatName === 'uri') formatName = 'url';
160
+
161
+ const formatKey = `format.${formatName}`;
162
+
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];
170
+ }
171
+ }
172
+
173
+ if (!message) {
174
+ // 使用默认模板
175
+ const template = messages[mappedKeyword] || messages[keyword] || err.message || 'Validation error';
176
+ message = template;
177
+ } else {
178
+ // 检查 message 是否为 key (包含点号且无空格,或者是已知的 key)
179
+ // 如果是 key,尝试从 messages 中查找
180
+ if (typeof message === 'string' && (message.includes('.') || messages[message])) {
181
+ let translated = messages[message];
182
+
183
+ // 尝试回退查找 (例如 pattern.phone.cn -> pattern.phone)
184
+ if (!translated && message.includes('.')) {
185
+ const parts = message.split('.');
186
+ while (parts.length > 1 && !translated) {
187
+ parts.pop();
188
+ const fallbackKey = parts.join('.');
189
+ translated = messages[fallbackKey];
190
+ }
191
+ }
192
+
193
+ if (translated) {
194
+ message = translated;
195
+ }
196
+ }
197
+ }
198
+
199
+ // 插值替换
200
+ const limit = err.params ? (err.params.limit || err.params.limitLength || err.params.comparison) : undefined;
201
+
202
+ const interpolateData = {
203
+ ...err.params,
204
+ path: label, // 使用 label 替换 path
205
+ label: label,
206
+ value: err.data,
207
+ limit: limit,
208
+ // 映射 min/max 以匹配模板
209
+ min: limit,
210
+ max: limit,
211
+ expected: err.params ? err.params.type : undefined
212
+ };
213
+
214
+ message = this._interpolate(message, interpolateData);
215
+
216
+ return {
217
+ path: fieldName,
218
+ message: message,
219
+ keyword: keyword,
220
+ params: err.params
221
+ };
222
+ });
223
+ }
224
+
225
+ /**
226
+ * 格式化为分组对象
227
+ * @param {Array<Object>} errors - 错误数组
228
+ * @returns {Object} 按路径分组的错误
229
+ */
230
+ formatGrouped(errors) {
231
+ const grouped = {};
232
+
233
+ for (const error of errors) {
234
+ const path = error.path || 'value';
235
+ if (!grouped[path]) {
236
+ grouped[path] = [];
237
+ }
238
+ grouped[path].push(this.format(error));
239
+ }
240
+
241
+ return grouped;
242
+ }
243
+
244
+ /**
245
+ * 格式化为纯文本
246
+ * @param {Array<Object>} errors - 错误数组
247
+ * @param {string} [separator='\n'] - 分隔符
248
+ * @returns {string} 纯文本错误消息
249
+ */
250
+ formatText(errors, separator = '\n') {
251
+ return this.formatAll(errors).join(separator);
252
+ }
253
+
254
+ /**
255
+ * 格式化为JSON字符串
256
+ * @param {Array<Object>} errors - 错误数组
257
+ * @param {boolean} [pretty=false] - 是否美化
258
+ * @returns {string} JSON字符串
259
+ */
260
+ formatJSON(errors, pretty = false) {
261
+ const formatted = this.formatDetailed(errors);
262
+ return pretty ? JSON.stringify(formatted, null, 2) : JSON.stringify(formatted);
263
+ }
264
+
265
+ /**
266
+ * 格式化为HTML列表
267
+ * @param {Array<Object>} errors - 错误数组
268
+ * @returns {string} HTML字符串
269
+ */
270
+ formatHTML(errors) {
271
+ const items = this.formatAll(errors)
272
+ .map(msg => ` <li>${this._escapeHTML(msg)}</li>`)
273
+ .join('\n');
274
+
275
+ return `<ul class="validation-errors">\n${items}\n</ul>`;
276
+ }
277
+
278
+ /**
279
+ * 插值替换
280
+ * @private
281
+ * @param {string} template - 模板字符串
282
+ * @param {Object} data - 数据对象
283
+ * @returns {string} 替换后的字符串
284
+ */
285
+ _interpolate(template, data) {
286
+ if (!template || typeof template !== 'string') {
287
+ return data.message || 'Validation error';
288
+ }
289
+
290
+
291
+ // 支持 {{#key}} 和 {key} 两种格式
292
+ return template.replace(/\{\{?#?(\w+)\}?\}/g, (match, key) => {
293
+ return data[key] !== undefined ? data[key] : match;
294
+ });
295
+ }
296
+
297
+ /**
298
+ * HTML转义
299
+ * @private
300
+ * @param {string} text - 文本
301
+ * @returns {string} 转义后的文本
302
+ */
303
+ _escapeHTML(text) {
304
+ const map = {
305
+ '&': '&amp;',
306
+ '<': '&lt;',
307
+ '>': '&gt;',
308
+ '"': '&quot;',
309
+ "'": '&#039;'
310
+ };
311
+ return text.replace(/[&<>"']/g, char => map[char]);
312
+ }
313
+
314
+ /**
315
+ * 设置语言环境
316
+ * @param {string} locale - 语言环境
317
+ */
318
+ setLocale(locale) {
319
+ this.locale = locale;
320
+ this.messages = this._loadMessages(locale);
321
+ }
322
+
323
+ /**
324
+ * 添加自定义消息模板
325
+ * @param {string} type - 错误类型
326
+ * @param {string} template - 消息模板
327
+ */
328
+ addMessage(type, template) {
329
+ this.messages[type] = template;
330
+ }
331
+
332
+ /**
333
+ * 批量添加自定义消息模板
334
+ * @param {Object} messages - 消息模板对象
335
+ */
336
+ addMessages(messages) {
337
+ Object.assign(this.messages, messages);
338
+ }
339
+ }
340
+
341
+ module.exports = ErrorFormatter;
342
+