schema-dsl 2.0.0 → 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.
- package/CHANGELOG.md +130 -113
- package/LICENSE +21 -21
- package/README.md +628 -628
- package/dist/{DslBuilder-DkLaOo9Q.d.ts → DslBuilder-BIgQOAXp.d.ts} +2 -0
- package/dist/{DslBuilder-DQDN0ZxZ.d.cts → DslBuilder-CjHTucNQ.d.cts} +2 -0
- package/dist/{Validator-hFWKGxir.d.ts → Validator-CllRdrY0.d.ts} +1 -1
- package/dist/{Validator-C7GsVQOH.d.cts → Validator-D6okG9tr.d.cts} +1 -1
- package/dist/index.cjs +75 -29
- package/dist/index.d.cts +10 -4
- package/dist/index.d.ts +10 -4
- package/dist/index.js +75 -29
- package/dist/plugins/custom-format.cjs +33 -17
- package/dist/plugins/custom-format.d.cts +1 -1
- package/dist/plugins/custom-format.d.ts +1 -1
- package/dist/plugins/custom-format.js +33 -17
- package/dist/plugins/custom-type-example.cjs +33 -17
- package/dist/plugins/custom-type-example.d.cts +1 -1
- package/dist/plugins/custom-type-example.d.ts +1 -1
- package/dist/plugins/custom-type-example.js +33 -17
- package/dist/plugins/custom-validator.cjs +0 -2
- package/dist/plugins/custom-validator.d.cts +1 -1
- package/dist/plugins/custom-validator.d.ts +1 -1
- package/dist/plugins/custom-validator.js +0 -2
- package/docs/FEATURE-INDEX.md +553 -553
- package/docs/add-custom-locale.md +496 -496
- package/docs/add-keyword.md +24 -24
- package/docs/api-reference.md +1047 -1047
- package/docs/api.md +13 -13
- package/docs/best-practices-project-structure.md +417 -417
- package/docs/best-practices.md +712 -712
- package/docs/cache-manager.md +344 -344
- package/docs/compile.md +45 -45
- package/docs/conditional-api.md +1307 -1307
- package/docs/custom-extensions-guide.md +339 -339
- package/docs/design-philosophy.md +606 -606
- package/docs/doc-index.md +324 -324
- package/docs/dsl-syntax.md +714 -714
- package/docs/dynamic-locale.md +608 -608
- package/docs/enum.md +482 -482
- package/docs/error-handling.md +1975 -1975
- package/docs/export-guide.md +501 -501
- package/docs/export-limitations.md +567 -567
- package/docs/faq.md +596 -596
- package/docs/frontend-i18n-guide.md +307 -307
- package/docs/i18n-user-guide.md +487 -487
- package/docs/i18n.md +476 -476
- package/docs/index.md +48 -48
- package/docs/json-schema-basics.md +40 -40
- package/docs/label-vs-description.md +271 -271
- package/docs/markdown-exporter.md +406 -406
- package/docs/mongodb-exporter.md +302 -302
- package/docs/multi-language.md +26 -26
- package/docs/multi-type-support.md +322 -322
- package/docs/mysql-exporter.md +280 -280
- package/docs/number-operators.md +449 -449
- package/docs/optional-marker-guide.md +326 -326
- package/docs/performance-guide.md +49 -49
- package/docs/plugin-system.md +381 -381
- package/docs/plugin-type-registration.md +34 -34
- package/docs/postgresql-exporter.md +311 -311
- package/docs/public/favicon.svg +4 -4
- package/docs/quick-start.md +435 -435
- package/docs/runtime-locale-support.md +532 -532
- package/docs/schema-helper.md +345 -345
- package/docs/schema-utils-advanced-issues.md +23 -23
- package/docs/schema-utils-best-practices.md +20 -20
- package/docs/schema-utils-chaining.md +150 -150
- package/docs/schema-utils.md +524 -524
- package/docs/security-checklist.md +20 -20
- package/docs/string-extensions.md +488 -488
- package/docs/troubleshooting.md +486 -486
- package/docs/type-converter.md +310 -310
- package/docs/type-reference.md +242 -242
- package/docs/typescript-guide.md +584 -584
- package/docs/union-type-guide.md +157 -157
- package/docs/union-types.md +284 -284
- package/docs/validate-async.md +491 -491
- package/docs/validate-batch.md +49 -49
- package/docs/validate-dsl-object-support.md +578 -578
- package/docs/validate.md +506 -506
- package/docs/validation-guide.md +502 -502
- package/docs/validator.md +39 -39
- package/package.json +131 -131
- package/plugins/custom-format.cjs +8 -8
- package/plugins/custom-type-example.cjs +8 -8
- package/plugins/custom-validator.cjs +8 -8
- package/src/adapters/DslAdapter.ts +111 -111
- package/src/adapters/index.ts +1 -1
- package/src/config/constants.ts +83 -83
- package/src/config/index.ts +2 -2
- package/src/config/patterns.ts +77 -77
- package/src/core/CacheManager.ts +169 -159
- package/src/core/ConditionalBuilder.ts +382 -382
- package/src/core/ConditionalRuntime.ts +27 -27
- package/src/core/ConditionalValidator.ts +254 -254
- package/src/core/DslBuilder.ts +687 -677
- package/src/core/ErrorCodes.ts +38 -38
- package/src/core/ErrorFormatter.ts +271 -271
- package/src/core/JSONSchemaCore.ts +65 -65
- package/src/core/Locale.ts +187 -187
- package/src/core/MessageTemplate.ts +42 -42
- package/src/core/ObjectDslBuilder.ts +64 -64
- package/src/core/PluginManager.ts +326 -326
- package/src/core/StringExtensions.ts +140 -140
- package/src/core/TemplateEngine.ts +44 -44
- package/src/core/Validator.ts +448 -448
- package/src/errors/I18nError.ts +159 -159
- package/src/errors/ValidationError.ts +105 -105
- package/src/exporters/BaseExporter.ts +60 -60
- package/src/exporters/MarkdownExporter.ts +305 -305
- package/src/exporters/MongoDBExporter.ts +126 -126
- package/src/exporters/MySQLExporter.ts +156 -155
- package/src/exporters/PostgreSQLExporter.ts +222 -222
- package/src/exporters/index.ts +18 -18
- package/src/index.ts +651 -633
- package/src/locales/en-US.ts +160 -160
- package/src/locales/es-ES.ts +160 -160
- package/src/locales/fr-FR.ts +160 -160
- package/src/locales/index.ts +103 -103
- package/src/locales/ja-JP.ts +160 -160
- package/src/locales/types.ts +156 -156
- package/src/locales/zh-CN.ts +160 -160
- package/src/parser/ConstraintParser.ts +101 -101
- package/src/parser/DslParser.ts +470 -470
- package/src/parser/SchemaCompiler.ts +66 -66
- package/src/parser/TypeRegistry.ts +250 -250
- package/src/parser/index.ts +6 -6
- package/src/plugins/custom-format.ts +124 -126
- package/src/plugins/custom-type-example.ts +106 -108
- package/src/plugins/custom-validator.ts +138 -140
- package/src/types/conditional.ts +28 -28
- package/src/types/config.ts +59 -59
- package/src/types/dsl.ts +131 -131
- package/src/types/error.ts +60 -60
- package/src/types/index.ts +17 -17
- package/src/types/infer.ts +127 -127
- package/src/types/plugin.ts +58 -58
- package/src/types/safe-regex.d.ts +9 -9
- package/src/types/schema.ts +66 -66
- package/src/types/validate.ts +71 -71
- package/src/utils/SchemaHelper.ts +196 -196
- package/src/utils/SchemaUtils.ts +365 -346
- package/src/utils/TypeConverter.ts +215 -215
- package/src/utils/index.ts +10 -10
- package/src/validators/CustomKeywords.ts +477 -477
|
@@ -1,307 +1,307 @@
|
|
|
1
|
-
# 前端动态切换语言 - 最佳实践指南
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
> **场景**: 前后端分离架构,前端需要动态切换验证错误消息语言
|
|
5
|
-
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
## 📋 目录
|
|
9
|
-
|
|
10
|
-
1. [使用方法](#使用方法)
|
|
11
|
-
2. [完整示例](#完整示例)
|
|
12
|
-
3. [常见问题](#常见问题)
|
|
13
|
-
|
|
14
|
-
---
|
|
15
|
-
|
|
16
|
-
## 完整示例
|
|
17
|
-
|
|
18
|
-
### 示例1:完整的 Express 应用
|
|
19
|
-
|
|
20
|
-
```javascript
|
|
21
|
-
// server.js
|
|
22
|
-
const express = require('express');
|
|
23
|
-
const cors = require('cors');
|
|
24
|
-
const { dsl, validate } = require('schema-dsl');
|
|
25
|
-
const path = require('path');
|
|
26
|
-
|
|
27
|
-
const app = express();
|
|
28
|
-
app.use(cors());
|
|
29
|
-
app.use(express.json());
|
|
30
|
-
|
|
31
|
-
// ========== 应用启动时配置(只执行一次)==========
|
|
32
|
-
dsl.config({
|
|
33
|
-
i18n: path.join(__dirname, 'locales') // 一次性加载所有语言包
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// Schema 定义
|
|
37
|
-
const schemas = {
|
|
38
|
-
user: dsl({
|
|
39
|
-
username: 'string:3-32!',
|
|
40
|
-
email: 'email!',
|
|
41
|
-
password: 'string:8-64!',
|
|
42
|
-
age: 'number:18-120',
|
|
43
|
-
phone: 'string'
|
|
44
|
-
}),
|
|
45
|
-
|
|
46
|
-
post: dsl({
|
|
47
|
-
title: 'string:1-200!',
|
|
48
|
-
content: 'string:10-10000!',
|
|
49
|
-
tags: 'array:1-5<string:1-20>'
|
|
50
|
-
})
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
// 通用验证端点
|
|
54
|
-
app.post('/api/validate/:type', (req, res) => {
|
|
55
|
-
const { type } = req.params;
|
|
56
|
-
const schema = schemas[type];
|
|
57
|
-
|
|
58
|
-
if (!schema) {
|
|
59
|
-
return res.status(404).json({ error: 'Schema not found' });
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// 从请求头获取语言偏好
|
|
63
|
-
const locale = req.headers['accept-language']?.split(',')[0]?.trim() || 'en-US';
|
|
64
|
-
|
|
65
|
-
// 验证(直接切换语言,无需重新加载)
|
|
66
|
-
const result = validate(schema, req.body, { locale });
|
|
67
|
-
|
|
68
|
-
res.json(result);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
// 用户注册(带验证)
|
|
72
|
-
app.post('/api/register', (req, res) => {
|
|
73
|
-
// 从请求头获取语言偏好
|
|
74
|
-
const locale = req.headers['accept-language']?.split(',')[0]?.trim() || 'en-US';
|
|
75
|
-
|
|
76
|
-
// 验证数据
|
|
77
|
-
const result = validate(schemas.user, req.body, { locale });
|
|
78
|
-
|
|
79
|
-
if (!result.valid) {
|
|
80
|
-
return res.status(400).json({
|
|
81
|
-
success: false,
|
|
82
|
-
errors: result.errors // 自动使用用户偏好的语言
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// 保存用户...
|
|
87
|
-
res.json({ success: true, message: '注册成功' });
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
app.listen(3000, () => {
|
|
91
|
-
console.log('Server running on http://localhost:3000');
|
|
92
|
-
console.log('语言包已加载,支持动态切换');
|
|
93
|
-
});
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
### 示例2:Vue 3 前端
|
|
97
|
-
|
|
98
|
-
```vue
|
|
99
|
-
<template>
|
|
100
|
-
<div class="validation-form">
|
|
101
|
-
<!-- 语言切换 -->
|
|
102
|
-
<div class="language-selector">
|
|
103
|
-
<button
|
|
104
|
-
v-for="lang in languages"
|
|
105
|
-
:key="lang.code"
|
|
106
|
-
:class="{ active: locale === lang.code }"
|
|
107
|
-
@click="locale = lang.code"
|
|
108
|
-
>
|
|
109
|
-
{{ lang.label }}
|
|
110
|
-
</button>
|
|
111
|
-
</div>
|
|
112
|
-
|
|
113
|
-
<!-- 表单 -->
|
|
114
|
-
<form @submit.prevent="handleSubmit">
|
|
115
|
-
<div class="form-group">
|
|
116
|
-
<label>用户名</label>
|
|
117
|
-
<input v-model="form.username" />
|
|
118
|
-
<span v-if="getError('username')" class="error">
|
|
119
|
-
{{ getError('username') }}
|
|
120
|
-
</span>
|
|
121
|
-
</div>
|
|
122
|
-
|
|
123
|
-
<div class="form-group">
|
|
124
|
-
<label>邮箱</label>
|
|
125
|
-
<input v-model="form.email" type="email" />
|
|
126
|
-
<span v-if="getError('email')" class="error">
|
|
127
|
-
{{ getError('email') }}
|
|
128
|
-
</span>
|
|
129
|
-
</div>
|
|
130
|
-
|
|
131
|
-
<div class="form-group">
|
|
132
|
-
<label>密码</label>
|
|
133
|
-
<input v-model="form.password" type="password" />
|
|
134
|
-
<span v-if="getError('password')" class="error">
|
|
135
|
-
{{ getError('password') }}
|
|
136
|
-
</span>
|
|
137
|
-
</div>
|
|
138
|
-
|
|
139
|
-
<button type="submit">提交</button>
|
|
140
|
-
</form>
|
|
141
|
-
</div>
|
|
142
|
-
</template>
|
|
143
|
-
|
|
144
|
-
<script setup>
|
|
145
|
-
import { ref, reactive } from 'vue';
|
|
146
|
-
|
|
147
|
-
const locale = ref('zh-CN');
|
|
148
|
-
const languages = [
|
|
149
|
-
{ code: 'zh-CN', label: '中文' },
|
|
150
|
-
{ code: 'en-US', label: 'English' },
|
|
151
|
-
{ code: 'ja-JP', label: '日本語' }
|
|
152
|
-
];
|
|
153
|
-
|
|
154
|
-
const form = reactive({
|
|
155
|
-
username: '',
|
|
156
|
-
email: '',
|
|
157
|
-
password: ''
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
const errors = ref([]);
|
|
161
|
-
|
|
162
|
-
const handleSubmit = async () => {
|
|
163
|
-
try {
|
|
164
|
-
const response = await fetch('http://localhost:3000/api/validate/user', {
|
|
165
|
-
method: 'POST',
|
|
166
|
-
headers: {
|
|
167
|
-
'Content-Type': 'application/json',
|
|
168
|
-
'Accept-Language': locale.value
|
|
169
|
-
},
|
|
170
|
-
body: JSON.stringify(form)
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
const result = await response.json();
|
|
174
|
-
|
|
175
|
-
if (!result.valid) {
|
|
176
|
-
errors.value = result.errors;
|
|
177
|
-
} else {
|
|
178
|
-
alert('验证通过!');
|
|
179
|
-
errors.value = [];
|
|
180
|
-
}
|
|
181
|
-
} catch (error) {
|
|
182
|
-
console.error('验证失败:', error);
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
const getError = (field) => {
|
|
187
|
-
const error = errors.value.find(e => e.path === field);
|
|
188
|
-
return error?.message;
|
|
189
|
-
};
|
|
190
|
-
</script>
|
|
191
|
-
|
|
192
|
-
<style scoped>
|
|
193
|
-
.error {
|
|
194
|
-
color: red;
|
|
195
|
-
font-size: 0.9em;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
.language-selector button.active {
|
|
199
|
-
background: #007bff;
|
|
200
|
-
color: white;
|
|
201
|
-
}
|
|
202
|
-
</style>
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
---
|
|
206
|
-
|
|
207
|
-
## 常见问题
|
|
208
|
-
|
|
209
|
-
### Q1: 为什么不能直接使用 `Locale.setLocale()`?
|
|
210
|
-
|
|
211
|
-
**A**: 因为 Node.js 是单线程异步的,多个请求可能同时修改全局状态,导致语言混乱。
|
|
212
|
-
|
|
213
|
-
```javascript
|
|
214
|
-
// ❌ 错误示例
|
|
215
|
-
app.post('/api/validate', (req, res) => {
|
|
216
|
-
Locale.setLocale('zh-CN'); // 全局修改
|
|
217
|
-
// 如果此时另一个请求设置了 'en-US',当前请求可能得到英文消息
|
|
218
|
-
const result = validate(schema, req.body);
|
|
219
|
-
res.json(result);
|
|
220
|
-
});
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
### Q2: 每次请求创建 Validator 实例会影响性能吗?
|
|
224
|
-
|
|
225
|
-
**A**: 实例创建本身很轻量,但**仍然建议复用同一个 `Validator` 实例**。原因不是构造函数慢,而是编译缓存挂在实例上;如果每个请求都 `new Validator()`,同一份 Schema 会反复出现首次编译 miss。
|
|
226
|
-
|
|
227
|
-
```javascript
|
|
228
|
-
const validator = new Validator();
|
|
229
|
-
|
|
230
|
-
app.post('/api/validate', (req, res) => {
|
|
231
|
-
const locale = resolveLocale(req);
|
|
232
|
-
const result = validator.validate(schema, req.body, { locale });
|
|
233
|
-
res.json(result);
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
// 说明:
|
|
237
|
-
// - 共享实例:同一 schema 的后续请求可以复用编译缓存
|
|
238
|
-
// - 语言仍通过 validate(..., { locale }) 按次传入,不要写进构造函数
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
### Q3: 如何支持更多语言?
|
|
242
|
-
|
|
243
|
-
**A**: 使用 `Locale.addLocale()` 添加自定义语言包。
|
|
244
|
-
|
|
245
|
-
```javascript
|
|
246
|
-
const { Locale } = require('schema-dsl');
|
|
247
|
-
|
|
248
|
-
Locale.addLocale('de-DE', {
|
|
249
|
-
required: '{{#label}} ist erforderlich',
|
|
250
|
-
'format.email': '{{#label}} muss eine gültige E-Mail-Adresse sein'
|
|
251
|
-
// ... 更多消息
|
|
252
|
-
});
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
### Q4: 如何在前端缓存语言包?
|
|
256
|
-
|
|
257
|
-
**A**: 后端返回错误消息已经是本地化的,前端无需处理。如果需要前端验证:
|
|
258
|
-
|
|
259
|
-
```javascript
|
|
260
|
-
// 前端可以复用同一套 schema-dsl 校验规则
|
|
261
|
-
import { dsl, validate } from 'schema-dsl';
|
|
262
|
-
|
|
263
|
-
const schema = dsl({ /* ... */ });
|
|
264
|
-
const result = validate(schema, formData, {
|
|
265
|
-
locale: currentLocale
|
|
266
|
-
});
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
### Q5: 如何处理 Cookie 或 Session 中的语言?
|
|
270
|
-
|
|
271
|
-
```javascript
|
|
272
|
-
// 中间件:优先级 Header > Cookie > Session > 默认
|
|
273
|
-
app.use((req, res, next) => {
|
|
274
|
-
const locale =
|
|
275
|
-
req.headers['accept-language']?.split(',')[0]?.trim() ||
|
|
276
|
-
req.cookies?.locale ||
|
|
277
|
-
req.session?.locale ||
|
|
278
|
-
'en-US';
|
|
279
|
-
|
|
280
|
-
req.locale = locale;
|
|
281
|
-
next();
|
|
282
|
-
});
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
---
|
|
286
|
-
|
|
287
|
-
## 总结
|
|
288
|
-
|
|
289
|
-
### ✅ 推荐做法
|
|
290
|
-
|
|
291
|
-
1. **复用共享 Validator 实例**:按次通过 `validate(..., { locale })` 传入语言
|
|
292
|
-
2. **通过请求头传递语言**:符合 HTTP 标准
|
|
293
|
-
3. **使用中间件统一处理**:提高代码复用性
|
|
294
|
-
|
|
295
|
-
---
|
|
296
|
-
|
|
297
|
-
**相关文档**:
|
|
298
|
-
- [API 参考](api-reference.md)
|
|
299
|
-
- [最佳实践](best-practices.md)
|
|
300
|
-
|
|
301
|
-
---
|
|
302
|
-
|
|
303
|
-
## 对应示例文件
|
|
304
|
-
|
|
305
|
-
**示例入口**: [frontend-i18n-guide.ts](https://github.com/vextjs/schema-dsl/blob/main/examples/docs/frontend-i18n-guide.ts)
|
|
306
|
-
**说明**: 覆盖前端常见的语言优先级解析、表单提交验证,以及把错误数组整理成字段级错误映射。
|
|
307
|
-
|
|
1
|
+
# 前端动态切换语言 - 最佳实践指南
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
> **场景**: 前后端分离架构,前端需要动态切换验证错误消息语言
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 📋 目录
|
|
9
|
+
|
|
10
|
+
1. [使用方法](#使用方法)
|
|
11
|
+
2. [完整示例](#完整示例)
|
|
12
|
+
3. [常见问题](#常见问题)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 完整示例
|
|
17
|
+
|
|
18
|
+
### 示例1:完整的 Express 应用
|
|
19
|
+
|
|
20
|
+
```javascript
|
|
21
|
+
// server.js
|
|
22
|
+
const express = require('express');
|
|
23
|
+
const cors = require('cors');
|
|
24
|
+
const { dsl, validate } = require('schema-dsl');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
|
|
27
|
+
const app = express();
|
|
28
|
+
app.use(cors());
|
|
29
|
+
app.use(express.json());
|
|
30
|
+
|
|
31
|
+
// ========== 应用启动时配置(只执行一次)==========
|
|
32
|
+
dsl.config({
|
|
33
|
+
i18n: path.join(__dirname, 'locales') // 一次性加载所有语言包
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Schema 定义
|
|
37
|
+
const schemas = {
|
|
38
|
+
user: dsl({
|
|
39
|
+
username: 'string:3-32!',
|
|
40
|
+
email: 'email!',
|
|
41
|
+
password: 'string:8-64!',
|
|
42
|
+
age: 'number:18-120',
|
|
43
|
+
phone: 'string'
|
|
44
|
+
}),
|
|
45
|
+
|
|
46
|
+
post: dsl({
|
|
47
|
+
title: 'string:1-200!',
|
|
48
|
+
content: 'string:10-10000!',
|
|
49
|
+
tags: 'array:1-5<string:1-20>'
|
|
50
|
+
})
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// 通用验证端点
|
|
54
|
+
app.post('/api/validate/:type', (req, res) => {
|
|
55
|
+
const { type } = req.params;
|
|
56
|
+
const schema = schemas[type];
|
|
57
|
+
|
|
58
|
+
if (!schema) {
|
|
59
|
+
return res.status(404).json({ error: 'Schema not found' });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 从请求头获取语言偏好
|
|
63
|
+
const locale = req.headers['accept-language']?.split(',')[0]?.trim() || 'en-US';
|
|
64
|
+
|
|
65
|
+
// 验证(直接切换语言,无需重新加载)
|
|
66
|
+
const result = validate(schema, req.body, { locale });
|
|
67
|
+
|
|
68
|
+
res.json(result);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// 用户注册(带验证)
|
|
72
|
+
app.post('/api/register', (req, res) => {
|
|
73
|
+
// 从请求头获取语言偏好
|
|
74
|
+
const locale = req.headers['accept-language']?.split(',')[0]?.trim() || 'en-US';
|
|
75
|
+
|
|
76
|
+
// 验证数据
|
|
77
|
+
const result = validate(schemas.user, req.body, { locale });
|
|
78
|
+
|
|
79
|
+
if (!result.valid) {
|
|
80
|
+
return res.status(400).json({
|
|
81
|
+
success: false,
|
|
82
|
+
errors: result.errors // 自动使用用户偏好的语言
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 保存用户...
|
|
87
|
+
res.json({ success: true, message: '注册成功' });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
app.listen(3000, () => {
|
|
91
|
+
console.log('Server running on http://localhost:3000');
|
|
92
|
+
console.log('语言包已加载,支持动态切换');
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 示例2:Vue 3 前端
|
|
97
|
+
|
|
98
|
+
```vue
|
|
99
|
+
<template>
|
|
100
|
+
<div class="validation-form">
|
|
101
|
+
<!-- 语言切换 -->
|
|
102
|
+
<div class="language-selector">
|
|
103
|
+
<button
|
|
104
|
+
v-for="lang in languages"
|
|
105
|
+
:key="lang.code"
|
|
106
|
+
:class="{ active: locale === lang.code }"
|
|
107
|
+
@click="locale = lang.code"
|
|
108
|
+
>
|
|
109
|
+
{{ lang.label }}
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<!-- 表单 -->
|
|
114
|
+
<form @submit.prevent="handleSubmit">
|
|
115
|
+
<div class="form-group">
|
|
116
|
+
<label>用户名</label>
|
|
117
|
+
<input v-model="form.username" />
|
|
118
|
+
<span v-if="getError('username')" class="error">
|
|
119
|
+
{{ getError('username') }}
|
|
120
|
+
</span>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div class="form-group">
|
|
124
|
+
<label>邮箱</label>
|
|
125
|
+
<input v-model="form.email" type="email" />
|
|
126
|
+
<span v-if="getError('email')" class="error">
|
|
127
|
+
{{ getError('email') }}
|
|
128
|
+
</span>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<div class="form-group">
|
|
132
|
+
<label>密码</label>
|
|
133
|
+
<input v-model="form.password" type="password" />
|
|
134
|
+
<span v-if="getError('password')" class="error">
|
|
135
|
+
{{ getError('password') }}
|
|
136
|
+
</span>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<button type="submit">提交</button>
|
|
140
|
+
</form>
|
|
141
|
+
</div>
|
|
142
|
+
</template>
|
|
143
|
+
|
|
144
|
+
<script setup>
|
|
145
|
+
import { ref, reactive } from 'vue';
|
|
146
|
+
|
|
147
|
+
const locale = ref('zh-CN');
|
|
148
|
+
const languages = [
|
|
149
|
+
{ code: 'zh-CN', label: '中文' },
|
|
150
|
+
{ code: 'en-US', label: 'English' },
|
|
151
|
+
{ code: 'ja-JP', label: '日本語' }
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
const form = reactive({
|
|
155
|
+
username: '',
|
|
156
|
+
email: '',
|
|
157
|
+
password: ''
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const errors = ref([]);
|
|
161
|
+
|
|
162
|
+
const handleSubmit = async () => {
|
|
163
|
+
try {
|
|
164
|
+
const response = await fetch('http://localhost:3000/api/validate/user', {
|
|
165
|
+
method: 'POST',
|
|
166
|
+
headers: {
|
|
167
|
+
'Content-Type': 'application/json',
|
|
168
|
+
'Accept-Language': locale.value
|
|
169
|
+
},
|
|
170
|
+
body: JSON.stringify(form)
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const result = await response.json();
|
|
174
|
+
|
|
175
|
+
if (!result.valid) {
|
|
176
|
+
errors.value = result.errors;
|
|
177
|
+
} else {
|
|
178
|
+
alert('验证通过!');
|
|
179
|
+
errors.value = [];
|
|
180
|
+
}
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error('验证失败:', error);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const getError = (field) => {
|
|
187
|
+
const error = errors.value.find(e => e.path === field);
|
|
188
|
+
return error?.message;
|
|
189
|
+
};
|
|
190
|
+
</script>
|
|
191
|
+
|
|
192
|
+
<style scoped>
|
|
193
|
+
.error {
|
|
194
|
+
color: red;
|
|
195
|
+
font-size: 0.9em;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.language-selector button.active {
|
|
199
|
+
background: #007bff;
|
|
200
|
+
color: white;
|
|
201
|
+
}
|
|
202
|
+
</style>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## 常见问题
|
|
208
|
+
|
|
209
|
+
### Q1: 为什么不能直接使用 `Locale.setLocale()`?
|
|
210
|
+
|
|
211
|
+
**A**: 因为 Node.js 是单线程异步的,多个请求可能同时修改全局状态,导致语言混乱。
|
|
212
|
+
|
|
213
|
+
```javascript
|
|
214
|
+
// ❌ 错误示例
|
|
215
|
+
app.post('/api/validate', (req, res) => {
|
|
216
|
+
Locale.setLocale('zh-CN'); // 全局修改
|
|
217
|
+
// 如果此时另一个请求设置了 'en-US',当前请求可能得到英文消息
|
|
218
|
+
const result = validate(schema, req.body);
|
|
219
|
+
res.json(result);
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Q2: 每次请求创建 Validator 实例会影响性能吗?
|
|
224
|
+
|
|
225
|
+
**A**: 实例创建本身很轻量,但**仍然建议复用同一个 `Validator` 实例**。原因不是构造函数慢,而是编译缓存挂在实例上;如果每个请求都 `new Validator()`,同一份 Schema 会反复出现首次编译 miss。
|
|
226
|
+
|
|
227
|
+
```javascript
|
|
228
|
+
const validator = new Validator();
|
|
229
|
+
|
|
230
|
+
app.post('/api/validate', (req, res) => {
|
|
231
|
+
const locale = resolveLocale(req);
|
|
232
|
+
const result = validator.validate(schema, req.body, { locale });
|
|
233
|
+
res.json(result);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// 说明:
|
|
237
|
+
// - 共享实例:同一 schema 的后续请求可以复用编译缓存
|
|
238
|
+
// - 语言仍通过 validate(..., { locale }) 按次传入,不要写进构造函数
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Q3: 如何支持更多语言?
|
|
242
|
+
|
|
243
|
+
**A**: 使用 `Locale.addLocale()` 添加自定义语言包。
|
|
244
|
+
|
|
245
|
+
```javascript
|
|
246
|
+
const { Locale } = require('schema-dsl');
|
|
247
|
+
|
|
248
|
+
Locale.addLocale('de-DE', {
|
|
249
|
+
required: '{{#label}} ist erforderlich',
|
|
250
|
+
'format.email': '{{#label}} muss eine gültige E-Mail-Adresse sein'
|
|
251
|
+
// ... 更多消息
|
|
252
|
+
});
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Q4: 如何在前端缓存语言包?
|
|
256
|
+
|
|
257
|
+
**A**: 后端返回错误消息已经是本地化的,前端无需处理。如果需要前端验证:
|
|
258
|
+
|
|
259
|
+
```javascript
|
|
260
|
+
// 前端可以复用同一套 schema-dsl 校验规则
|
|
261
|
+
import { dsl, validate } from 'schema-dsl';
|
|
262
|
+
|
|
263
|
+
const schema = dsl({ /* ... */ });
|
|
264
|
+
const result = validate(schema, formData, {
|
|
265
|
+
locale: currentLocale
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Q5: 如何处理 Cookie 或 Session 中的语言?
|
|
270
|
+
|
|
271
|
+
```javascript
|
|
272
|
+
// 中间件:优先级 Header > Cookie > Session > 默认
|
|
273
|
+
app.use((req, res, next) => {
|
|
274
|
+
const locale =
|
|
275
|
+
req.headers['accept-language']?.split(',')[0]?.trim() ||
|
|
276
|
+
req.cookies?.locale ||
|
|
277
|
+
req.session?.locale ||
|
|
278
|
+
'en-US';
|
|
279
|
+
|
|
280
|
+
req.locale = locale;
|
|
281
|
+
next();
|
|
282
|
+
});
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## 总结
|
|
288
|
+
|
|
289
|
+
### ✅ 推荐做法
|
|
290
|
+
|
|
291
|
+
1. **复用共享 Validator 实例**:按次通过 `validate(..., { locale })` 传入语言
|
|
292
|
+
2. **通过请求头传递语言**:符合 HTTP 标准
|
|
293
|
+
3. **使用中间件统一处理**:提高代码复用性
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
**相关文档**:
|
|
298
|
+
- [API 参考](api-reference.md)
|
|
299
|
+
- [最佳实践](best-practices.md)
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## 对应示例文件
|
|
304
|
+
|
|
305
|
+
**示例入口**: [frontend-i18n-guide.ts](https://github.com/vextjs/schema-dsl/blob/main/examples/docs/frontend-i18n-guide.ts)
|
|
306
|
+
**说明**: 覆盖前端常见的语言优先级解析、表单提交验证,以及把错误数组整理成字段级错误映射。
|
|
307
|
+
|