Guide to adding custom language packs
📖 Overview
This guide will teach you how to add a custom language pack or extend an existing language for schema-dsl.
Node.js Requirements: >=18.0.0
Language file formats supported by directory loading (Node >=18) by default: .js (CommonJS), .cjs, .json, .jsonc, .json5.
Recommendation: If your application is a type: module / ESM project, give priority to using .cjs, .json, .jsonc, .json5.
Trusted directory note: .js / .cjs locale files are executed by Node. Keep them for trusted application-owned locale directories, or configure codeLocaleFiles: 'deny' to load only .json, .jsonc, and .json5.
🏗️ Multi-person collaboration: subdirectory splitting language packs (new in v1.2.3)⭐
Applicable scenarios: Multi-person/multi-module development to avoid Git conflicts and code conflicts caused by stacking all language keys in the same file.
Directory structure
project/
├── locales/
│ ├── core/ # Public code segment: 1000-1999 (framework layer maintenance)
│ │ ├── zh-CN.cjs
│ │ └── en-US.jsonc
│ ├── account/ # Account module code segment: 10000-10999 (Developer A)
│ │ ├── zh-CN.cjs
│ │ └── en-US.jsonc
│ ├── order/ # Order module code section: 20000-20999 (Developer B)
│ │ ├── zh-CN.json5
│ │ └── en-US.json5
│ └── payment/ # Payment module code segment: 30000-30999 (Developer C)
│ ├── zh-CN.cjs
│ └── en-US.cjs
└── app.js
Each module maintains its own language files independently
// locales/account/zh-CN.cjs — Developer A maintains independently without interfering with each other
module.exports = {
'account.notFound': { code: 10001, message: 'Account does not exist' },
'account.locked': { code: 10002, message: 'Account has been locked' },
};
// locales/order/zh-CN.json5 — independently maintained by developer B
const orderZhCN = {
'order.notFound': { code: 20001, message: 'The order does not exist' },
'order.notPaid': { code: 20002, message: 'Order not paid' },
}
Application startup: one line of configuration, automatic recursive merging
// app.js
import { s, validate } from 'schema-dsl/pure';
import path from 'path';
// Automatically recursively scan all subdirectories under locales/ and merge the same language files into a complete language package
s.config({
i18n: path.join(__dirname, 'locales')
});
If the directory is data-only or not a trusted code source, disable code locale files while keeping JSON-family files:
s.config({
i18n: path.join(__dirname, 'locales'),
codeLocaleFiles: 'deny'
});
- The subdirectory names (
account/, order/) are only used as the module organization layer and do not affect the final language key naming
- Load order: scan recursively in file system alphabetical order
- When the same language key is repeated:
WARN log will be logged by default, and strict mode can be turned on to block startup.
Strict mode: Block startup when key conflicts (recommended for CI environment)
s.config({
i18n: path.join(__dirname, 'locales'),
strict: true // When keys with the same name conflict, an Error will be thrown directly to prevent silent overwriting.
});
//Conflict example output:
// Error: [schema-dsl] i18n key conflict in locale 'zh-CN'
//Conflict key: account.notFound
// Source file: /project/locales/account/zh-CN.cjs
Code section division suggestions
When multiple people develop, it is recommended to maintain a locales/CODE-SEGMENTS.md in the project root directory and agree on the code number segment of each module:
Non-language files such as CODE-SEGMENTS.md / CODE-SEGMENTS.js will be automatically skipped, so there is no need to worry about being accidentally loaded.
🚀 Quick start
Correct usage: Load all language packages at once when the application starts, and switch directly during runtime.
Step 1: Create language pack files
## Project structure
my-project/
├── locales/ # Language pack directory
│ ├── zh-CN.cjs # Chinese (CommonJS / ESM projects are stable)
│ ├── en-US.jsonc # English (with comments / trailing comma)
│ └── pt-BR.json5 # Portuguese (JSON5 style)
└── app.js
Step 2: Define language package (locales/pt-BR.json5)
{
// Generic validation error
'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 validation
'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 validation
'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 validation
'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 validation
'boolean.base': '{{#label}} deve ser um booleano',
// Object validation
'object.base': '{{#label}} deve ser um objeto',
// Array validation
'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 validation
'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}}',
// Custom mode
'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'
}
Step 3: Load all languages at once when the app starts
import { s, validate } from 'schema-dsl/pure';
import path from 'path';
// ========== Configuration when the application starts (only executed once) ==========
s.config({
i18n: path.join(__dirname, 'locales') // Automatically load all language files in the directory
});
// Description:
// 1. Automatically scan `.js` (CommonJS), `.cjs`, `.json`, `.jsonc`, `.json5` in the locales/ directory
// 2. Extract the language code from the file name (such as pt-BR.cjs → pt-BR)
// 3. Automatically load and register all language packages
// 4. The user-defined language pack will be merged with the system default language pack, and the user’s will take precedence.
// ========== Switch language directly during runtime (no need to reload) ==========
const schema = s({ username: 'string:3-32!' });
// use Portuguese
const result1 = validate(schema, { username: 'ab' }, { locale: 'pt-BR' });
// Error message: "username deve ter pelo menos 3 caracteres"
// use Chinese
const result2 = validate(schema, { username: 'ab' }, { locale: 'zh-CN' });
// Error message: "username cannot be less than 3 characters long"
// use English
const result3 = validate(schema, { username: 'ab' }, { locale: 'en-US' });
// Error message: "username length must be at least 3"
Language pack merge strategy
// System built-in zh-CN language pack
const systemZhCN = {
'required': '{{#label}} is required',
'string.minLength': '{{#label}} cannot be less than {{#limit}} characters'
};
// User-defined locales/zh-CN.cjs
const userZhCN = {
'required': '{{#label}} must be filled in', // Override the system default
'custom.myError': 'Custom error' // Add custom message
};
// Final merge result (deep merge)
const finalZhCN = {
'required': '{{#label}}must be filled in', // ✅ User priority
'string.minLength': '{{#label}} cannot be less than {{#limit}} characters', // keep the system default
'custom.myError': 'Custom error' // Add custom message
};
Method 2: Pass in the object directly (suitable for dynamic configuration)
import { s } from 'schema-dsl/pure';
import ptBR from './locales/pt-BR.cjs';
import deDE from './locales/de-DE.cjs';
import koKR from './locales/ko-KR.cjs';
// Configure when application starts
s.config({
i18n: {
'pt-BR': ptBR,
'de-DE': deDE,
'ko-KR': koKR
}
});
//Switch directly during runtime
validate(schema, data, { locale: 'pt-BR' });
validate(schema, data, { locale: 'de-DE' });
⚠️ Error example (not recommended)
❌ Error: Loading language packs individually at runtime
import { Locale } from 'schema-dsl/pure';
// ❌ Not recommended: dynamically load before each validation
async function validateUser(data, locale) {
if (locale === 'pt-BR') {
const { default: ptBR } = await import('./locales/pt-BR.cjs');
Locale.addLocale('pt-BR', ptBR); // Load every time, poor performance
}
return validate(schema, data, { locale });
}
// ✅ Correct: Load once when the app starts
// app.js startup entry
s.config({ i18n: './locales' }); // Only load once
//Switch directly during runtime without reloading
function validateUser(data, locale) {
return validate(schema, data, { locale }); // ✅ Direct switch, good performance
}
Why is "first load, switch at runtime" recommended?
🎯 Full example
// ========== app.js (application startup entrance) ==========
import express from 'express';
import { s, validate } from 'schema-dsl/pure';
import path from 'path';
//Load all language packs at once when the application starts
s.config({
i18n: path.join(__dirname, 'locales')
});
const app = express();
// ========== routes/user.js (business routing) ==========
const userSchema = s({
username: 'string:3-32!',
email: 'email!',
age: 'number:18-120'
});
app.post('/api/users', (req, res) => {
// Get user language preference from request header
const locale = req.headers['accept-language']?.split(',')[0]?.trim() || 'en-US';
// Validation (switch language directly, no loading required)
const result = validate(userSchema, req.body, { locale });
if (!result.valid) {
return res.status(400).json({
errors: result.errors // Automatically use the user's preferred language
});
}
// Handle the request...
});
📋 Complete list of message keys
Universal keys
String validation key
Numeric validation key
Custom mode key
🎨 Template variables
All error messages support the following template variables:
📚 Refer to the built-in language pack
You can refer to the built-in language pack as a template:
import { Locale } from 'schema-dsl/pure';
// View Chinese language pack
const zhCN = Locale.getMessages('zh-CN');
console.log(zhCN);
// View English language pack
const enUS = Locale.getMessages('en-US');
console.log(enUS);
Or view the source code directly:
- Chinese:
src/locales/zh-CN.ts
- English:
src/locales/en-US.ts
- Japanese:
src/locales/ja-JP.ts
- Spanish:
src/locales/es-ES.ts
- French:
src/locales/fr-FR.ts
✅ Best Practices
- Completeness: Make sure all common error message keys are translated
- Consistency: Keep the error message style consistent
- Template variables: Correctly use
{{#label}}, {{#limit}} and other variables
- Test: Test after adding the language pack to ensure that all messages are displayed correctly
- Documentation: Write usage instructions for custom language packs
🤝 Contribute language packs
If you have added a new language pack for schema-dsl, please submit a Pull Request:
- Fork project
- Create a new language file (such as
pt-BR.ts) in the src/locales/ directory
- Complete translation of all message keys
- Register new language in
src/locales/index.ts
- Add test cases (in the
test/unit/locales/ directory)
- Submit Pull Request
📞 Support
If you have trouble adding a language pack:
Corresponding sample file
Example entry: add-custom-locale.ts
Description: Minimal workflow that covers Locale.addLocale() registering a new language, reading message text, and performing validation under a custom locale.