添加自定义语言包指南

版本: v2.0.0-beta.1
最后更新: 2026-04-30


📖 概述

本指南将教你如何为 schema-dsl 添加自定义语言包或扩展现有语言。

Node.js 要求>=18.0.0

目录加载(Node >=18)默认支持的语言文件格式.js(CommonJS)、.cjs.json.jsonc.json5
推荐:如果你的应用是 type: module / ESM 项目,优先使用 .cjs.json.jsonc.json5


🏗️ 多人协作:子目录拆分语言包(v1.2.3 新增)⭐

适用场景:多人/多模块开发,避免所有语言 key 堆在同一文件产生 Git 冲突和 code 码冲突。

目录结构

project/
├── locales/
   ├── core/               # 公共 code 段:1000-1999(框架层维护)
   ├── zh-CN.cjs
   └── en-US.jsonc
   ├── account/            # 账户模块 code 段:10000-10999(开发者A)
   ├── zh-CN.cjs
   └── en-US.jsonc
   ├── order/              # 订单模块 code 段:20000-20999(开发者B)
   ├── zh-CN.json5
   └── en-US.json5
   └── payment/            # 支付模块 code 段:30000-30999(开发者C)
       ├── zh-CN.cjs
       └── en-US.cjs
└── app.js

每个模块独立维护自己的语言文件

// locales/account/zh-CN.cjs — 开发者A 独立维护,互不干扰
module.exports = {
  'account.notFound': { code: 10001, message: '账户不存在' },
  'account.locked':   { code: 10002, message: '账户已锁定' },
};

// locales/order/zh-CN.json5 — 开发者B 独立维护
const orderZhCN = {
  'order.notFound': { code: 20001, message: '订单不存在' },
  'order.notPaid':  { code: 20002, message: '订单未支付' },
}

应用启动:一行配置,自动递归合并

// app.js
const { dsl, validate } = require('schema-dsl');
const path = require('path');

// 自动递归扫描 locales/ 下所有子目录,同语言文件合并为一个完整语言包
dsl.config({
  i18n: path.join(__dirname, 'locales')
});
  • 子目录名(account/order/)仅作为模块组织层,不影响最终语言 key 命名
  • 加载顺序:按文件系统字母序递归扫描
  • 同语言 key 出现重复时:默认打 WARN 日志,可开启严格模式阻断启动

严格模式:key 冲突时阻断启动(推荐 CI 环境)

dsl.config({
  i18n: path.join(__dirname, 'locales'),
  strict: true   // 同名 key 冲突时直接抛 Error,防止静默覆盖
});

// 冲突示例输出:
// Error: [schema-dsl] i18n key 冲突 in locale 'zh-CN'
//   冲突 key: account.notFound
//   来源文件: /project/locales/account/zh-CN.cjs

Code 段划分建议

多人开发时建议在项目根目录维护一份 locales/CODE-SEGMENTS.md,约定各模块的 code 号段:

模块code 范围负责人
core(公共)1000–1999框架组
account10000–10999开发者A
order20000–20999开发者B
payment30000–30999开发者C

CODE-SEGMENTS.md / CODE-SEGMENTS.js 等非语言文件会被自动跳过,无需担心被误加载。


🚀 快速开始

推荐方式:配置语言包目录(一次性加载所有语言)⭐

正确的使用方式:在应用启动时一次性加载所有语言包,运行时直接切换。

第1步:创建语言包文件

第1步:创建语言包文件

# 项目结构
my-project/
├── locales/              # 语言包目录
   ├── zh-CN.cjs        # 中文(CommonJS / ESM 项目都稳定)
   ├── en-US.jsonc      # 英文(带注释 / 末尾逗号)
   └── pt-BR.json5      # 葡萄牙语(JSON5 风格)
└── app.js

