NestJS国际化

学习目标

  • 掌握NestJS国际化模块的使用方法
  • 理解语言文件的结构和管理方式
  • 学习如何实现动态语言切换
  • 了解国际化的最佳实践和常见问题

核心知识点

1. 国际化简介

国际化(Internationalization,简称i18n)是指设计和开发应用程序时,使其能够轻松适应不同语言和地区的需求,而无需对代码进行重大修改。在NestJS中,国际化支持主要通过@nestjs/i18n包实现。

2. 安装和配置

首先,我们需要安装国际化模块:

npm install --save @nestjs/i18n

然后,在应用的根模块中导入并配置国际化模块:

// src/app.module.ts
import { Module } from '@nestjs/common';
import { I18nModule, QueryResolver, AcceptLanguageResolver } from '@nestjs/i18n';
import * as path from 'path';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    I18nModule.forRoot({
      fallbackLanguage: 'en',
      loaderOptions: {
        path: path.join(__dirname, '/i18n/'),
        watch: true,
      },
      resolvers: [
        new QueryResolver(['lang', 'locale']),
        new AcceptLanguageResolver(),
      ],
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

3. 语言文件结构

NestJS的国际化模块支持多种语言文件格式,包括JSON、YAML和CSV。默认情况下,它使用JSON格式。语言文件应该按照以下结构组织:

src/
  i18n/
    en/
      translation.json
    zh/
      translation.json

4. 语言文件内容

语言文件的内容是一个键值对对象,其中键是翻译的标识符,值是对应语言的翻译文本。例如:

英文语言文件 (src/i18n/en/translation.json):

{
  "HELLO": "Hello",
  "WELCOME": "Welcome to our application",
  "GREETING": "Hello {{name}}, how are you today?",
  "USER": {
    "NAME": "Name",
    "EMAIL": "Email",
    "AGE": "Age"
  }
}

中文语言文件 (src/i18n/zh/translation.json):

{
  "HELLO": "你好",
  "WELCOME": "欢迎使用我们的应用",
  "GREETING": "你好 {{name}},今天过得怎么样?",
  "USER": {
    "NAME": "姓名",
    "EMAIL": "邮箱",
    "AGE": "年龄"
  }
}

5. 使用翻译

在NestJS中,我们可以通过以下几种方式使用翻译:

5.1 在控制器中使用

// src/app.controller.ts
import { Controller, Get, Query } from '@nestjs/common';
import { I18nService } from '@nestjs/i18n';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(
    private readonly appService: AppService,
    private readonly i18n: I18nService,
  ) {}

  @Get()
async getHello(@Query('name') name: string) {
    return await this.i18n.translate('GREETING', {
      args: { name: name || 'World' },
    });
  }
}

5.2 在服务中使用

// src/app.service.ts
import { Injectable } from '@nestjs/common';
import { I18nService } from '@nestjs/i18n';

@Injectable()
export class AppService {
  constructor(private readonly i18n: I18nService) {}

  async getHello(): Promise<string> {
    return await this.i18n.translate('HELLO');
  }
}

5.3 在管道中使用

// src/common/pipes/validation.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';
import { I18nService } from '@nestjs/i18n';

@Injectable()
export class ValidationPipe implements PipeTransform<any> {
  constructor(private readonly i18n: I18nService) {}

  async transform(value: any, { metatype }: ArgumentMetadata) {
    if (!metatype || !this.toValidate(metatype)) {
      return value;
    }
    const object = plainToClass(metatype, value);
    const errors = await validate(object);
    if (errors.length > 0) {
      const errorMessages = [];
      for (const error of errors) {
        const constraints = error.constraints;
        for (const key in constraints) {
          errorMessages.push(await this.i18n.translate(`VALIDATION.${key}`, {
            args: error.value,
          }));
        }
      }
      throw new BadRequestException(errorMessages);
    }
    return value;
  }

  private toValidate(metatype: Function): boolean {
    const types: Function[] = [String, Boolean, Number, Array, Object];
    return !types.includes(metatype);
  }
}

6. 动态语言切换

NestJS的国际化模块提供了多种解析器来确定当前语言:

  • QueryResolver:从查询参数中获取语言
  • HeaderResolver:从请求头中获取语言
  • CookieResolver:从cookie中获取语言
  • AcceptLanguageResolver:从Accept-Language请求头中获取语言
  • LocalStorageResolver:从localStorage中获取语言(仅在GraphQL上下文中可用)

我们可以根据需要组合使用这些解析器。

7. 自定义解析器

除了使用内置的解析器外,我们还可以创建自定义解析器:

// src/common/resolvers/custom-language.resolver.ts
import { I18nResolver } from '@nestjs/i18n';
import { Request } from 'express';

export class CustomLanguageResolver implements I18nResolver {
  resolve(req: Request): string | string[] {
    // 从请求中获取语言,例如从JWT令牌中
    const language = req.headers['x-language'] as string;
    return language || 'en';
  }
}

然后在模块配置中使用:

// src/app.module.ts
import { Module } from '@nestjs/common';
import { I18nModule, QueryResolver } from '@nestjs/i18n';
import * as path from 'path';
import { CustomLanguageResolver } from './common/resolvers/custom-language.resolver';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    I18nModule.forRoot({
      fallbackLanguage: 'en',
      loaderOptions: {
        path: path.join(__dirname, '/i18n/'),
        watch: true,
      },
      resolvers: [
        new QueryResolver(['lang', 'locale']),
        new CustomLanguageResolver(),
      ],
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

实用案例分析

案例1:多语言用户界面

需求分析

我们需要实现一个支持多语言的用户界面,用户可以通过下拉菜单切换语言,系统会根据用户的选择显示相应语言的内容。

实现方案

  1. 创建语言文件
// src/i18n/en/translation.json
{
  "LANGUAGE": "Language",
  "ENGLISH": "English",
  "CHINESE": "Chinese",
  "DASHBOARD": "Dashboard",
  "PROFILE": "Profile",
  "SETTINGS": "Settings",
  "LOGOUT": "Logout"
}
// src/i18n/zh/translation.json
{
  "LANGUAGE": "语言",
  "ENGLISH": "英语",
  "CHINESE": "中文",
  "DASHBOARD": "仪表盘",
  "PROFILE": "个人资料",
  "SETTINGS": "设置",
  "LOGOUT": "退出登录"
}
  1. 创建语言切换服务
// src/i18n/i18n.service.ts
import { Injectable } from '@nestjs/common';
import { I18nService } from '@nestjs/i18n';

@Injectable()
export class CustomI18nService {
  constructor(private readonly i18n: I18nService) {}

  async translate(key: string, options?: any): Promise<string> {
    return this.i18n.translate(key, options);
  }

  getSupportedLanguages(): Array<{ code: string; name: string }> {
    return [
      { code: 'en', name: 'English' },
      { code: 'zh', name: '中文' },
    ];
  }
}
  1. 在控制器中使用
// src/app.controller.ts
import { Controller, Get, Query, Res } from '@nestjs/common';
import { Response } from 'express';
import { I18nService } from '@nestjs/i18n';
import { CustomI18nService } from './i18n/i18n.service';

@Controller()
export class AppController {
  constructor(
    private readonly i18n: I18nService,
    private readonly customI18nService: CustomI18nService,
  ) {}

  @Get()
async getIndex(@Query('lang') lang: string, @Res() res: Response) {
    const translations = {
      language: await this.i18n.translate('LANGUAGE'),
      english: await this.i18n.translate('ENGLISH'),
      chinese: await this.i18n.translate('CHINESE'),
      dashboard: await this.i18n.translate('DASHBOARD'),
      profile: await this.i18n.translate('PROFILE'),
      settings: await this.i18n.translate('SETTINGS'),
      logout: await this.i18n.translate('LOGOUT'),
    };

    const supportedLanguages = this.customI18nService.getSupportedLanguages();

    res.render('index', {
      translations,
      supportedLanguages,
      currentLanguage: lang || 'en',
    });
  }

  @Get('/switch-language')
async switchLanguage(@Query('lang') lang: string, @Res() res: Response) {
    // 设置语言cookie
    res.cookie('lang', lang, { maxAge: 900000, httpOnly: true });
    // 重定向回首页
    res.redirect('/');
  }
}
  1. 创建视图文件
<!-- src/views/index.ejs -->
<!DOCTYPE html>
<html lang="<%= currentLanguage %>">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>NestJS Internationalization</title>
</head>
<body>
  <div class="header">
    <h1><%= translations.dashboard %></h1>
    <div class="language-switcher">
      <label for="language"><%= translations.language %>:</label>
      <select id="language" onchange="switchLanguage(this.value)">
        <% supportedLanguages.forEach(lang => { %>
          <option value="<%= lang.code %>" <%= currentLanguage === lang.code ? 'selected' : '' %>>
            <%= lang.name %>
          </option>
        <% }); %>
      </select>
    </div>
  </div>
  <nav>
    <ul>
      <li><a href="#"><%= translations.dashboard %></a></li>
      <li><a href="#"><%= translations.profile %></a></li>
      <li><a href="#"><%= translations.settings %></a></li>
      <li><a href="#"><%= translations.logout %></a></li>
    </ul>
  </nav>
  <div class="content">
    <h2><%= translations.welcome %></h2>
    <p><%= translations.greeting.replace('{{name}}', 'User') %></p>
  </div>
  <script>
    function switchLanguage(lang) {
      window.location.href = '/?lang=' + lang;
    }
  </script>
</body>
</html>

案例2:多语言API响应

需求分析

我们需要实现一个支持多语言的API,根据请求的语言设置返回相应语言的错误消息和响应内容。

实现方案

  1. 创建语言文件
// src/i18n/en/errors.json
{
  "VALIDATION_ERROR": "Validation error",
  "USER_NOT_FOUND": "User not found",
  "INTERNAL_SERVER_ERROR": "Internal server error",
  "UNAUTHORIZED": "Unauthorized access"
}
// src/i18n/zh/errors.json
{
  "VALIDATION_ERROR": "验证错误",
  "USER_NOT_FOUND": "用户不存在",
  "INTERNAL_SERVER_ERROR": "服务器内部错误",
  "UNAUTHORIZED": "未授权访问"
}
  1. 创建自定义异常过滤器
// src/common/filters/i18n-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { Request, Response } from 'express';
import { I18nService } from '@nestjs/i18n';

@Catch(HttpException)
export class I18nExceptionFilter implements ExceptionFilter {
  constructor(private readonly i18n: I18nService) {}

  async catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();

    let message = exception.message;

    // 尝试翻译错误消息
    try {
      message = await this.i18n.translate(`ERRORS.${message}`, {
        lang: request.headers['accept-language'] || 'en',
      });
    } catch (e) {
      // 如果翻译失败,使用原始消息
    }

    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
        message: message,
      });
  }
}
  1. 在主文件中使用
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { I18nExceptionFilter } from './common/filters/i18n-exception.filter';
import { I18nService } from '@nestjs/i18n';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // 获取i18n服务
  const i18nService = app.get(I18nService);
  // 使用自定义异常过滤器
  app.useGlobalFilters(new I18nExceptionFilter(i18nService));
  
  await app.listen(3000);
}
bootstrap();
  1. 在控制器中使用
