Dynamic Multilingual Configuration Guide

Basic principles

Validator of schema-dsl supports dynamically specifying the language during validation without the need for global switching.

core methods

validator.validate(schema, data, {
  locale: 'zh-CN' // Dynamically specify language
});

This is the most recommended solution. There is no need to modify the global state and it supports concurrent requests.

1.1 Configuration when the application starts (loading all languages at once)

Use s.config to load all custom language packs 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) ==========

//Method 1: Pass in the directory path (recommended)⭐
// Node >=18: Automatically scan .js (CommonJS), .cjs, .json, .jsonc, .json5 files in the directory
s.config({
  i18n: path.join(__dirname, 'locales')
});

// For data-only or untrusted locale directories, use:
// s.config({ i18n: path.join(__dirname, 'locales'), codeLocaleFiles: 'deny' });

//Method 2: Directly pass in the object
s.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'
    }
  }
});

// Description:
// - only executed once when the app starts
// - Automatically merge with the system's built-in language pack (user-defined ones take precedence)
// - No need to reload during runtime, switch directly

1.2 Switch languages directly during runtime (no need to reload)

import { s, validate } from 'schema-dsl/pure';

//define Schema
const schema = s({
  username: 'string:3-32!',
  email: 'email!'
});

// test data
const data = { username: 'ab', email: 'invalid' };

// ========== Switch language directly during runtime ==========

// use Chinese
const result1 = validate(schema, data, { locale: 'zh-CN' });
// Error: "username cannot be less than 3 characters long"

// use french
const result2 = validate(schema, data, { locale: 'fr-FR' });
// Error: "username doit contenir au moins 3 caractères"

// use german
const result3 = validate(schema, data, { locale: 'de-DE' });
// Error: "username muss mindestens 3 Zeichen lang sein"

// Description:
// - No need to reload the language pack
// - Each validation can use a different language
// - supports high concurrency (no global state modification)

1.3 Obtain the language from the request header (actual application scenario)

import express from 'express';
import { s, validate } from 'schema-dsl/pure';
import path from 'path';

const app = express();

// ========== Configuration when the application starts (only executed once) ==========
s.config({
  i18n: path.join(__dirname, 'locales')
});

//define Schema
const userSchema = s({
  username: 'string:3-32!',
  email: 'email!',
  password: 'string:8-32!'
});

// ========== Express routing ==========
app.post('/api/user/register', (req, res) => {
  // Get the language preference from the request header
  const locale = parseAcceptLanguage(req.headers['accept-language']);

  //Verify data (switch language directly without reloading)
  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
    });
  }

  // Processed successfully...
  res.json({ message: 'User registered successfully' });
});

1.3 Parsing the Accept-Language header

/**
 * Parse the Accept-Language header
 * @param {string} acceptLanguage - the value of the Accept-Language header
 * @returns {string} language code
 */
function parseAcceptLanguage(acceptLanguage) {
  if (!acceptLanguage) return 'en-US';

  // Accept-Language format: 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 };
  });

  // Sort by weight
  languages.sort((a, b) => b.q - a.q);

  //Map to supported languages
  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'; //Default language
}

// use
app.post('/api/user/register', (req, res) => {
  const locale = parseAcceptLanguage(req.headers['accept-language']);

  const result = validator.validate(schema, req.body, { locale });

  // ...
});

Option 2: Temporarily switch language

Suitable for a few scenarios.

2.1 Use closures to save the original language

function validateWithLocale(validator, schema, data, locale) {
  const originalLocale = Locale.getLocale();

  try {
    Locale.setLocale(locale);
    return validator.validate(schema, data);
  } finally {
    Locale.setLocale(originalLocale); //Restore original language
  }
}

// use
app.post('/api/user/register', (req, res) => {
  const locale = parseAcceptLanguage(req.headers['accept-language']);

  const result = validateWithLocale(validator, schema, req.body, locale);

  // ...
});

Option 3: Express/Koa middleware

Encapsulated as middleware to automatically handle language switching.

Through one-time configuration of middleware, subsequent business code does not need to care about language parameters.

import { Validator } from 'schema-dsl/pure';
const validator = new Validator();

const schemaIoMiddleware = (req, res, next) => {
  // 1. Automatically obtain the language
  const lang = req.headers['accept-language']?.split(',')[0]?.trim() || 'en-US';
  // Simple matching logic (actually accept-language-parser can be used)
  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. Mount the validation method bound to the language
  req.validate = (schema, data) => {
    return validator.validate(schema, data, { locale });
  };

  next();
};

app.use(schemaIoMiddleware);

// Used in business
app.post('/users', (req, res) => {
  // Call directly and automatically use the language parsed by the middleware
  const result = req.validate(userSchema, req.body);

  if (!result.valid) {
    return res.status(400).json({ errors: result.errors });
  }

  // ...
});

For a complete example, please refer to dynamic-locale.ts.

3.2 Koa middleware

import { Locale, Validator } from 'schema-dsl/pure';

const validator = new Validator();

/**
 * Koa language middleware
 */
function localeMiddleware() {
  return async (ctx, next) => {
    // parse language
    const locale = parseAcceptLanguage(ctx.headers['accept-language']);

    // save to context
    ctx.locale = locale;

    // Reuse the shared Validator to avoid re-creating the instance and cache for each request
    ctx.validate = function(schema, data) {
      return validator.validate(schema, data, { locale: ctx.locale });
    };

    await next();
  };
}

