运行时多语言支持 - schema-dsl

版本: v1.1.8+
更新日期: 2026-01-30


📋 概述

schema-dsl 的 dsl.errorI18nError 支持运行时指定语言,无需修改全局语言设置。

这对于 API 开发特别有用,可以根据每个请求的语言偏好(如 Accept-Language 请求头)动态返回对应语言的错误消息。

🆕 智能参数识别(v1.1.8)

v1.1.8 新增:支持简化语法,从4个参数减少到2个参数

// ✅ 新增:简化语法(推荐)
dsl.error.throw('account.notFound', 'zh-CN');
dsl.error.throw('account.notFound', 'zh-CN', 404);

// ✅ 标准语法(完全兼容)
dsl.error.throw('account.notFound', {}, 404, 'zh-CN');

智能识别规则

  • 第2个参数是 string → 识别为语言参数
  • 第2个参数是 object → 识别为参数对象
  • 第2个参数是 null/undefined/数组 → 使用默认值

🎨 支持的模板语法(v1.1.4+)

schema-dsl 现在支持多种模板语法格式,提供更好的兼容性:

语法格式示例说明版本
{{#variable}}余额{{#balance}}元井号格式(现有)v1.0.0+
{{variable}}余额{{balance}}元无井号格式(新增)v1.1.4+
{variable}余额{balance}元单花括号(新增)v1.1.4+
混合格式{{#user}}在{date}购买{{product}}可混用多种格式v1.1.4+

示例

// 所有格式都支持
Locale.addLocale('zh-CN', {
  'msg1': '余额不足,当前{{#balance}}元',  // {{#}} 格式
  'msg2': '用户{{name}}已登录',            // {{}} 格式
  'msg3': '订单{orderId}已支付',           // {} 格式
  'msg4': '{{#user}}在{date}购买了{{product}}'  // 混合格式
});

向后兼容

  • ✅ 现有的 {{#variable}} 格式完全兼容
  • ✅ 所有单元测试通过
  • ✅ 无破坏性变更

🎯 三种使用方式

方式 1: 简化语法(v1.1.8 推荐)⭐

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

// 配置语言包
Locale.addLocale('zh-CN', {
  'account.notFound': {
    code: 40001,
    message: '账户不存在'
  }
});

Locale.addLocale('en-US', {
  'account.notFound': {
    code: 40001,
    message: 'Account not found'
  }
});

// ✅ 简化语法:直接传语言参数
const error1 = dsl.error.create('account.notFound', 'zh-CN');
console.log(error1.message);  // "账户不存在"

const error2 = dsl.error.create('account.notFound', 'en-US');
console.log(error2.message);  // "Account not found"

适用场景

  • 不需要参数插值
  • API 开发中最常见
  • 代码最简洁

方式 2: 全局语言设置(传统方式)

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

// 设置全局语言
Locale.setLocale('zh-CN');

// 后续所有错误都使用中文
const error1 = dsl.error.create('account.notFound');
console.log(error1.message);  // "账户不存在"

const error2 = dsl.error.create('user.noPermission');
console.log(error2.message);  // "没有管理员权限"

适用场景

  • 单一语言的应用
  • 不需要动态切换语言
  • 简单的错误处理

方式 2: 运行时指定语言(推荐用于 API)⭐

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

// 全局保持默认语言
Locale.setLocale('zh-CN');

// 每次调用时指定语言
const error1 = dsl.error.create('account.notFound', {}, 404, 'zh-CN');
console.log(error1.message);  // "账户不存在"

const error2 = dsl.error.create('account.notFound', {}, 404, 'en-US');
console.log(error2.message);  // "Account not found"

const error3 = dsl.error.create('account.notFound', {}, 404, 'ja-JP');
console.log(error3.message);  // "account.notFound"(日语未翻译)

适用场景

  • 多语言 API
  • 根据请求头动态返回多语言错误
  • 同一请求中需要多种语言
  • 微服务架构中的错误传递

🔧 API 参数

dsl.error.create()

dsl.error.create(
  code: string,          // 错误代码(如 'account.notFound')
  params?: object,       // 参数插值(如 { balance: 50 })
  statusCode?: number,   // HTTP 状态码(默认 400)
  locale?: string        // 🆕 运行时语言(如 'en-US')
): I18nError

dsl.error.throw()

dsl.error.throw(
  code: string,
  params?: object,
  statusCode?: number,
  locale?: string        // 🆕 运行时语言
): never

dsl.error.assert()

dsl.error.assert(
  condition: any,
  code: string,
  params?: object,
  statusCode?: number,
  locale?: string        // 🆕 运行时语言
): void

💡 实际应用场景

场景 1: Express/Koa 中根据请求头返回多语言错误

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

function getRequestLocale(acceptLanguage) {
  return acceptLanguage?.split(',')[0]?.trim() || 'zh-CN';
}

// Express 中间件
app.get('/api/account/:id', async (req, res, next) => {
  try {
    const account = await getAccount(req.params.id);
    
    // 根据请求头获取语言
    const locale = getRequestLocale(req.headers['accept-language']);
    
    // 使用运行时语言抛出错误
    dsl.error.assert(account, 'account.notFound', {}, 404, locale);
    
    res.json(account);
  } catch (error) {
    if (error instanceof I18nError) {
      return res.status(error.statusCode).json(error.toJSON());
    }
    next(error);
  }
});

// 请求示例
// 中文客户端: Accept-Language: zh-CN
// 响应: { "code": "account.notFound", "message": "账户不存在", ... }

// 英文客户端: Accept-Language: en-US
// 响应: { "code": "account.notFound", "message": "Account not found", ... }

场景 2: 微服务架构中的错误传递

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

// 服务 A: 用户服务
async function getUserService(userId, locale) {
  const user = await db.findUser(userId);
  
  // 传递 locale 到错误
  dsl.error.assert(user, 'user.notFound', { userId }, 404, locale);
  
  return user;
}

// 服务 B: API 网关
app.get('/api/users/:id', async (req, res) => {
  try {
    const locale = getRequestLocale(req.headers['accept-language']);
    
    // 调用用户服务,传递 locale
    const user = await getUserService(req.params.id, locale);
    
    res.json(user);
  } catch (error) {
    // 错误已经是正确的语言
    res.status(error.statusCode).json(error.toJSON());
  }
});

场景 3: 同一请求中使用多种语言

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

// 批量验证,为不同用户返回不同语言的错误
async function batchValidateAccounts(requests) {
  const results = [];
  
  for (const req of requests) {
    try {
      const account = await getAccount(req.accountId);
      
      // 每个用户使用各自的语言偏好
      dsl.error.assert(
        account.balance >= req.amount,
        'account.insufficientBalance',
        { balance: account.balance, required: req.amount },
        400,
        req.locale  // 每个用户的语言偏好
      );
      
      results.push({ success: true, accountId: req.accountId });
    } catch (error) {
      results.push({
        success: false,
        accountId: req.accountId,
        error: error.toJSON()  // 错误已经是对应用户的语言
      });
    }
  }
  
  return results;
}

// 调用示例
const results = await batchValidateAccounts([
  { accountId: '001', amount: 100, locale: 'zh-CN' },  // 中文用户
  { accountId: '002', amount: 200, locale: 'en-US' },  // 英文用户
  { accountId: '003', amount: 300, locale: 'ja-JP' }   // 日文用户
]);

// 结果:每个用户收到对应语言的错误消息

场景 4: GraphQL Resolver 中的多语言错误

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

const resolvers = {
  Query: {
    account: async (_, { id }, context) => {
      // 从 context 获取用户语言偏好
      const locale = context.user?.locale || 'zh-CN';
      
      const account = await getAccount(id);
      
      // 使用运行时语言
      dsl.error.assert(account, 'account.notFound', {}, 404, locale);
      
      return account;
    }
  }
};

🔍 运行时语言 vs 全局语言

对比表

特性全局语言运行时语言
设置方式Locale.setLocale('zh-CN')dsl.error.create(..., locale)
影响范围全局所有错误仅当前错误
是否改变全局状态✅ 是❌ 否
适用场景单一语言应用多语言 API
并发安全⚠️ 需注意✅ 完全安全
推荐用于简单应用API/微服务

并发安全性

全局语言(不推荐用于多语言 API):

// ❌ 并发不安全
app.get('/api/account/:id', async (req, res) => {
  // 修改全局状态
  Locale.setLocale(req.headers['accept-language']?.split(',')[0]?.trim() || 'zh-CN');
  
  // 如果同时有多个请求,语言会互相干扰
  const error = dsl.error.create('account.notFound');
  // 错误消息可能是错误的语言!
});

运行时语言(推荐):

// ✅ 并发安全
app.get('/api/account/:id', async (req, res) => {
  const locale = req.headers['accept-language']?.split(',')[0]?.trim() || 'zh-CN';
  
  // 不修改全局状态,每个请求独立
  const error = dsl.error.create('account.notFound', {}, 404, locale);
  // 错误消息始终是正确的语言
});

📊 测试验证

运行时语言测试

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

// 设置全局为中文
Locale.setLocale('zh-CN');

// 测试1: 运行时指定不同语言
const error1 = dsl.error.create('account.notFound', {}, 404, 'zh-CN');
const error2 = dsl.error.create('account.notFound', {}, 404, 'en-US');
const error3 = dsl.error.create('account.notFound', {}, 404, 'ja-JP');

console.log(error1.message);  // "账户不存在"
console.log(error2.message);  // "Account not found"
console.log(error3.message);  // "account.notFound"

// 测试2: 验证全局语言未被改变
const currentLocale = Locale.getLocale();
console.log(currentLocale);  // "zh-CN"

const error4 = dsl.error.create('user.noPermission');  // 不指定locale
console.log(error4.message);  // "没有管理员权限"(使用全局语言)

带参数的运行时语言

const error1 = dsl.error.create(
  'account.insufficientBalance',
  { balance: 50, required: 100 },
  400,
  'zh-CN'
);
console.log(error1.message);  // "余额不足,当前余额50,需要100"

const error2 = dsl.error.create(
  'account.insufficientBalance',
  { balance: 50, required: 100 },
  400,
  'en-US'
);
console.log(error2.message);  // "Insufficient balance, current: 50, required: 100"

🎯 最佳实践

1. API 开发中始终使用运行时语言

// ✅ 推荐
app.get('/api/account/:id', async (req, res) => {
  const locale = req.headers['accept-language']?.split(',')[0]?.trim() || 'zh-CN';
  
  try {
    const account = await getAccount(req.params.id);
    dsl.error.assert(account, 'account.notFound', {}, 404, locale);
    res.json(account);
  } catch (error) {
    res.status(error.statusCode).json(error.toJSON());
  }
});

// ❌ 不推荐
app.get('/api/account/:id', async (req, res) => {
  Locale.setLocale(req.headers['accept-language']?.split(',')[0]?.trim() || 'zh-CN');  // 并发不安全
  // ...
});

2. 统一封装语言获取逻辑

// 工具函数
function getUserLocale(req) {
  return req.user?.locale || 
         req.headers['accept-language']?.split(',')[0]?.trim() || 
         'zh-CN';
}

// 在业务代码中使用
app.get('/api/account/:id', async (req, res) => {
  const locale = getUserLocale(req);
  
  try {
    const account = await getAccount(req.params.id);
    dsl.error.assert(account, 'account.notFound', {}, 404, locale);
    res.json(account);
  } catch (error) {
    res.status(error.statusCode).json(error.toJSON());
  }
});

3. 在微服务间传递 locale

// 服务 A: 底层服务
async function getUser(userId, options = {}) {
  const user = await db.findUser(userId);
  
  dsl.error.assert(
    user,
    'user.notFound',
    { userId },
    404,
    options.locale  // 接收 locale 参数
  );
  
  return user;
}

// 服务 B: API 网关
app.get('/api/users/:id', async (req, res) => {
  const locale = getUserLocale(req);
  
  try {
    const user = await getUser(req.params.id, { locale });
    res.json(user);
  } catch (error) {
    res.status(error.statusCode).json(error.toJSON());
  }
});

📝 向后兼容

完全向后兼容

  • 现有代码无需修改
  • locale 参数为可选参数
  • 不传 locale 时使用全局语言
  • 相关单元测试已覆盖

🔗 相关文档


对应示例文件

示例入口: runtime-locale-support.ts
说明: 覆盖运行时指定 locale 创建错误对象、参数插值,以及“局部语言切换不污染全局状态”的关键行为。


最后更新: 2026-05-08
作者: schema-dsl Team