// src/users/users.controller.ts
import { Controller, Get, Param, NotFoundException } from '@nestjs/common';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get(':id')
async findOne(@Param('id') id: string): Promise<User> {
    const user = await this.usersService.findOne(+id);
    if (!user) {
      throw new NotFoundException('USER_NOT_FOUND');
    }
    return user;
  }
}

常见问题与解决方案

1. 语言文件未加载

可能原因

  • 语言文件路径配置错误
  • 语言文件格式不正确
  • 语言文件编码问题

解决方案

  • 检查语言文件路径配置是否正确
  • 确保语言文件是有效的JSON格式
  • 使用UTF-8编码保存语言文件

2. 翻译不生效

可能原因

  • 翻译键不存在
  • 语言解析器配置错误
  • 语言文件未更新

解决方案

  • 检查翻译键是否存在于语言文件中
  • 检查语言解析器配置是否正确
  • 确保语言文件已保存并重新加载

3. 动态语言切换不工作

可能原因

  • 解析器顺序不正确
  • Cookie或查询参数设置错误
  • 缓存问题

解决方案

  • 调整解析器顺序,将优先级高的解析器放在前面
  • 检查Cookie或查询参数的设置是否正确
  • 清除浏览器缓存后重试

最佳实践

  1. 语言文件组织:按照功能模块组织语言文件,提高可维护性
  2. 命名规范:使用大写字母和下划线作为翻译键,保持一致性
  3. 参数化翻译:使用参数化翻译文本,提高灵活性
  4. 默认语言:始终设置一个默认语言作为回退选项
  5. 错误处理:为翻译失败添加适当的错误处理
  6. 缓存策略:使用缓存提高翻译性能
  7. 测试:为国际化功能编写单元测试,确保翻译正确