// Apply middleware
app.use(localeMiddleware());

// use
router.post('/api/user/register', async (ctx) => {
  // Automatically use the requested language
  const result = ctx.validate(userSchema, ctx.request.body);

  if (!result.valid) {
    ctx.status = 400;
    ctx.body = { errors: result.errors };
    return;
  }

  // ...
});

Complete example

Express complete example

import express from 'express';
import { s, Validator, Locale } from 'schema-dsl/pure';

const app = express();
app.use(express.json());

// ========== 1. Initialize language package ==========

Locale.addLocale('zh-CN', {
  'required': '{{#label}} cannot be empty',
  'min': '{{#label}}at least {{#limit}} characters',
  'max': '{{#label}} is at most {{#limit}} characters',
  'pattern': '{{#label}} format is incorrect',
  'format': 'Please enter a valid {{#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. Tool function ==========

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. Middleware ==========

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. Define Schema ==========

const userSchema = s({
  username: s('string:3-32!').label('username'),
  email: s('email!').label('email address'),
  password: s('string:8-64!').pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/)
    .label('password')
    .messages({
      'pattern': 'Password must contain uppercase and lowercase letters and numbers'
    }),
  age: s('number:18-120').label('age')
});

// ========== 5. API routing ==========

app.post('/api/user/register', (req, res) => {
  // Validate data (automatically use request language)
  const result = req.validate(userSchema, req.body);

  if (!result.valid) {
    return res.status(400).json({
      success: false,
      errors: result.errors,
      locale: req.locale // Return the language used
    });
  }

  // Handle registration logic
  res.json({
    success: true,
    message: req.locale === 'zh-CN'? 'Registration successful': 'Registration successful'
  });
});

// ========== 6. Test ==========

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
  console.log('\nTest command:');
  console.log('# Chinese error message');
  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# English error message');
  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"}\'');
});

best practices

1. Centralized management of language packs

// locales/index.js
import zhCN from './zh-CN.cjs';
import enUS from './en-US.cjs';
import jaJP from './ja-JP.cjs';

export default {
  'zh-CN': zhCN,
  'en-US': enUS,
  'ja-JP': jaJP
};

// locales/zh-CN.json
{
  "required": "{{#label}} cannot be empty",
  "min": "{{#label}} is at least {{#limit}} characters",
  "max": "{{#label}} is at most {{#limit}} characters",
  "pattern": "{{#label}} format is incorrect",
  "format": "Please enter a valid {{#label}}"
}

// initialization
import locales from './locales/index.js';
Object.entries(locales).forEach(([locale, messages]) => {
  Locale.addLocale(locale, messages);
});

2. Supported language list

const SUPPORTED_LOCALES = ['zh-CN', 'en-US', 'ja-JP'];

function getSupportedLocale(requestLocale) {
  return SUPPORTED_LOCALES.includes(requestLocale)
    ? requestLocale
    : 'en-US';
}

3. Cache validator

// cache validators for each language
const validators = {
  'zh-CN': new Validator(),
  'en-US': new Validator(),
  'ja-JP': new Validator()
};

function getValidator(locale) {
  return validators[locale] || validators['en-US'];
}

// use
const result = getValidator(req.locale).validate(
  schema,
  data,
  { locale: req.locale }
);

4. Error response standardization

function sendValidationError(res, result, locale) {
  res.status(400).json({
    success: false,
    code: 'VALIDATION_ERROR',
    message: locale === 'zh-CN'? 'Validation failed': 'Validation failed',
    errors: result.errors,
    locale: locale
  });
}

// use
if (!result.valid) {
  return sendValidationError(res, result, req.locale);
}

Plan comparison

planadvantageshortcomingRecommendation
Option 1: Specify during validation✅ No race issues
✅ Support concurrency
✅ Simple code
-⭐⭐⭐⭐⭐
Option 2: Temporary switch✅ Simple to implement⚠️ Concurrency race problem⭐⭐⭐
Option 3: Middleware✅ Automation
✅ Unified management
✅ Reusable shared Validator cache
-⭐⭐⭐⭐⭐

Recommended: Option 1 + Option 3 (Middleware Encapsulation)


FAQ

Q1: How to deal with unsupported languages?

A: Fall back to default language

Do not directly pass the original Accept-Language header to locale; common values in browsers will have q= weights, which should be parsed first and then rolled back.

function parseAcceptLanguage(acceptLanguage) {
  //... parsing logic
  return supportedLocale || 'en-US'; // Default English
}

Q2: Does it support dynamic loading of language packs?

A: Support

async function loadLocale(locale) {
  if (!Locale.getAvailableLocales().includes(locale)) {
    const messages = await import(`./locales/${locale}.json`);
    Locale.addLocale(locale, messages);
  }
}

// use
app.use(async (req, res, next) => {
  await loadLocale(req.locale);
  next();
});

Q3: How to customize error messages for certain fields?

A: Use .messages() method

const schema = s({
  password: s('string:8-64!').label('password')
    .messages({
      'required': req.locale === 'zh-CN'
        ? 'Please enter password'
        : 'Please enter password',
      'min': req.locale === 'zh-CN'
        ? 'Password is too short, at least 8 characters'
        : 'Password is too short, at least 8 characters'
    })
});


Corresponding sample file

Example entry: dynamic-locale.ts Description: Covers Accept-Language parsing, runtime locale selection, and validation entries for the same schema in different request languages.