schema-dsl 1.0.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.
- package/.eslintignore +10 -0
- package/.eslintrc.json +27 -0
- package/.github/CODE_OF_CONDUCT.md +45 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +57 -0
- package/.github/ISSUE_TEMPLATE/config.yml +11 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +45 -0
- package/.github/ISSUE_TEMPLATE/question.md +31 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +70 -0
- package/.github/SECURITY.md +184 -0
- package/.github/workflows/ci.yml +33 -0
- package/CHANGELOG.md +633 -0
- package/CONTRIBUTING.md +368 -0
- package/LICENSE +21 -0
- package/README.md +1184 -0
- package/STATUS.md +101 -0
- package/docs/FEATURE-INDEX.md +519 -0
- package/docs/INDEX.md +253 -0
- package/docs/api-reference.md +1096 -0
- package/docs/best-practices.md +672 -0
- package/docs/cache-manager.md +336 -0
- package/docs/design-philosophy.md +601 -0
- package/docs/dsl-syntax.md +653 -0
- package/docs/dynamic-locale.md +552 -0
- package/docs/error-handling.md +703 -0
- package/docs/export-guide.md +462 -0
- package/docs/export-limitations.md +551 -0
- package/docs/faq.md +577 -0
- package/docs/frontend-i18n-guide.md +290 -0
- package/docs/i18n-user-guide.md +476 -0
- package/docs/label-vs-description.md +262 -0
- package/docs/markdown-exporter.md +397 -0
- package/docs/mongodb-exporter.md +295 -0
- package/docs/multi-type-support.md +319 -0
- package/docs/mysql-exporter.md +273 -0
- package/docs/plugin-system.md +542 -0
- package/docs/postgresql-exporter.md +304 -0
- package/docs/quick-start.md +761 -0
- package/docs/schema-helper.md +340 -0
- package/docs/schema-utils-chaining.md +143 -0
- package/docs/schema-utils.md +490 -0
- package/docs/string-extensions.md +480 -0
- package/docs/troubleshooting.md +471 -0
- package/docs/type-converter.md +319 -0
- package/docs/type-reference.md +219 -0
- package/docs/validate-async.md +480 -0
- package/docs/validate.md +486 -0
- package/docs/validation-guide.md +484 -0
- package/examples/array-dsl-example.js +227 -0
- package/examples/custom-extension.js +85 -0
- package/examples/dsl-match-example.js +74 -0
- package/examples/dsl-style.js +118 -0
- package/examples/dynamic-locale-configuration.js +348 -0
- package/examples/dynamic-locale-example.js +287 -0
- package/examples/export-demo.js +130 -0
- package/examples/express-integration.js +376 -0
- package/examples/i18n-full-demo.js +310 -0
- package/examples/i18n-memory-safety.examples.js +268 -0
- package/examples/markdown-export.js +71 -0
- package/examples/middleware-usage.js +93 -0
- package/examples/new-features-comparison.js +315 -0
- package/examples/password-reset/README.md +153 -0
- package/examples/password-reset/schema.js +26 -0
- package/examples/password-reset/test.js +101 -0
- package/examples/plugin-system.examples.js +205 -0
- package/examples/schema-utils-chaining.examples.js +250 -0
- package/examples/simple-example.js +122 -0
- package/examples/string-extensions.js +297 -0
- package/examples/user-registration/README.md +156 -0
- package/examples/user-registration/routes.js +92 -0
- package/examples/user-registration/schema.js +150 -0
- package/examples/user-registration/server.js +74 -0
- package/index.d.ts +1999 -0
- package/index.js +282 -0
- package/index.mjs +30 -0
- package/lib/adapters/DslAdapter.js +699 -0
- package/lib/adapters/index.js +20 -0
- package/lib/config/constants.js +286 -0
- package/lib/config/patterns/creditCard.js +9 -0
- package/lib/config/patterns/idCard.js +9 -0
- package/lib/config/patterns/index.js +8 -0
- package/lib/config/patterns/licensePlate.js +4 -0
- package/lib/config/patterns/passport.js +4 -0
- package/lib/config/patterns/phone.js +9 -0
- package/lib/config/patterns/postalCode.js +5 -0
- package/lib/core/CacheManager.js +376 -0
- package/lib/core/DslBuilder.js +740 -0
- package/lib/core/ErrorCodes.js +233 -0
- package/lib/core/ErrorFormatter.js +342 -0
- package/lib/core/JSONSchemaCore.js +347 -0
- package/lib/core/Locale.js +119 -0
- package/lib/core/MessageTemplate.js +89 -0
- package/lib/core/PluginManager.js +448 -0
- package/lib/core/StringExtensions.js +209 -0
- package/lib/core/Validator.js +376 -0
- package/lib/errors/ValidationError.js +191 -0
- package/lib/exporters/MarkdownExporter.js +420 -0
- package/lib/exporters/MongoDBExporter.js +162 -0
- package/lib/exporters/MySQLExporter.js +212 -0
- package/lib/exporters/PostgreSQLExporter.js +289 -0
- package/lib/exporters/index.js +24 -0
- package/lib/locales/en-US.js +65 -0
- package/lib/locales/es-ES.js +66 -0
- package/lib/locales/fr-FR.js +66 -0
- package/lib/locales/index.js +8 -0
- package/lib/locales/ja-JP.js +66 -0
- package/lib/locales/zh-CN.js +93 -0
- package/lib/utils/LRUCache.js +174 -0
- package/lib/utils/SchemaHelper.js +240 -0
- package/lib/utils/SchemaUtils.js +445 -0
- package/lib/utils/TypeConverter.js +245 -0
- package/lib/utils/index.js +13 -0
- package/lib/validators/CustomKeywords.js +203 -0
- package/lib/validators/index.js +11 -0
- package/package.json +70 -0
- package/plugins/custom-format.js +101 -0
- package/plugins/custom-validator.js +200 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 扩展自定义类型示例
|
|
3
|
+
*
|
|
4
|
+
* 演示如何在项目中扩展 SchemaIO 的功能:
|
|
5
|
+
* 1. 扩展 DSL 方法 (DslBuilder.prototype)
|
|
6
|
+
* 2. 添加自定义格式 (Validator.addFormat)
|
|
7
|
+
* 3. 添加自定义关键字 (Validator.addKeyword)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { dsl, Validator, DslBuilder } = require('../index');
|
|
11
|
+
|
|
12
|
+
// ========== 1. 扩展 DSL 方法 ==========
|
|
13
|
+
|
|
14
|
+
// 扩展 .zipCode() 方法
|
|
15
|
+
DslBuilder.prototype.zipCode = function(country = 'cn') {
|
|
16
|
+
const patterns = {
|
|
17
|
+
cn: /^[1-9]\d{5}$/,
|
|
18
|
+
us: /^\d{5}(-\d{4})?$/
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const pattern = patterns[country];
|
|
22
|
+
if (!pattern) throw new Error(`Unsupported country: ${country}`);
|
|
23
|
+
|
|
24
|
+
return this.pattern(pattern)
|
|
25
|
+
.messages({ 'pattern': `无效的邮政编码 (${country})` });
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// 扩展 .isEven() 方法 (自定义验证逻辑)
|
|
29
|
+
DslBuilder.prototype.isEven = function() {
|
|
30
|
+
return this.custom(value => {
|
|
31
|
+
if (typeof value === 'number' && value % 2 !== 0) {
|
|
32
|
+
return '必须是偶数';
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// ========== 2. 添加自定义格式 (Validator) ==========
|
|
38
|
+
|
|
39
|
+
const validator = new Validator();
|
|
40
|
+
|
|
41
|
+
// 添加 'ipv4-cidr' 格式
|
|
42
|
+
validator.ajv.addFormat('ipv4-cidr', {
|
|
43
|
+
type: 'string',
|
|
44
|
+
validate: (ip) => {
|
|
45
|
+
const parts = ip.split('/');
|
|
46
|
+
if (parts.length !== 2) return false;
|
|
47
|
+
const [addr, mask] = parts;
|
|
48
|
+
// 简单验证 (实际应使用更严谨的正则)
|
|
49
|
+
return /^\d{1,3}(\.\d{1,3}){3}$/.test(addr) &&
|
|
50
|
+
Number(mask) >= 0 && Number(mask) <= 32;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ========== 3. 使用扩展功能 ==========
|
|
55
|
+
|
|
56
|
+
const schema = dsl({
|
|
57
|
+
// 使用扩展的 DSL 方法 (注意:自定义方法需要用 dsl() 包裹,或者手动扩展 String.prototype)
|
|
58
|
+
zip: dsl('string!').zipCode('cn'),
|
|
59
|
+
count: dsl('integer!').isEven(),
|
|
60
|
+
|
|
61
|
+
// 使用自定义格式 (通过 string 类型 + format)
|
|
62
|
+
network: 'string'.format('ipv4-cidr')
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ========== 4. 测试验证 ==========
|
|
66
|
+
|
|
67
|
+
const data = {
|
|
68
|
+
zip: '123', // 无效
|
|
69
|
+
count: 3, // 无效 (奇数)
|
|
70
|
+
network: 'invalid' // 无效
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const result = validator.validate(schema, data);
|
|
74
|
+
|
|
75
|
+
console.log('验证结果:', result.valid);
|
|
76
|
+
if (!result.valid) {
|
|
77
|
+
result.errors.forEach(e => console.log(`- ${e.path}: ${e.message}`));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 输出预期:
|
|
81
|
+
// - zip: 无效的邮政编码 (cn)
|
|
82
|
+
// - count: 必须是偶数
|
|
83
|
+
// - network: must match format "ipv4-cidr"
|
|
84
|
+
|
|
85
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSL Match 语法示例 (v2.1.0)
|
|
3
|
+
*
|
|
4
|
+
* 展示如何使用 dsl.match 和 dsl.if 进行条件验证
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { dsl, validate } = require('../index');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
|
|
10
|
+
function log(msg) {
|
|
11
|
+
console.log(msg);
|
|
12
|
+
fs.appendFileSync('dsl-match-output.txt', msg + '\n');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
log('Start example...');
|
|
16
|
+
|
|
17
|
+
log('========== DSL Match 语法示例 ==========\n');
|
|
18
|
+
|
|
19
|
+
// 1. 基本用法:根据类型验证格式
|
|
20
|
+
log('✨ 1. 基本用法');
|
|
21
|
+
|
|
22
|
+
const contactSchema = dsl({
|
|
23
|
+
contactType: 'email|phone',
|
|
24
|
+
|
|
25
|
+
// 根据 contactType 的值决定 contact 的验证规则
|
|
26
|
+
contact: dsl.match('contactType', {
|
|
27
|
+
email: 'email!',
|
|
28
|
+
phone: 'string:11!',
|
|
29
|
+
_default: 'string'
|
|
30
|
+
})
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const data1 = { contactType: 'email', contact: 'test@example.com' };
|
|
34
|
+
log('Email验证: ' + (validate(contactSchema, data1).valid ? '✅ 通过' : '❌ 失败'));
|
|
35
|
+
|
|
36
|
+
const data2 = { contactType: 'phone', contact: '13800138000' };
|
|
37
|
+
log('Phone验证: ' + (validate(contactSchema, data2).valid ? '✅ 通过' : '❌ 失败'));
|
|
38
|
+
|
|
39
|
+
const data3 = { contactType: 'phone', contact: '123456789012' }; // 12位,超过11
|
|
40
|
+
log('错误验证: ' + (validate(contactSchema, data3).valid ? '✅ 通过' : '❌ 失败'));
|
|
41
|
+
log('');
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
// 2. 处理中文和特殊字符
|
|
45
|
+
log('✨ 2. 中文和特殊字符');
|
|
46
|
+
|
|
47
|
+
const discountSchema = dsl({
|
|
48
|
+
level: 'string',
|
|
49
|
+
|
|
50
|
+
discount: dsl.match('level', {
|
|
51
|
+
'普通用户': 'number:0-5',
|
|
52
|
+
'VIP-1': 'number:0-20',
|
|
53
|
+
'100': 'number:0-50'
|
|
54
|
+
})
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
log('普通用户: ' + validate(discountSchema, { level: '普通用户', discount: 3 }).valid);
|
|
58
|
+
log('VIP-1: ' + validate(discountSchema, { level: 'VIP-1', discount: 15 }).valid);
|
|
59
|
+
log('');
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
// 3. dsl.if 简单条件
|
|
63
|
+
log('✨ 3. dsl.if 简单条件');
|
|
64
|
+
|
|
65
|
+
const vipSchema = dsl({
|
|
66
|
+
isVip: 'boolean',
|
|
67
|
+
// 如果是VIP,折扣0-50,否则0-10
|
|
68
|
+
discount: dsl.if('isVip', 'number:0-50', 'number:0-10')
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
log('VIP折扣: ' + validate(vipSchema, { isVip: true, discount: 40 }).valid);
|
|
72
|
+
log('非VIP折扣: ' + validate(vipSchema, { isVip: false, discount: 40 }).valid); // false
|
|
73
|
+
log('');
|
|
74
|
+
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSL风格API示例 v2.0
|
|
3
|
+
*
|
|
4
|
+
* 演示新的DSL Builder Pattern:
|
|
5
|
+
* - 简单场景:纯字符串DSL
|
|
6
|
+
* - 复杂场景:字符串直接链式调用(✨ 无需 dsl() 包裹)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { dsl, Validator } = require('../index');
|
|
10
|
+
|
|
11
|
+
// ========== 1. 简单场景:纯字符串DSL ==========
|
|
12
|
+
|
|
13
|
+
const simpleSchema = dsl({
|
|
14
|
+
username: 'string:3-32!', // 必填字符串,长度3-32
|
|
15
|
+
email: 'email!', // 必填邮箱
|
|
16
|
+
age: 'number:18-120', // 可选数字,范围18-120
|
|
17
|
+
status: 'active|inactive|pending', // 枚举值
|
|
18
|
+
tags: 'array<string:1-20>', // 字符串数组,每项长度1-20
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
console.log('========== 简单Schema(纯DSL)==========');
|
|
22
|
+
console.log(JSON.stringify(simpleSchema, null, 2));
|
|
23
|
+
|
|
24
|
+
// ========== 2. 复杂场景:字符串直接链式调用 ✨ ==========
|
|
25
|
+
|
|
26
|
+
const complexSchema = dsl({
|
|
27
|
+
// ✨ 超简洁!字符串直接调用链式方法,无需 dsl() 包裹
|
|
28
|
+
username: 'string:3-32!'
|
|
29
|
+
.pattern(/^[a-zA-Z0-9_]+$/)
|
|
30
|
+
.messages({
|
|
31
|
+
'pattern': '只能包含字母、数字和下划线'
|
|
32
|
+
})
|
|
33
|
+
.label('用户名')
|
|
34
|
+
.description('用户登录名'),
|
|
35
|
+
|
|
36
|
+
// ✨ 邮箱:直接链式
|
|
37
|
+
email: 'email!'
|
|
38
|
+
.label('邮箱地址')
|
|
39
|
+
.messages({
|
|
40
|
+
'format': '请输入有效的邮箱地址'
|
|
41
|
+
}),
|
|
42
|
+
|
|
43
|
+
// ✨ 密码:复杂正则 + 自定义消息
|
|
44
|
+
password: 'string:8-64!'
|
|
45
|
+
.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/)
|
|
46
|
+
.messages({
|
|
47
|
+
'min': '密码长度不能少于8位',
|
|
48
|
+
'pattern': '密码必须包含大小写字母和数字'
|
|
49
|
+
})
|
|
50
|
+
.label('密码'),
|
|
51
|
+
|
|
52
|
+
// 年龄:简单约束(无需链式)
|
|
53
|
+
age: 'number:18-120',
|
|
54
|
+
|
|
55
|
+
// 角色:枚举(无需链式)
|
|
56
|
+
role: 'user|admin|moderator',
|
|
57
|
+
|
|
58
|
+
// 嵌套对象:混合使用
|
|
59
|
+
profile: {
|
|
60
|
+
bio: 'string:500', // 简单:纯DSL
|
|
61
|
+
website: 'url' // ✨ 也可以链式
|
|
62
|
+
.description('个人主页'),
|
|
63
|
+
avatar: 'url'
|
|
64
|
+
.label('头像URL')
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
console.log('\n========== 复杂Schema(DSL + 链式)==========');
|
|
69
|
+
console.log(JSON.stringify(complexSchema, null, 2));
|
|
70
|
+
|
|
71
|
+
// ========== 3. 验证数据 ==========
|
|
72
|
+
|
|
73
|
+
const { validate } = require('../index');
|
|
74
|
+
|
|
75
|
+
const testData = {
|
|
76
|
+
username: 'john_doe',
|
|
77
|
+
email: 'john@example.com',
|
|
78
|
+
password: 'Password123',
|
|
79
|
+
age: 25,
|
|
80
|
+
role: 'user',
|
|
81
|
+
profile: {
|
|
82
|
+
bio: 'Full-stack developer',
|
|
83
|
+
website: 'https://example.com',
|
|
84
|
+
avatar: 'https://example.com/avatar.jpg'
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
console.log('\n========== 验证数据 ==========');
|
|
89
|
+
const result = validate(complexSchema, testData);
|
|
90
|
+
console.log('验证结果:', result.valid ? '✅ 通过' : '❌ 失败');
|
|
91
|
+
if (!result.valid) {
|
|
92
|
+
console.log('错误:', result.errors);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ========== 4. 展示API优势 ==========
|
|
96
|
+
|
|
97
|
+
console.log('\n========== API对比 ==========');
|
|
98
|
+
|
|
99
|
+
// v1.0: 需要 dsl() 包裹
|
|
100
|
+
console.log('❌ v1.0(需要 dsl()):');
|
|
101
|
+
console.log(` email: dsl('email!')
|
|
102
|
+
.pattern(/custom/)
|
|
103
|
+
.messages({ 'pattern': '格式不正确' })
|
|
104
|
+
.label('邮箱地址')`);
|
|
105
|
+
|
|
106
|
+
// v2.0: 字符串直接链式
|
|
107
|
+
console.log('\n✅ v2.0(字符串直接链式):');
|
|
108
|
+
console.log(` email: 'email!'
|
|
109
|
+
.pattern(/custom/)
|
|
110
|
+
.messages({ 'pattern': '格式不正确' })
|
|
111
|
+
.label('邮箱地址')`);
|
|
112
|
+
|
|
113
|
+
console.log('\n✅ DSL v2.0 示例运行完成!');
|
|
114
|
+
console.log('💡 提示:简单字段用纯DSL,复杂字段直接链式调用');
|
|
115
|
+
console.log('🎉 特色:字符串直接调用方法,无需 dsl() 包裹!');
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 动态多语言配置完整示例
|
|
3
|
+
*
|
|
4
|
+
* 演示如何在各种场景下动态配置和切换语言
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { dsl, validate, Locale } = require('../index');
|
|
8
|
+
|
|
9
|
+
console.log('========== 动态多语言配置示例 ==========\n');
|
|
10
|
+
|
|
11
|
+
// ========================================
|
|
12
|
+
// 示例 1: 基本的语言切换
|
|
13
|
+
// ========================================
|
|
14
|
+
console.log('【示例 1】基本的语言切换\n');
|
|
15
|
+
|
|
16
|
+
const userSchema = dsl({
|
|
17
|
+
email: 'email!',
|
|
18
|
+
age: 'number:18-120!'
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// 中文环境
|
|
22
|
+
Locale.setLocale('zh-CN');
|
|
23
|
+
let result = validate(userSchema, { email: 'invalid', age: 10 });
|
|
24
|
+
console.log('中文错误:', result.errors.map(e => e.message).join(', '));
|
|
25
|
+
|
|
26
|
+
// 切换到英文
|
|
27
|
+
Locale.setLocale('en-US');
|
|
28
|
+
result = validate(userSchema, { email: 'invalid', age: 10 });
|
|
29
|
+
console.log('英文错误:', result.errors.map(e => e.message).join(', '));
|
|
30
|
+
|
|
31
|
+
console.log('\n' + '='.repeat(60) + '\n');
|
|
32
|
+
|
|
33
|
+
// ========================================
|
|
34
|
+
// 示例 2: 使用 dsl.config() 批量配置语言包
|
|
35
|
+
// ========================================
|
|
36
|
+
console.log('【示例 2】使用 dsl.config() 批量配置\n');
|
|
37
|
+
|
|
38
|
+
dsl.config({
|
|
39
|
+
locales: {
|
|
40
|
+
'zh-CN': {
|
|
41
|
+
'custom.tooYoung': '{{#label}}年龄太小,需满18岁',
|
|
42
|
+
'custom.emailTaken': '{{#label}}已被占用'
|
|
43
|
+
},
|
|
44
|
+
'en-US': {
|
|
45
|
+
'custom.tooYoung': '{{#label}} is too young, must be 18+',
|
|
46
|
+
'custom.emailTaken': '{{#label}} is already taken'
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// 测试自定义消息
|
|
52
|
+
Locale.setLocale('zh-CN');
|
|
53
|
+
console.log('中文自定义:', Locale.getMessage('custom.tooYoung'));
|
|
54
|
+
|
|
55
|
+
Locale.setLocale('en-US');
|
|
56
|
+
console.log('英文自定义:', Locale.getMessage('custom.tooYoung'));
|
|
57
|
+
|
|
58
|
+
console.log('\n' + '='.repeat(60) + '\n');
|
|
59
|
+
|
|
60
|
+
// ========================================
|
|
61
|
+
// 示例 3: 模拟前端语言切换
|
|
62
|
+
// ========================================
|
|
63
|
+
console.log('【示例 3】模拟前端语言切换\n');
|
|
64
|
+
|
|
65
|
+
class LanguageManager {
|
|
66
|
+
constructor() {
|
|
67
|
+
this.locale = 'en-US';
|
|
68
|
+
this.storage = {}; // 模拟 localStorage
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 获取用户偏好语言
|
|
72
|
+
getUserPreference() {
|
|
73
|
+
return this.storage.userLanguage || this.detectBrowserLanguage();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 检测浏览器语言(模拟)
|
|
77
|
+
detectBrowserLanguage() {
|
|
78
|
+
// 模拟 navigator.language
|
|
79
|
+
const browserLang = 'zh-CN';
|
|
80
|
+
const langMap = {
|
|
81
|
+
'zh': 'zh-CN',
|
|
82
|
+
'zh-CN': 'zh-CN',
|
|
83
|
+
'en': 'en-US',
|
|
84
|
+
'en-US': 'en-US'
|
|
85
|
+
};
|
|
86
|
+
return langMap[browserLang] || langMap[browserLang.split('-')[0]] || 'en-US';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 切换语言
|
|
90
|
+
changeLanguage(newLocale) {
|
|
91
|
+
console.log(`切换语言: ${this.locale} → ${newLocale}`);
|
|
92
|
+
this.locale = newLocale;
|
|
93
|
+
this.storage.userLanguage = newLocale;
|
|
94
|
+
Locale.setLocale(newLocale);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 初始化
|
|
98
|
+
init() {
|
|
99
|
+
const preferredLang = this.getUserPreference();
|
|
100
|
+
console.log(`初始化语言: ${preferredLang}`);
|
|
101
|
+
this.changeLanguage(preferredLang);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const langManager = new LanguageManager();
|
|
106
|
+
langManager.init();
|
|
107
|
+
|
|
108
|
+
// 验证(使用检测到的语言)
|
|
109
|
+
result = validate(userSchema, { email: 'test', age: 15 });
|
|
110
|
+
console.log('当前语言:', Locale.getLocale());
|
|
111
|
+
console.log('错误消息:', result.errors.map(e => e.message).join(', '));
|
|
112
|
+
|
|
113
|
+
// 用户手动切换
|
|
114
|
+
langManager.changeLanguage('en-US');
|
|
115
|
+
result = validate(userSchema, { email: 'test', age: 15 });
|
|
116
|
+
console.log('错误消息:', result.errors.map(e => e.message).join(', '));
|
|
117
|
+
|
|
118
|
+
console.log('\n' + '='.repeat(60) + '\n');
|
|
119
|
+
|
|
120
|
+
// ========================================
|
|
121
|
+
// 示例 4: 模拟服务端请求语言切换
|
|
122
|
+
// ========================================
|
|
123
|
+
console.log('【示例 4】服务端请求语言切换(安全模式)\n');
|
|
124
|
+
|
|
125
|
+
function validateWithLocale(schema, data, requestLocale) {
|
|
126
|
+
// 保存原始语言
|
|
127
|
+
const originalLocale = Locale.getLocale();
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
// 临时切换到请求的语言
|
|
131
|
+
Locale.setLocale(requestLocale);
|
|
132
|
+
|
|
133
|
+
// 执行验证
|
|
134
|
+
const result = validate(schema, data);
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
locale: requestLocale,
|
|
138
|
+
errors: result.errors
|
|
139
|
+
};
|
|
140
|
+
} finally {
|
|
141
|
+
// 恢复原始语言(重要!)
|
|
142
|
+
Locale.setLocale(originalLocale);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 模拟多个并发请求
|
|
147
|
+
console.log('请求1 (中文):');
|
|
148
|
+
const req1 = validateWithLocale(userSchema, { email: 'bad' }, 'zh-CN');
|
|
149
|
+
console.log(' 语言:', req1.locale);
|
|
150
|
+
console.log(' 错误:', req1.errors.map(e => e.message).join(', '));
|
|
151
|
+
|
|
152
|
+
console.log('\n请求2 (英文):');
|
|
153
|
+
const req2 = validateWithLocale(userSchema, { email: 'bad' }, 'en-US');
|
|
154
|
+
console.log(' 语言:', req2.locale);
|
|
155
|
+
console.log(' 错误:', req2.errors.map(e => e.message).join(', '));
|
|
156
|
+
|
|
157
|
+
console.log('\n请求3 (日文):');
|
|
158
|
+
const req3 = validateWithLocale(userSchema, { email: 'bad' }, 'ja-JP');
|
|
159
|
+
console.log(' 语言:', req3.locale);
|
|
160
|
+
console.log(' 错误:', req3.errors.map(e => e.message).join(', '));
|
|
161
|
+
|
|
162
|
+
console.log('\n全局语言未被污染:', Locale.getLocale());
|
|
163
|
+
|
|
164
|
+
console.log('\n' + '='.repeat(60) + '\n');
|
|
165
|
+
|
|
166
|
+
// ========================================
|
|
167
|
+
// 示例 5: 动态加载语言包(模拟)
|
|
168
|
+
// ========================================
|
|
169
|
+
console.log('【示例 5】动态加载语言包\n');
|
|
170
|
+
|
|
171
|
+
class LocaleLoader {
|
|
172
|
+
constructor() {
|
|
173
|
+
this.loadedLocales = new Set(['en-US']); // 默认已加载
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 模拟异步加载语言包
|
|
177
|
+
async loadLocale(locale) {
|
|
178
|
+
if (this.loadedLocales.has(locale)) {
|
|
179
|
+
console.log(`语言包 "${locale}" 已加载,跳过`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.log(`正在加载语言包 "${locale}"...`);
|
|
184
|
+
|
|
185
|
+
// 模拟网络延迟
|
|
186
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
187
|
+
|
|
188
|
+
// 模拟语言包内容
|
|
189
|
+
const mockLanguagePacks = {
|
|
190
|
+
'fr-FR': {
|
|
191
|
+
'required': '{{#label}} est requis',
|
|
192
|
+
'format.email': '{{#label}} doit être une adresse e-mail valide'
|
|
193
|
+
},
|
|
194
|
+
'de-DE': {
|
|
195
|
+
'required': '{{#label}} ist erforderlich',
|
|
196
|
+
'format.email': '{{#label}} muss eine gültige E-Mail-Adresse sein'
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const messages = mockLanguagePacks[locale];
|
|
201
|
+
if (messages) {
|
|
202
|
+
Locale.addLocale(locale, messages);
|
|
203
|
+
this.loadedLocales.add(locale);
|
|
204
|
+
console.log(`✅ 语言包 "${locale}" 加载成功`);
|
|
205
|
+
} else {
|
|
206
|
+
console.log(`❌ 语言包 "${locale}" 不存在,回退到英文`);
|
|
207
|
+
return 'en-US';
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 切换语言(带自动加载)
|
|
212
|
+
async changeLanguage(locale) {
|
|
213
|
+
await this.loadLocale(locale);
|
|
214
|
+
Locale.setLocale(locale);
|
|
215
|
+
console.log(`当前语言: ${Locale.getLocale()}\n`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const loader = new LocaleLoader();
|
|
220
|
+
|
|
221
|
+
(async () => {
|
|
222
|
+
// 加载法语
|
|
223
|
+
await loader.changeLanguage('fr-FR');
|
|
224
|
+
result = validate(userSchema, { email: 'mauvais' });
|
|
225
|
+
console.log('法语错误:', result.errors.map(e => e.message).join(', '));
|
|
226
|
+
|
|
227
|
+
console.log('');
|
|
228
|
+
|
|
229
|
+
// 加载德语
|
|
230
|
+
await loader.changeLanguage('de-DE');
|
|
231
|
+
result = validate(userSchema, { email: 'schlecht' });
|
|
232
|
+
console.log('德语错误:', result.errors.map(e => e.message).join(', '));
|
|
233
|
+
|
|
234
|
+
console.log('\n' + '='.repeat(60) + '\n');
|
|
235
|
+
|
|
236
|
+
// ========================================
|
|
237
|
+
// 示例 6: 同时获取多种语言的错误消息
|
|
238
|
+
// ========================================
|
|
239
|
+
console.log('【示例 6】同时获取多种语言的错误消息\n');
|
|
240
|
+
|
|
241
|
+
const ErrorFormatter = require('../lib/core/ErrorFormatter');
|
|
242
|
+
const Validator = require('../lib/core/Validator');
|
|
243
|
+
|
|
244
|
+
const validator = new Validator();
|
|
245
|
+
const invalidData = { email: 'invalid-email', age: 10 };
|
|
246
|
+
|
|
247
|
+
// 执行验证
|
|
248
|
+
const validationResult = validator.validate(userSchema, invalidData);
|
|
249
|
+
|
|
250
|
+
// 获取多种语言的错误消息
|
|
251
|
+
const languages = ['zh-CN', 'en-US', 'ja-JP'];
|
|
252
|
+
const multiLangErrors = {};
|
|
253
|
+
|
|
254
|
+
languages.forEach(lang => {
|
|
255
|
+
const formatter = new ErrorFormatter(lang);
|
|
256
|
+
multiLangErrors[lang] = formatter.formatDetailed(validationResult.errors);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
console.log('多语言错误消息:');
|
|
260
|
+
console.log(JSON.stringify(multiLangErrors, null, 2));
|
|
261
|
+
|
|
262
|
+
console.log('\n' + '='.repeat(60) + '\n');
|
|
263
|
+
|
|
264
|
+
// ========================================
|
|
265
|
+
// 示例 7: 完整的前端应用场景
|
|
266
|
+
// ========================================
|
|
267
|
+
console.log('【示例 7】完整的前端应用场景\n');
|
|
268
|
+
|
|
269
|
+
class FormValidator {
|
|
270
|
+
constructor() {
|
|
271
|
+
this.schema = dsl({
|
|
272
|
+
username: 'string:3-20!'.label('username'),
|
|
273
|
+
email: 'email!'.label('email'),
|
|
274
|
+
password: 'string:8-32!'.label('password')
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// 初始化语言
|
|
278
|
+
this.initLanguage();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
initLanguage() {
|
|
282
|
+
// 1. 从 localStorage 恢复
|
|
283
|
+
const savedLang = this.getStoredLanguage();
|
|
284
|
+
|
|
285
|
+
// 2. 如果没有保存,检测浏览器语言
|
|
286
|
+
const lang = savedLang || this.detectBrowserLanguage();
|
|
287
|
+
|
|
288
|
+
// 3. 设置语言
|
|
289
|
+
this.changeLanguage(lang);
|
|
290
|
+
|
|
291
|
+
console.log(`应用启动,语言初始化为: ${lang}`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
getStoredLanguage() {
|
|
295
|
+
// 模拟 localStorage.getItem
|
|
296
|
+
return null; // 首次访问
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
detectBrowserLanguage() {
|
|
300
|
+
// 模拟 navigator.language
|
|
301
|
+
return 'zh-CN';
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
changeLanguage(lang) {
|
|
305
|
+
console.log(`用户选择语言: ${lang}`);
|
|
306
|
+
Locale.setLocale(lang);
|
|
307
|
+
// 模拟 localStorage.setItem
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
validateForm(formData) {
|
|
311
|
+
const result = validate(this.schema, formData);
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
valid: result.valid,
|
|
315
|
+
errors: result.errors.map(err => ({
|
|
316
|
+
field: err.path,
|
|
317
|
+
message: err.message
|
|
318
|
+
}))
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const formValidator = new FormValidator();
|
|
324
|
+
|
|
325
|
+
// 模拟表单提交
|
|
326
|
+
console.log('\n提交表单(中文环境):');
|
|
327
|
+
let formResult = formValidator.validateForm({
|
|
328
|
+
username: 'ab',
|
|
329
|
+
email: 'bad',
|
|
330
|
+
password: '123'
|
|
331
|
+
});
|
|
332
|
+
console.log('验证结果:', JSON.stringify(formResult, null, 2));
|
|
333
|
+
|
|
334
|
+
// 用户切换语言
|
|
335
|
+
console.log('\n用户切换到英文:');
|
|
336
|
+
formValidator.changeLanguage('en-US');
|
|
337
|
+
|
|
338
|
+
console.log('\n再次提交表单(英文环境):');
|
|
339
|
+
formResult = formValidator.validateForm({
|
|
340
|
+
username: 'ab',
|
|
341
|
+
email: 'bad',
|
|
342
|
+
password: '123'
|
|
343
|
+
});
|
|
344
|
+
console.log('验证结果:', JSON.stringify(formResult, null, 2));
|
|
345
|
+
|
|
346
|
+
console.log('\n========== 所有示例完成 ==========');
|
|
347
|
+
})();
|
|
348
|
+
|