第2步:定义语言包(locales/pt-BR.json5

module.exports = {
  // 通用验证错误
  'required': '{{#label}} é obrigatório',
  'type': '{{#label}} deve ser do tipo {{#expected}}',
  'min': '{{#label}} deve ter pelo menos {{#limit}} caracteres',
  'max': '{{#label}} não pode exceder {{#limit}} caracteres',
  'length': '{{#label}} deve ter exatamente {{#limit}} caracteres',
  'pattern': '{{#label}} formato inválido',
  'enum': '{{#label}} deve ser um dos seguintes valores: {{#allowed}}',
  
  // 格式验证
  'format.email': '{{#label}} deve ser um e-mail válido',
  'format.url': '{{#label}} deve ser uma URL válida',
  'format.uuid': '{{#label}} deve ser um UUID válido',
  'format.date': '{{#label}} deve ser uma data válida (YYYY-MM-DD)',
  'format.datetime': '{{#label}} deve ser uma data/hora válida (ISO 8601)',
  
  // 字符串验证
  'string.minLength': '{{#label}} deve ter pelo menos {{#limit}} caracteres',
  'string.maxLength': '{{#label}} não pode exceder {{#limit}} caracteres',
  'string.pattern': '{{#label}} formato inválido',
  'string.alphanum': '{{#label}} deve conter apenas letras e números',
  
  // 数字验证
  'number.base': '{{#label}} deve ser um número',
  'number.min': '{{#label}} não pode ser menor que {{#limit}}',
  'number.max': '{{#label}} não pode ser maior que {{#limit}}',
  'number.integer': '{{#label}} deve ser um inteiro',
  'number.positive': '{{#label}} deve ser um número positivo',
  'number.negative': '{{#label}} deve ser um número negativo',
  
  // 布尔验证
  'boolean.base': '{{#label}} deve ser um booleano',
  
  // 对象验证
  'object.base': '{{#label}} deve ser um objeto',
  
  // 数组验证
  'array.base': '{{#label}} deve ser um array',
  'array.min': '{{#label}} deve ter pelo menos {{#limit}} itens',
  'array.max': '{{#label}} não pode ter mais de {{#limit}} itens',
  
  // 日期验证
  'date.base': '{{#label}} deve ser uma data válida',
  'date.min': '{{#label}} não pode ser anterior a {{#limit}}',
  'date.max': '{{#label}} não pode ser posterior a {{#limit}}',
  
  // 自定义模式
  'pattern.phone.cn': 'Número de telefone inválido',
  'pattern.idCard.cn': 'Número de identidade inválido',
  'pattern.creditCard': 'Número de cartão de crédito inválido',
  'pattern.objectId': 'ObjectId inválido',
  'pattern.hexColor': 'Código de cor hexadecimal inválido',
  'pattern.macAddress': 'Endereço MAC inválido',
  'pattern.cron': 'Expressão Cron inválida',
  'pattern.slug': 'Slug deve conter apenas letras minúsculas, números e hífens'
};

第3步:应用启动时一次性加载所有语言

const { dsl, validate } = require('schema-dsl');
const path = require('path');

// ========== 应用启动时配置(只执行一次)==========
dsl.config({
  i18n: path.join(__dirname, 'locales')  // 自动加载目录下所有语言文件
});

// 说明:
// 1. 自动扫描 locales/ 目录下的 `.js`(CommonJS)、`.cjs`、`.json`、`.jsonc`、`.json5`
// 2. 从文件名提取语言代码(如 pt-BR.cjs → pt-BR)
// 3. 自动加载并注册所有语言包
// 4. 用户自定义的语言包会与系统默认语言包合并,用户的优先

// ========== 运行时直接切换语言(无需重新加载)==========
const schema = dsl({ username: 'string:3-32!' });

// 使用葡萄牙语
const result1 = validate(schema, { username: 'ab' }, { locale: 'pt-BR' });
// 错误消息: "username deve ter pelo menos 3 caracteres"

// 使用中文
const result2 = validate(schema, { username: 'ab' }, { locale: 'zh-CN' });
// 错误消息: "username长度不能少于3个字符"

// 使用英文
const result3 = validate(schema, { username: 'ab' }, { locale: 'en-US' });
// 错误消息: "username length must be at least 3"

语言包合并策略

// 系统内置的 zh-CN 语言包
const systemZhCN = {
  'required': '{{#label}}是必填项',
  'string.minLength': '{{#label}}长度不能少于{{#limit}}个字符'
};

// 用户自定义的 locales/zh-CN.cjs
const userZhCN = {
  'required': '{{#label}}必须填写',  // 覆盖系统默认
  'custom.myError': '自定义错误'     // 新增自定义消息
};

// 最终合并结果(深度合并)
const finalZhCN = {
  'required': '{{#label}}必须填写',                      // ✅ 用户的优先
  'string.minLength': '{{#label}}长度不能少于{{#limit}}个字符', // 保留系统默认
  'custom.myError': '自定义错误'                         // 新增自定义消息
};

方式2:直接传入对象(适合动态配置)

const { dsl } = require('schema-dsl');

// 应用启动时配置
dsl.config({
  i18n: {
    'pt-BR': require('./locales/pt-BR'),
    'de-DE': require('./locales/de-DE'),
    'ko-KR': require('./locales/ko-KR')
  }
});

// 运行时直接切换
validate(schema, data, { locale: 'pt-BR' });
validate(schema, data, { locale: 'de-DE' });

⚠️ 错误示例(不推荐)

❌ 错误:运行时单个加载语言包

const { Locale } = require('schema-dsl');

// ❌ 不推荐:在每次验证前动态加载
function validateUser(data, locale) {
  if (locale === 'pt-BR') {
    Locale.addLocale('pt-BR', require('./locales/pt-BR'));  // 每次都加载,性能差
  }
  return validate(schema, data, { locale });
}
// ✅ 正确:应用启动时一次性加载
// app.js 启动入口
dsl.config({ i18n: './locales' });  // 只加载一次

// 运行时直接切换,无需重新加载
function validateUser(data, locale) {
  return validate(schema, data, { locale });  // ✅ 直接切换,性能好
}

为什么推荐"首次加载,运行时切换"?

方式加载次数性能内存推荐度
首次加载所有1次⭐⭐⭐⭐⭐ 极快✅ 强烈推荐
运行时单个加载N次⭐⭐ 慢❌ 不推荐

🎯 完整示例

// ========== app.js(应用启动入口)==========
const express = require('express');
const { dsl, validate } = require('schema-dsl');
const path = require('path');

// 应用启动时一次性加载所有语言包
dsl.config({
  i18n: path.join(__dirname, 'locales')
});

const app = express();

// ========== routes/user.js(业务路由)==========
const userSchema = dsl({
  username: 'string:3-32!',
  email: 'email!',
  age: 'number:18-120'
});

app.post('/api/users', (req, res) => {
  // 从请求头获取用户语言偏好
  const locale = req.headers['accept-language']?.split(',')[0]?.trim() || 'en-US';
  
  // 验证(直接切换语言,无需加载)
  const result = validate(userSchema, req.body, { locale });
  
  if (!result.valid) {
    return res.status(400).json({
      errors: result.errors  // 自动使用用户偏好的语言
    });
  }
  
  // 处理请求...
});

📋 完整的消息键列表

通用键

键名说明示例
required必填字段{{#label}} é obrigatório
type类型错误{{#label}} deve ser do tipo {{#expected}}
min最小长度(通用){{#label}} deve ter pelo menos {{#limit}} caracteres
max最大长度(通用){{#label}} não pode exceder {{#limit}} caracteres
length精确长度{{#label}} deve ter exatamente {{#limit}} caracteres
pattern模式匹配{{#label}} formato inválido
enum枚举值{{#label}} deve ser um dos seguintes: {{#allowed}}

字符串验证键

键名说明可用变量
string.minLength最小长度{{#label}}, {{#limit}}
string.maxLength最大长度{{#label}}, {{#limit}}
string.length精确长度{{#label}}, {{#limit}}
string.pattern模式匹配{{#label}}
string.alphanum字母数字{{#label}}
string.enum枚举值{{#label}}, {{#valids}}

数字验证键

键名说明可用变量
number.base类型错误{{#label}}
number.min最小值{{#label}}, {{#limit}}
number.max最大值{{#label}}, {{#limit}}
number.integer整数{{#label}}
number.positive正数{{#label}}
number.negative负数{{#label}}
number.precision小数精度{{#label}}, {{#limit}}
number.port端口号{{#label}}

格式验证键

键名说明
format.email邮箱格式
format.urlURL格式
format.uuidUUID格式
format.date日期格式
format.datetime日期时间格式
format.time时间格式
format.ipv4IPv4地址
format.ipv6IPv6地址
format.binaryBase64编码

自定义模式键

键名说明
pattern.phone.cn中国手机号
pattern.phone.us美国电话号
pattern.idCard.cn中国身份证
pattern.creditCard信用卡号
pattern.objectIdMongoDB ObjectId
pattern.hexColor十六进制颜色
pattern.macAddressMAC地址
pattern.cronCron表达式
pattern.slugURL别名
pattern.username用户名
pattern.password.weak弱密码
pattern.password.medium中等密码
pattern.password.strong强密码
pattern.password.veryStrong超强密码

🎨 模板变量

所有错误消息支持以下模板变量:

变量说明使用示例
{{#label}}字段标签{{#label}} é obrigatório
{{#limit}}限制值(长度/大小)deve ter pelo menos {{#limit}} caracteres
{{#allowed}}允许的值列表deve ser um dos seguintes: {{#allowed}}
{{#expected}}期望的类型deve ser do tipo {{#expected}}
{{#valids}}有效值列表(数组)deve ser: {{#valids}}
{{#path}}字段路径Erro no campo {{#path}}

📚 参考内置语言包

你可以参考内置的语言包作为模板:

const { Locale } = require('schema-dsl');

// 查看中文语言包
const zhCN = Locale.getMessages('zh-CN');
console.log(zhCN);

// 查看英文语言包
const enUS = Locale.getMessages('en-US');
console.log(enUS);

或者直接查看源码:

  • 中文:src/locales/zh-CN.ts
  • 英文:src/locales/en-US.ts
  • 日语:src/locales/ja-JP.ts
  • 西班牙语:src/locales/es-ES.ts
  • 法语:src/locales/fr-FR.ts

✅ 最佳实践

  1. 完整性:确保翻译所有常用的错误消息键
  2. 一致性:保持错误消息风格统一
  3. 模板变量:正确使用 {{#label}}{{#limit}} 等变量
  4. 测试:添加语言包后进行测试,确保所有消息正确显示
  5. 文档:为自定义语言包编写使用说明

🤝 贡献语言包

如果你为 schema-dsl 添加了新语言包,欢迎提交 Pull Request:

  1. Fork 项目
  2. src/locales/ 目录创建新语言文件(如 pt-BR.ts
  3. 完整翻译所有消息键
  4. src/locales/index.ts 中注册新语言
  5. 添加测试用例(在 test/unit/locales/ 目录)
  6. 提交 Pull Request

📞 支持

如果你在添加语言包时遇到问题:


对应示例文件

示例入口: add-custom-locale.ts
说明: 覆盖 Locale.addLocale() 注册新语言、读取消息文本,以及在自定义 locale 下执行验证的最小工作流。