代码优化建议

  1. 使用翻译服务封装:创建一个封装了i18n服务的自定义服务,提供更简洁的API
  2. 翻译键常量:使用TypeScript枚举定义翻译键,避免拼写错误
  3. 批量翻译:实现批量翻译功能,减少HTTP请求
  4. 延迟加载:对于大型应用,考虑使用延迟加载语言文件
  5. 翻译管理工具:使用专业的翻译管理工具,如i18next,提高翻译效率

总结

NestJS的国际化模块提供了一种简洁、高效的方式来实现多语言支持。通过本文的学习,你应该已经掌握了:

  • 如何安装和配置国际化模块
  • 如何创建和组织语言文件
  • 如何在控制器、服务和管道中使用翻译
  • 如何实现动态语言切换
  • 如何创建自定义语言解析器
  • 国际化的最佳实践和常见问题解决方案

国际化是现代应用程序的重要组成部分,它可以帮助你扩大用户群体,提高用户体验。合理使用NestJS的国际化功能,可以让你的应用程序更好地适应全球化需求。

互动问答

  1. 以下哪个是NestJS国际化模块的正确安装命令?
    A. npm install --save nestjs-i18n
    B. npm install --save @nestjs/i18n
    C. npm install --save i18n
    D. npm install --save @i18n/nestjs

  2. 如何在NestJS中设置默认语言?

  3. 如何在翻译文本中使用动态参数?

  4. 如何创建自定义语言解析器?

  5. 如何在异常过滤器中使用国际化?

« 上一篇 NestJS定时任务 下一篇 » NestJS HTTP客户端