schema-dsl 1.1.3 → 1.1.5

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.
@@ -0,0 +1,470 @@
1
+ # 运行时多语言支持 - schema-dsl
2
+
3
+ **版本**: v1.1.4+
4
+ **更新日期**: 2026-01-13
5
+
6
+ ---
7
+
8
+ ## 📋 概述
9
+
10
+ schema-dsl 的 `dsl.error` 和 `I18nError` 现在支持**运行时指定语言**,无需修改全局语言设置。
11
+
12
+ 这对于 **API 开发**特别有用,可以根据每个请求的语言偏好(如 `Accept-Language` 请求头)动态返回对应语言的错误消息。
13
+
14
+ ### 🎨 支持的模板语法(v1.1.4+)
15
+
16
+ schema-dsl 现在支持**多种模板语法格式**,提供更好的兼容性:
17
+
18
+ | 语法格式 | 示例 | 说明 | 版本 |
19
+ |---------|------|------|------|
20
+ | `{{#variable}}` | `余额{{#balance}}元` | 井号格式(现有) | v1.0.0+ |
21
+ | `{{variable}}` | `余额{{balance}}元` | 无井号格式(新增) | v1.1.4+ |
22
+ | `{variable}` | `余额{balance}元` | 单花括号(新增) | v1.1.4+ |
23
+ | 混合格式 | `{{#user}}在{date}购买{{product}}` | 可混用多种格式 | v1.1.4+ |
24
+
25
+ **示例**:
26
+ ```javascript
27
+ // 所有格式都支持
28
+ Locale.addLocale('zh-CN', {
29
+ 'msg1': '余额不足,当前{{#balance}}元', // {{#}} 格式
30
+ 'msg2': '用户{{name}}已登录', // {{}} 格式
31
+ 'msg3': '订单{orderId}已支付', // {} 格式
32
+ 'msg4': '{{#user}}在{date}购买了{{product}}' // 混合格式
33
+ });
34
+ ```
35
+
36
+ **向后兼容**:
37
+ - ✅ 现有的 `{{#variable}}` 格式完全兼容
38
+ - ✅ 所有单元测试通过(921个测试)
39
+ - ✅ 无破坏性变更
40
+
41
+ ---
42
+
43
+ ## 🎯 两种使用方式
44
+
45
+ ### 方式 1: 全局语言设置(传统方式)
46
+
47
+ ```javascript
48
+ const { dsl, Locale } = require('schema-dsl');
49
+
50
+ // 设置全局语言
51
+ Locale.setLocale('zh-CN');
52
+
53
+ // 后续所有错误都使用中文
54
+ const error1 = dsl.error.create('account.notFound');
55
+ console.log(error1.message); // "账户不存在"
56
+
57
+ const error2 = dsl.error.create('user.noPermission');
58
+ console.log(error2.message); // "没有管理员权限"
59
+ ```
60
+
61
+ **适用场景**:
62
+ - 单一语言的应用
63
+ - 不需要动态切换语言
64
+ - 简单的错误处理
65
+
66
+ ---
67
+
68
+ ### 方式 2: 运行时指定语言(推荐用于 API)⭐
69
+
70
+ ```javascript
71
+ const { dsl, Locale } = require('schema-dsl');
72
+
73
+ // 全局保持默认语言
74
+ Locale.setLocale('zh-CN');
75
+
76
+ // 每次调用时指定语言
77
+ const error1 = dsl.error.create('account.notFound', {}, 404, 'zh-CN');
78
+ console.log(error1.message); // "账户不存在"
79
+
80
+ const error2 = dsl.error.create('account.notFound', {}, 404, 'en-US');
81
+ console.log(error2.message); // "Account not found"
82
+
83
+ const error3 = dsl.error.create('account.notFound', {}, 404, 'ja-JP');
84
+ console.log(error3.message); // "account.notFound"(日语未翻译)
85
+ ```
86
+
87
+ **适用场景**:
88
+ - 多语言 API
89
+ - 根据请求头动态返回多语言错误
90
+ - 同一请求中需要多种语言
91
+ - 微服务架构中的错误传递
92
+
93
+ ---
94
+
95
+ ## 🔧 API 参数
96
+
97
+ ### dsl.error.create()
98
+
99
+ ```typescript
100
+ dsl.error.create(
101
+ code: string, // 错误代码(如 'account.notFound')
102
+ params?: object, // 参数插值(如 { balance: 50 })
103
+ statusCode?: number, // HTTP 状态码(默认 400)
104
+ locale?: string // 🆕 运行时语言(如 'en-US')
105
+ ): I18nError
106
+ ```
107
+
108
+ ### dsl.error.throw()
109
+
110
+ ```typescript
111
+ dsl.error.throw(
112
+ code: string,
113
+ params?: object,
114
+ statusCode?: number,
115
+ locale?: string // 🆕 运行时语言
116
+ ): never
117
+ ```
118
+
119
+ ### dsl.error.assert()
120
+
121
+ ```typescript
122
+ dsl.error.assert(
123
+ condition: any,
124
+ code: string,
125
+ params?: object,
126
+ statusCode?: number,
127
+ locale?: string // 🆕 运行时语言
128
+ ): void
129
+ ```
130
+
131
+ ---
132
+
133
+ ## 💡 实际应用场景
134
+
135
+ ### 场景 1: Express/Koa 中根据请求头返回多语言错误
136
+
137
+ ```javascript
138
+ const { dsl } = require('schema-dsl');
139
+
140
+ // Express 中间件
141
+ app.get('/api/account/:id', async (req, res, next) => {
142
+ try {
143
+ const account = await getAccount(req.params.id);
144
+
145
+ // 根据请求头获取语言
146
+ const locale = req.headers['accept-language'] || 'zh-CN';
147
+
148
+ // 使用运行时语言抛出错误
149
+ dsl.error.assert(account, 'account.notFound', {}, 404, locale);
150
+
151
+ res.json(account);
152
+ } catch (error) {
153
+ if (error instanceof I18nError) {
154
+ return res.status(error.statusCode).json(error.toJSON());
155
+ }
156
+ next(error);
157
+ }
158
+ });
159
+
160
+ // 请求示例
161
+ // 中文客户端: Accept-Language: zh-CN
162
+ // 响应: { "code": "account.notFound", "message": "账户不存在", ... }
163
+
164
+ // 英文客户端: Accept-Language: en-US
165
+ // 响应: { "code": "account.notFound", "message": "Account not found", ... }
166
+ ```
167
+
168
+ ---
169
+
170
+ ### 场景 2: 微服务架构中的错误传递
171
+
172
+ ```javascript
173
+ const { dsl } = require('schema-dsl');
174
+
175
+ // 服务 A: 用户服务
176
+ async function getUserService(userId, locale) {
177
+ const user = await db.findUser(userId);
178
+
179
+ // 传递 locale 到错误
180
+ dsl.error.assert(user, 'user.notFound', { userId }, 404, locale);
181
+
182
+ return user;
183
+ }
184
+
185
+ // 服务 B: API 网关
186
+ app.get('/api/users/:id', async (req, res) => {
187
+ try {
188
+ const locale = req.headers['accept-language'] || 'zh-CN';
189
+
190
+ // 调用用户服务,传递 locale
191
+ const user = await getUserService(req.params.id, locale);
192
+
193
+ res.json(user);
194
+ } catch (error) {
195
+ // 错误已经是正确的语言
196
+ res.status(error.statusCode).json(error.toJSON());
197
+ }
198
+ });
199
+ ```
200
+
201
+ ---
202
+
203
+ ### 场景 3: 同一请求中使用多种语言
204
+
205
+ ```javascript
206
+ const { dsl } = require('schema-dsl');
207
+
208
+ // 批量验证,为不同用户返回不同语言的错误
209
+ async function batchValidateAccounts(requests) {
210
+ const results = [];
211
+
212
+ for (const req of requests) {
213
+ try {
214
+ const account = await getAccount(req.accountId);
215
+
216
+ // 每个用户使用各自的语言偏好
217
+ dsl.error.assert(
218
+ account.balance >= req.amount,
219
+ 'account.insufficientBalance',
220
+ { balance: account.balance, required: req.amount },
221
+ 400,
222
+ req.locale // 每个用户的语言偏好
223
+ );
224
+
225
+ results.push({ success: true, accountId: req.accountId });
226
+ } catch (error) {
227
+ results.push({
228
+ success: false,
229
+ accountId: req.accountId,
230
+ error: error.toJSON() // 错误已经是对应用户的语言
231
+ });
232
+ }
233
+ }
234
+
235
+ return results;
236
+ }
237
+
238
+ // 调用示例
239
+ const results = await batchValidateAccounts([
240
+ { accountId: '001', amount: 100, locale: 'zh-CN' }, // 中文用户
241
+ { accountId: '002', amount: 200, locale: 'en-US' }, // 英文用户
242
+ { accountId: '003', amount: 300, locale: 'ja-JP' } // 日文用户
243
+ ]);
244
+
245
+ // 结果:每个用户收到对应语言的错误消息
246
+ ```
247
+
248
+ ---
249
+
250
+ ### 场景 4: GraphQL Resolver 中的多语言错误
251
+
252
+ ```javascript
253
+ const { dsl } = require('schema-dsl');
254
+
255
+ const resolvers = {
256
+ Query: {
257
+ account: async (_, { id }, context) => {
258
+ // 从 context 获取用户语言偏好
259
+ const locale = context.user?.locale || 'zh-CN';
260
+
261
+ const account = await getAccount(id);
262
+
263
+ // 使用运行时语言
264
+ dsl.error.assert(account, 'account.notFound', {}, 404, locale);
265
+
266
+ return account;
267
+ }
268
+ }
269
+ };
270
+ ```
271
+
272
+ ---
273
+
274
+ ## 🔍 运行时语言 vs 全局语言
275
+
276
+ ### 对比表
277
+
278
+ | 特性 | 全局语言 | 运行时语言 |
279
+ |------|---------|-----------|
280
+ | 设置方式 | `Locale.setLocale('zh-CN')` | `dsl.error.create(..., locale)` |
281
+ | 影响范围 | 全局所有错误 | 仅当前错误 |
282
+ | 是否改变全局状态 | ✅ 是 | ❌ 否 |
283
+ | 适用场景 | 单一语言应用 | 多语言 API |
284
+ | 并发安全 | ⚠️ 需注意 | ✅ 完全安全 |
285
+ | 推荐用于 | 简单应用 | API/微服务 |
286
+
287
+ ### 并发安全性
288
+
289
+ **全局语言**(不推荐用于多语言 API):
290
+
291
+ ```javascript
292
+ // ❌ 并发不安全
293
+ app.get('/api/account/:id', async (req, res) => {
294
+ // 修改全局状态
295
+ Locale.setLocale(req.headers['accept-language']);
296
+
297
+ // 如果同时有多个请求,语言会互相干扰
298
+ const error = dsl.error.create('account.notFound');
299
+ // 错误消息可能是错误的语言!
300
+ });
301
+ ```
302
+
303
+ **运行时语言**(推荐):
304
+
305
+ ```javascript
306
+ // ✅ 并发安全
307
+ app.get('/api/account/:id', async (req, res) => {
308
+ const locale = req.headers['accept-language'];
309
+
310
+ // 不修改全局状态,每个请求独立
311
+ const error = dsl.error.create('account.notFound', {}, 404, locale);
312
+ // 错误消息始终是正确的语言
313
+ });
314
+ ```
315
+
316
+ ---
317
+
318
+ ## 📊 测试验证
319
+
320
+ ### 运行时语言测试
321
+
322
+ ```javascript
323
+ const { dsl, Locale } = require('schema-dsl');
324
+
325
+ // 设置全局为中文
326
+ Locale.setLocale('zh-CN');
327
+
328
+ // 测试1: 运行时指定不同语言
329
+ const error1 = dsl.error.create('account.notFound', {}, 404, 'zh-CN');
330
+ const error2 = dsl.error.create('account.notFound', {}, 404, 'en-US');
331
+ const error3 = dsl.error.create('account.notFound', {}, 404, 'ja-JP');
332
+
333
+ console.log(error1.message); // "账户不存在"
334
+ console.log(error2.message); // "Account not found"
335
+ console.log(error3.message); // "account.notFound"
336
+
337
+ // 测试2: 验证全局语言未被改变
338
+ const currentLocale = Locale.getLocale();
339
+ console.log(currentLocale); // "zh-CN"
340
+
341
+ const error4 = dsl.error.create('user.noPermission'); // 不指定locale
342
+ console.log(error4.message); // "没有管理员权限"(使用全局语言)
343
+ ```
344
+
345
+ ### 带参数的运行时语言
346
+
347
+ ```javascript
348
+ const error1 = dsl.error.create(
349
+ 'account.insufficientBalance',
350
+ { balance: 50, required: 100 },
351
+ 400,
352
+ 'zh-CN'
353
+ );
354
+ console.log(error1.message); // "余额不足,当前余额50,需要100"
355
+
356
+ const error2 = dsl.error.create(
357
+ 'account.insufficientBalance',
358
+ { balance: 50, required: 100 },
359
+ 400,
360
+ 'en-US'
361
+ );
362
+ console.log(error2.message); // "Insufficient balance, current: 50, required: 100"
363
+ ```
364
+
365
+ ---
366
+
367
+ ## 🎯 最佳实践
368
+
369
+ ### 1. API 开发中始终使用运行时语言
370
+
371
+ ```javascript
372
+ // ✅ 推荐
373
+ app.get('/api/account/:id', async (req, res) => {
374
+ const locale = req.headers['accept-language'] || 'zh-CN';
375
+
376
+ try {
377
+ const account = await getAccount(req.params.id);
378
+ dsl.error.assert(account, 'account.notFound', {}, 404, locale);
379
+ res.json(account);
380
+ } catch (error) {
381
+ res.status(error.statusCode).json(error.toJSON());
382
+ }
383
+ });
384
+
385
+ // ❌ 不推荐
386
+ app.get('/api/account/:id', async (req, res) => {
387
+ Locale.setLocale(req.headers['accept-language']); // 并发不安全
388
+ // ...
389
+ });
390
+ ```
391
+
392
+ ### 2. 统一封装语言获取逻辑
393
+
394
+ ```javascript
395
+ // 工具函数
396
+ function getUserLocale(req) {
397
+ return req.user?.locale ||
398
+ req.headers['accept-language'] ||
399
+ 'zh-CN';
400
+ }
401
+
402
+ // 在业务代码中使用
403
+ app.get('/api/account/:id', async (req, res) => {
404
+ const locale = getUserLocale(req);
405
+
406
+ try {
407
+ const account = await getAccount(req.params.id);
408
+ dsl.error.assert(account, 'account.notFound', {}, 404, locale);
409
+ res.json(account);
410
+ } catch (error) {
411
+ res.status(error.statusCode).json(error.toJSON());
412
+ }
413
+ });
414
+ ```
415
+
416
+ ### 3. 在微服务间传递 locale
417
+
418
+ ```javascript
419
+ // 服务 A: 底层服务
420
+ async function getUser(userId, options = {}) {
421
+ const user = await db.findUser(userId);
422
+
423
+ dsl.error.assert(
424
+ user,
425
+ 'user.notFound',
426
+ { userId },
427
+ 404,
428
+ options.locale // 接收 locale 参数
429
+ );
430
+
431
+ return user;
432
+ }
433
+
434
+ // 服务 B: API 网关
435
+ app.get('/api/users/:id', async (req, res) => {
436
+ const locale = getUserLocale(req);
437
+
438
+ try {
439
+ const user = await getUser(req.params.id, { locale });
440
+ res.json(user);
441
+ } catch (error) {
442
+ res.status(error.statusCode).json(error.toJSON());
443
+ }
444
+ });
445
+ ```
446
+
447
+ ---
448
+
449
+ ## 📝 向后兼容
450
+
451
+ ✅ **完全向后兼容**
452
+
453
+ - 现有代码无需修改
454
+ - `locale` 参数为可选参数
455
+ - 不传 `locale` 时使用全局语言
456
+ - 所有单元测试通过(949/949)
457
+
458
+ ---
459
+
460
+ ## 🔗 相关文档
461
+
462
+ - [多语言配置指南](./i18n.md)
463
+ - [错误处理完整指南](./error-handling.md)
464
+ - [I18nError API 参考](./api-reference.md)
465
+
466
+ ---
467
+
468
+ **最后更新**: 2026-01-13
469
+ **作者**: schema-dsl Team
470
+