前端存在国际化,后端也存在国际化。
前端 React 使用 react-i18next
,vue 使用 vue-i18n
; 后端 NestJS 就使用 nestjs-i18n
。
i18n
: 是 internationalization 的缩写,是通过取第一个字母(i),最后一个字母(n),以及中间18个字符组成的。
下面来了解 nest-i18n
在 Nestjs 中是如何使用的。
本篇需要了解 nestjs 的基本语法
基本使用
先安装依赖
bash
pnpm add nestjs-i18n --save # 版本号为:^10.5.1
然后就是定义一个全局 i18n 模块
ts
// i18m.module.ts
import { Module, Global } from "@nestjs/common"
import { I18nModule, QueryResolver, HeaderResolver } from "nestjs-i18n"
import * as path from "path"
import { I18nNestService } from "./i18n.service"
@Global()
@Module({
imports: [
I18nModule.forRoot({
// 默认语法
fallbackLanguage: "zh",
// 国际化文件配置路径
loaderOptions: {
path: path.join(__dirname, "../../i18n/"),
watch: true,
},
// 支持 query 或者 header 传递 lang 参数(有顺序问题)
resolvers: [new QueryResolver(["lang", "l"]), new HeaderResolver(["lang"])],
}),
],
providers: [I18nNestService],
exports: [I18nNestService],
})
export class I18nNestModule {}
fallbackLanguage
就是配置一种默认语法;比如en
或者zh
; 这个属性值根据你的配置文件(src/i18n
)里面的文件目录保持一致。比如我的:
-
loaderOptions
: 就是加载对应的配置文件;一般来说 i18n 放在 src 目录下,但是我的i18n.module.ts
放在src/core/i18n
下面,那么就需要退两级来找到 i18n 配置文件。tsloaderOptions: { // 退了两级 path: path.join(__dirname, "../../i18n/"), watch: true, },
-
resolvers
: 解析器。也就是从哪里读取当前语言信息;QueryResolver 从 query 中读取(比如: ?lang=en 或者是 ?l=zh);HeaderResolver 从 header 中读取(在 header 里面加上 lang 属性即可)。当然还有其他的解析器。 -
@Global
把 I18nNestModule 定义成一个全局的模块,后续的 I18nService 可以直接导入使用。那么看看 I18nService
ts
// i18n.service.ts
import { Injectable } from "@nestjs/common"
import { I18nService, I18nContext } from "nestjs-i18n"
@Injectable()
export class I18nNestService {
constructor(private readonly i18n: I18nService) {}
t(key: string, options?: Record<string, any>): string {
return this.i18n.t(key, {
...options,
// 读取语言信息
lang: I18nContext.current().lang,
})
}
}
那么在使用的使用,直接传递 key 值就行了;如:this.i18n.t('system.hello')
。
当配置好之后,就可以启动 nest 项目了。对了,还差一步,nest 项目启动会打成一个 dist 包,但是 i18n 没有引入,不会被打入进包,需要配置一下自动复杂。
json
// nest-cli.json
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true,
// 新增(自动复制国际化静态资源)
"assets": [{ "include": "i18n/**/*", "watchAssets": true }]
}
}
上面准备工作做完了,来测试一下接口的返回值
ts
import { Controller, Get, Inject } from "@nestjs/common"
import { I18nNestService } from "src/core/i18n/i18n.service"
@Controller("auth")
export class AuthController {
@Inject(I18nNestService)
i18n: I18nNestService
@Get("test")
test() {
return this.i18n.t("system.test")
}
}
直接使用 I18nNestService,调用 t 函数传递一个 key 即可。
json
// i18n/en/system
{
"test": "hello"
}
// i18n/zh/system
{
"test": "你好"
}
默认是中文zh
(因为上面注册模块时,配置了默认值)
当在 query 上设置 lang 属性,属性值为 en
(header 也是一样的)
还有一种情况,针对 post 请求 dto 的验证,那么该怎么写呢?
定义一个 dto
ts
import { IsString, IsNotEmpty, IsNumber } from "class-validator"
import { i18nValidationMessage } from "nestjs-i18n"
export class TestDto {
@IsString({
message: i18nValidationMessage("system.isString", {
key: "name",
}),
})
@IsNotEmpty({
message: i18nValidationMessage("system.notEmpty", {
key: "name",
}),
})
name: string
@IsNumber(
{},
{
message: i18nValidationMessage("system.isNumber", {
key: "age",
}),
},
)
@IsNotEmpty({
message: i18nValidationMessage("system.notEmpty", {
key: "age",
}),
})
age: number
}
i18nValidationMessage
是针对国际化动态传递参数的
国际化文件配置信息
json
// en/system.json
{
"notEmpty": "{key} can not be empty",
"isString": "{key} mast be a string",
"isNumber": "{key} mast be a number"
}
// zh/system.ts
{
"notEmpty": "{key}不能为空",
"isString": "{key}必须是字符串",
"isNumber": "{key}必须是数字"
}
针对 DTO 的验证,需要使用到 AOP 中的 pipe。
@nestjs/common
提供的 ValidationPipe 是无法获取到国际化的错误信息的,需要借助 nestjs-i18n
提供的 I18nValidationPipe
。在 main.ts 中使用一下:
ts
// main.ts
app.useGlobalPipes(
new I18nValidationPipe({
whitelist: true,
transform: true,
}),
)
验证 post 请求的 dto
ts
import { Controller, Post, Body, Inject } from "@nestjs/common"
import { TestDto } from "./dto/auth.dto"
import { I18nNestService } from "src/core/i18n/i18n.service"
@Controller("auth")
export class AuthController {
@Inject(I18nNestService)
i18n: I18nNestService
@Post("testPost")
testPost(@Body() user: TestDto) {
return this.i18n.t("system.test")
}
}
调用接口,默认情况下,是中文
在 header 中设置 lang 属性,属性值为 en,继续请求
发现效果跟预想的都是一样。
这就是 nestjs-i18n 的大致语法,其实跟前端的使用方法大致一样。
项目中的二次封装
dto 的封装
问题一眼就看出来了
- 逻辑清晰,但是代码量比较多
- 这里 dto 才两个字段,如果是存在很多字段,这种写法就很不可观了。
那么就需要优化一下。
在这里,自己封装三个装饰器 @Empty()
、@RequireString()
、RequireNumber()
ts
// validate.decorator.ts
import { IsString, IsNotEmpty, IsNumber } from "class-validator"
import { applyDecorators } from "@nestjs/common"
import { i18nValidationMessage } from "nestjs-i18n"
/**
* 非空
*/
const Empty = (name?: string) =>
IsNotEmpty({
message: args =>
i18nValidationMessage("system.notEmpty", {
key: name || args.property,
})(args),
})
/**
* 必填字符串
*/
export const RequireString = (name?: string) =>
applyDecorators(
IsString({
message: args =>
i18nValidationMessage("system.isString", {
key: name || args.property,
})(args),
}),
Empty(name),
)
/**
* 必填数字
*/
export const RequireNumber = (name?: string) =>
applyDecorators(
IsNumber(
{},
{
message: options =>
i18nValidationMessage("system.isNumber", {
key: name || options.property,
})(options),
},
),
Empty(name),
)
@nestjs/common
提供了 applyDecorators 函数,用于合并多个装饰器。
那么重新编写 dto 文件
ts
export class TestDto {
@RequireString()
name: string
@RequireNumber()
age: number
}
是不是简洁了很多。可以测试一下,效果是一样的。
犯错记录
针对 i18nValidationMessage 可以动态传递 key 值,但是 key 又是动态获取的,这里遇到了一个卡点,花费了很多时间。
无论是 IsString, IsNumber, IsNotEmpty 都能接受一个 message,其类型是
string | ((validationArguments: ValidationArguments) => string)
这里我们采用函数的形式,函数里面的参数可以动态的拿到属性值,然后传递给 i18nValidationMessage 函数。
i18nValidationMessage 函数的类型:
ts// 简化了一下 function i18nValidationMessage(key: Path<K>, args?: any): (a: ValidationArguments) => string;
- 参数一:国际化 key 值
- 参数二(可选):动态传递的值(注意类似,虽然类型 any,但是只有传递对象,才有实际效果;传递一个函数,返回一个对象也是不行的)
ts// 那么就直接采用 message 函数形式直接返回 i18nValidationMessage,可以还是报错 IsString({ message: args => i18nValidationMessage("system.isString", { key: name || args.property, }), }) // 可以根据类型分析,i18nValidationMessage 返回的也是一个函数,其类型跟 message 函数返回的类型是一样的,那么就需要进行二次调用 IsString({ message: args => i18nValidationMessage("system.isString", { key: name || args.property, })(args) })
ERROR:最初没有二次调用,找了一半天的错误。
I18nValidationExceptionFilter 的封装
前端错误提示信息,是根据后端返回的 message 来进行提示用的,但是当 dto 不满足条件时,返回的格式是这样的:
message 是一个数组的形式,这对前端展示错误信息是不友好的,message 格式没有统一。
而返回一个数组的形式,是 nestjs-i18n 提供的 I18nValidationExceptionFilter
处理后返回的形式。那只需要在此基础上,继续优化一下就行了。
ts
// i18n.filter.ts
import { Request } from "express"
import { I18nValidationExceptionFilter } from "nestjs-i18n"
import { BUSINESS_CODE } from "src/enums"
export class I18nValidationFilter extends I18nValidationExceptionFilter {
constructor() {
super({
//返回简单的格式形式
detailedErrors: false,
// 自定义body返回的格式
responseBodyFormatter: (host, _, errors: any) => {
// 参数三:errors 就是我们上面看到的数组
const ctx = host.switchToHttp()
const request = ctx.getRequest<Request>()
return {
success: false,
timestamp: new Date().toISOString(),
message: errors.join(", "), // 转化成字符串
code: BUSINESS_CODE.PARAMS_ERROR,
path: request.url,
}
},
})
}
}
然后全局注册一下 filter 即可,就能使用了(全局注册,就不用多说了),继续调用接口
message 就是字符串了,前端就可以直接展示即可。
异常统一国际化处理
针对一些错误请求,比如说:
- 服务报错
- 用户不存在
- 没有权限
- ...
上面的错误信息,如果都需要国际化,是不是在每个地方都需要转化一下,在进行返回。
其实不需要的,直接在异常拦截中,进行转化,只需要再报错的地方,返回对应的错误信息key值即可。
类似这样的,直接返回 key 值。然后呢,在 http 异常统一处理的时候拦截
ts
// 拦截 http 异常处理
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
private logger = new Logger("HttpExceptionFilter")
// 注入 I18nNestService 服务
@Inject(I18nNestService)
private i18n: I18nNestService
catch(exception: HttpException, host: ArgumentsHost) {
// ...
const exceptionErrorInfo: any = exception.getResponse()
const errorMessage = exceptionErrorInfo?.['message']
response.status(status).json({
success: false,
timestamp: new Date().toISOString(),
// 国际化转化
message: typeof errorMessage === "string" ? this.i18n.t(errorMessage) : errorMessage,
code: BUSINESS_CODE.SYSTEM_ERROR,
path: request.url,
})
}
}
就算 key 在国际化文件中没有配置,那么就会原封不动的返回 key 值,所以没有报错风险。
若后续在 nestjs 开发中,如果发现针对国际化的封装,可以继续追加。
总结
nestjs-i18n的使用你学会了吗?
核心 api 就这几个:
- I18nModule:注册全局配置模块
- I18nValidationPipe:dto 的拦截
- I18nContext:获取当前国际化的配置信息
- i18nValidationMessage:动态传值
- I18nValidationExceptionFilter:国际化错误拦截处理