#动态多语言配置指南
更新时间: 2025-12-25
场景: 从请求头动态获取语言配置
#📑 目录
#基本原理
schema-dsl 的 Validator 支持在验证时动态指定语言,无需全局切换。
#核心方法
validator.validate(schema, data, {
locale: 'zh-CN' // 动态指定语言
});#方案1: 验证时指定语言(推荐)✅
这是最推荐的方案,无需修改全局状态,支持并发请求。
#1.1 应用启动时配置(一次性加载所有语言)
使用 dsl.config 在应用启动时一次性加载所有自定义语言包。
const { dsl, validate } = require('schema-dsl');
const path = require('path');
// ========== 应用启动时配置(只执行一次)==========
// 方式一:传入目录路径(推荐)⭐
// Node >=18:自动扫描目录下的 .js(CommonJS)、.cjs、.json、.jsonc、.json5 文件
dsl.config({
i18n: path.join(__dirname, 'locales')
});
// 方式二:直接传入对象
dsl.config({
i18n: {
'fr-FR': {
'required': '{{#label}} est requis',
'string.minLength': '{{#label}} doit contenir au moins {{#limit}} caractères'
},
'de-DE': {
'required': '{{#label}} ist erforderlich',
'string.minLength': '{{#label}} muss mindestens {{#limit}} Zeichen lang sein'
}
}
});
// 说明:
// - 只在应用启动时执行一次
// - 自动与系统内置语言包合并(用户自定义的优先)
// - 运行时无需重新加载,直接切换#1.2 运行时直接切换语言(无需重新加载)
const { dsl, validate } = require('schema-dsl');
// 定义 Schema
const schema = dsl({
username: 'string:3-32!',
email: 'email!'
});
// 测试数据
const data = { username: 'ab', email: 'invalid' };
// ========== 运行时直接切换语言 ==========
// 使用中文
const result1 = validate(schema, data, { locale: 'zh-CN' });
// 错误: "username长度不能少于3个字符"
// 使用法语
const result2 = validate(schema, data, { locale: 'fr-FR' });
// 错误: "username doit contenir au moins 3 caractères"
// 使用德语
const result3 = validate(schema, data, { locale: 'de-DE' });
// 错误: "username muss mindestens 3 Zeichen lang sein"
// 说明:
// - 无需重新加载语言包
// - 每次验证可以使用不同语言
// - 支持高并发(无全局状态修改)#1.3 从请求头获取语言(实际应用场景)
const express = require('express');
const { dsl, validate } = require('schema-dsl');
const path = require('path');
const app = express();
// ========== 应用启动时配置(只执行一次)==========
dsl.config({
i18n: path.join(__dirname, 'locales')
});
// 定义 Schema
const userSchema = dsl({
username: 'string:3-32!',
email: 'email!',
password: 'string:8-32!'
});
// ========== Express 路由 ==========
app.post('/api/user/register', (req, res) => {
// 从请求头获取语言偏好
const locale = parseAcceptLanguage(req.headers['accept-language']);
// 验证数据(直接切换语言,无需重新加载)
const result = validate(userSchema, req.body, { locale });
if (!result.valid) {
return res.status(400).json({
errors: result.errors // 自动使用用户偏好的语言
});
}
// 处理成功...
res.json({ message: 'User registered successfully' });
});#1.3 解析 Accept-Language 头
/**
* 解析 Accept-Language 头
* @param {string} acceptLanguage - Accept-Language 头的值
* @returns {string} 语言代码
*/
function parseAcceptLanguage(acceptLanguage) {
if (!acceptLanguage) return 'en-US';
// Accept-Language 格式: zh-CN,zh;q=0.9,en;q=0.8
const languages = acceptLanguage.split(',').map(lang => {
const [code, qValue] = lang.trim().split(';');
const q = qValue ? parseFloat(qValue.split('=')[1]) : 1.0;
return { code: code.trim(), q };
});
// 按权重排序
languages.sort((a, b) => b.q - a.q);
// 映射到支持的语言
const supportedLocales = ['zh-CN', 'en-US', 'ja-JP'];
for (const lang of languages) {
const matched = supportedLocales.find(locale =>
locale.toLowerCase() === lang.code.toLowerCase() ||
locale.split('-')[0] === lang.code.split('-')[0]
);
if (matched) return matched;
}
return 'en-US'; // 默认语言
}
// 使用
app.post('/api/user/register', (req, res) => {
const locale = parseAcceptLanguage(req.headers['accept-language']);
const result = validator.validate(schema, req.body, { locale });
// ...
});#方案2: 临时切换语言
适用于少数场景。
#2.1 使用闭包保存原语言
function validateWithLocale(validator, schema, data, locale) {
const originalLocale = Locale.getLocale();
try {
Locale.setLocale(locale);
return validator.validate(schema, data);
} finally {
Locale.setLocale(originalLocale); // 恢复原语言
}
}
// 使用
app.post('/api/user/register', (req, res) => {
const locale = parseAcceptLanguage(req.headers['accept-language']);
const result = validateWithLocale(validator, schema, req.body, locale);
// ...
});#方案3: Express/Koa 中间件
封装为中间件,自动处理语言切换。
#3.1 Express 中间件 (推荐)
通过中间件一次性配置,后续业务代码无需关心语言参数。
const { Validator } = require('schema-dsl');
const validator = new Validator();
const schemaIoMiddleware = (req, res, next) => {
// 1. 自动获取语言
const lang = req.headers['accept-language']?.split(',')[0]?.trim() || 'en-US';
// 简单匹配逻辑 (实际可使用 accept-language-parser)
const locale = lang.includes('zh') ? 'zh-CN' :
lang.includes('ja') ? 'ja-JP' :
lang.includes('es') ? 'es-ES' :
lang.includes('fr') ? 'fr-FR' : 'en-US';
// 2. 挂载绑定了语言的验证方法
req.validate = (schema, data) => {
return validator.validate(schema, data, { locale });
};
next();
};
app.use(schemaIoMiddleware);
// 业务中使用
app.post('/users', (req, res) => {
// 直接调用,自动使用中间件解析的语言
const result = req.validate(userSchema, req.body);
if (!result.valid) {
return res.status(400).json({ errors: result.errors });
}
// ...
});完整示例请参考 dynamic-locale.ts。
#3.2 Koa 中间件
const { Locale, Validator } = require('schema-dsl');
const validator = new Validator();
/**
* Koa 语言中间件
*/
function localeMiddleware() {
return async (ctx, next) => {
// 解析语言
const locale = parseAcceptLanguage(ctx.headers['accept-language']);
// 保存到上下文
ctx.locale = locale;
// 复用共享 Validator,避免每个请求都重新建立实例和缓存
ctx.validate = function(schema, data) {
return validator.validate(schema, data, { locale: ctx.locale });
};
await next();
};
}
// 应用中间件
app.use(localeMiddleware());
// 使用
router.post('/api/user/register', async (ctx) => {
// 自动使用请求的语言
const result = ctx.validate(userSchema, ctx.request.body);
if (!result.valid) {
ctx.status = 400;
ctx.body = { errors: result.errors };
return;
}
// ...
});#完整示例
#Express 完整示例
const express = require('express');
const { dsl, Validator, Locale } = require('schema-dsl');
const app = express();
app.use(express.json());
// ========== 1. 初始化语言包 ==========
Locale.addLocale('zh-CN', {
'required': '{{#label}}不能为空',
'min': '{{#label}}至少{{#limit}}个字符',
'max': '{{#label}}最多{{#limit}}个字符',
'pattern': '{{#label}}格式不正确',
'format': '请输入有效的{{#label}}'
});
Locale.addLocale('en-US', {
'required': '{{#label}} is required',
'min': '{{#label}} must be at least {{#limit}} characters',
'max': '{{#label}} must be at most {{#limit}} characters',
'pattern': '{{#label}} format is invalid',
'format': 'Please enter a valid {{#label}}'
});
// ========== 2. 工具函数 ==========
function parseAcceptLanguage(acceptLanguage) {
if (!acceptLanguage) return 'en-US';
const languages = acceptLanguage.split(',').map(lang => {
const [code, qValue] = lang.trim().split(';');
const q = qValue ? parseFloat(qValue.split('=')[1]) : 1.0;
return { code: code.trim(), q };
});
languages.sort((a, b) => b.q - a.q);
const supportedLocales = ['zh-CN', 'en-US'];
for (const lang of languages) {
const matched = supportedLocales.find(locale =>
locale.toLowerCase() === lang.code.toLowerCase()
);
if (matched) return matched;
}
return 'en-US';
}
// ========== 3. 中间件 ==========
const validator = new Validator();
function localeMiddleware(req, res, next) {
req.locale = parseAcceptLanguage(req.headers['accept-language']);
req.validate = function(schema, data) {
return validator.validate(schema, data, { locale: req.locale });
};
next();
}
app.use(localeMiddleware);
// ========== 4. 定义Schema ==========
const userSchema = dsl({
username: 'string:3-32!'.label('用户名'),
email: 'email!'.label('邮箱地址'),
password: 'string:8-64!'
.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/)
.label('密码')
.messages({
'pattern': '密码必须包含大小写字母和数字'
}),
age: 'number:18-120'.label('年龄')
});
// ========== 5. API 路由 ==========
app.post('/api/user/register', (req, res) => {
// 验证数据(自动使用请求语言)
const result = req.validate(userSchema, req.body);
if (!result.valid) {
return res.status(400).json({
success: false,
errors: result.errors,
locale: req.locale // 返回使用的语言
});
}
// 处理注册逻辑
res.json({
success: true,
message: req.locale === 'zh-CN' ? '注册成功' : 'Registration successful'
});
});
// ========== 6. 测试 ==========
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
console.log('\n测试命令:');
console.log('# 中文错误消息');
console.log('curl -X POST http://localhost:3000/api/user/register \\');
console.log(' -H "Content-Type: application/json" \\');
console.log(' -H "Accept-Language: zh-CN" \\');
console.log(' -d \'{"username":"ab"}\'');
console.log('\n# 英文错误消息');
console.log('curl -X POST http://localhost:3000/api/user/register \\');
console.log(' -H "Content-Type: application/json" \\');
console.log(' -H "Accept-Language: en-US" \\');
console.log(' -d \'{"username":"ab"}\'');
});#最佳实践
#1. 语言包集中管理
// locales/index.js
module.exports = {
'zh-CN': require('./zh-CN.json'),
'en-US': require('./en-US.json'),
'ja-JP': require('./ja-JP.json')
};
// locales/zh-CN.json
{
"required": "{{#label}}不能为空",
"min": "{{#label}}至少{{#limit}}个字符",
"max": "{{#label}}最多{{#limit}}个字符",
"pattern": "{{#label}}格式不正确",
"format": "请输入有效的{{#label}}"
}
// 初始化
const locales = require('./locales');
Object.entries(locales).forEach(([locale, messages]) => {
Locale.addLocale(locale, messages);
});#2. 支持的语言列表
const SUPPORTED_LOCALES = ['zh-CN', 'en-US', 'ja-JP'];
function getSupportedLocale(requestLocale) {
return SUPPORTED_LOCALES.includes(requestLocale)
? requestLocale
: 'en-US';
}#3. 缓存验证器
// 为每个语言缓存验证器
const validators = {
'zh-CN': new Validator(),
'en-US': new Validator(),
'ja-JP': new Validator()
};
function getValidator(locale) {
return validators[locale] || validators['en-US'];
}
// 使用
const result = getValidator(req.locale).validate(
schema,
data,
{ locale: req.locale }
);#4. 错误响应标准化
function sendValidationError(res, result, locale) {
res.status(400).json({
success: false,
code: 'VALIDATION_ERROR',
message: locale === 'zh-CN' ? '验证失败' : 'Validation failed',
errors: result.errors,
locale: locale
});
}
// 使用
if (!result.valid) {
return sendValidationError(res, result, req.locale);
}#方案对比
| 方案 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|
| 方案1: 验证时指定 | ✅ 无竞态问题 ✅ 支持并发 ✅ 代码简洁 | - | ⭐⭐⭐⭐⭐ |
| 方案2: 临时切换 | ✅ 实现简单 | ⚠️ 并发竞态问题 | ⭐⭐⭐ |
| 方案3: 中间件 | ✅ 自动化 ✅ 统一管理 ✅ 可复用共享 Validator 缓存 | - | ⭐⭐⭐⭐⭐ |
推荐: 方案1 + 方案3(中间件封装)
#常见问题
#Q1: 如何处理不支持的语言?
A: 回退到默认语言
不要直接把原始 Accept-Language 头透传给 locale;浏览器常见值会带 q= 权重,应该先解析再回退。
function parseAcceptLanguage(acceptLanguage) {
// ...解析逻辑
return supportedLocale || 'en-US'; // 默认英文
}#Q2: 是否支持动态加载语言包?
A: 支持
async function loadLocale(locale) {
if (!Locale.getAvailableLocales().includes(locale)) {
const messages = await import(`./locales/${locale}.json`);
Locale.addLocale(locale, messages);
}
}
// 使用
app.use(async (req, res, next) => {
await loadLocale(req.locale);
next();
});#Q3: 如何自定义某些字段的错误消息?
A: 使用 .messages() 方法
const schema = dsl({
password: 'string:8-64!'
.label('密码')
.messages({
'required': req.locale === 'zh-CN'
? '请输入密码'
: 'Please enter password',
'min': req.locale === 'zh-CN'
? '密码太短了,至少8个字符'
: 'Password is too short, at least 8 characters'
})
});#相关文档
#对应示例文件
示例入口: dynamic-locale.ts
说明: 覆盖 Accept-Language 解析、运行时 locale 选择,以及同一 schema 在不同请求语言下的验证入口。
最后更新: 2026-05-08
作者: schema-dsl Team