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.json4. 语言文件内容
语言文件的内容是一个键值对对象,其中键是翻译的标识符,值是对应语言的翻译文本。例如:
英文语言文件 (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:多语言用户界面
需求分析
我们需要实现一个支持多语言的用户界面,用户可以通过下拉菜单切换语言,系统会根据用户的选择显示相应语言的内容。
实现方案
- 创建语言文件:
// 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": "退出登录"
}- 创建语言切换服务:
// 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: '中文' },
];
}
}- 在控制器中使用:
// 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('/');
}
}- 创建视图文件:
<!-- 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,根据请求的语言设置返回相应语言的错误消息和响应内容。
实现方案
- 创建语言文件:
// 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": "未授权访问"
}- 创建自定义异常过滤器:
// 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,
});
}
}- 在主文件中使用:
// 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();- 在控制器中使用:
// 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或查询参数的设置是否正确
- 清除浏览器缓存后重试
最佳实践
- 语言文件组织:按照功能模块组织语言文件,提高可维护性
- 命名规范:使用大写字母和下划线作为翻译键,保持一致性
- 参数化翻译:使用参数化翻译文本,提高灵活性
- 默认语言:始终设置一个默认语言作为回退选项
- 错误处理:为翻译失败添加适当的错误处理
- 缓存策略:使用缓存提高翻译性能
- 测试:为国际化功能编写单元测试,确保翻译正确
代码优化建议
- 使用翻译服务封装:创建一个封装了i18n服务的自定义服务,提供更简洁的API
- 翻译键常量:使用TypeScript枚举定义翻译键,避免拼写错误
- 批量翻译:实现批量翻译功能,减少HTTP请求
- 延迟加载:对于大型应用,考虑使用延迟加载语言文件
- 翻译管理工具:使用专业的翻译管理工具,如i18next,提高翻译效率
总结
NestJS的国际化模块提供了一种简洁、高效的方式来实现多语言支持。通过本文的学习,你应该已经掌握了:
- 如何安装和配置国际化模块
- 如何创建和组织语言文件
- 如何在控制器、服务和管道中使用翻译
- 如何实现动态语言切换
- 如何创建自定义语言解析器
- 国际化的最佳实践和常见问题解决方案
国际化是现代应用程序的重要组成部分,它可以帮助你扩大用户群体,提高用户体验。合理使用NestJS的国际化功能,可以让你的应用程序更好地适应全球化需求。
互动问答
以下哪个是NestJS国际化模块的正确安装命令?
A.npm install --save nestjs-i18n
B.npm install --save @nestjs/i18n
C.npm install --save i18n
D.npm install --save @i18n/nestjs如何在NestJS中设置默认语言?
如何在翻译文本中使用动态参数?
如何创建自定义语言解析器?
如何在异常过滤器中使用国际化?