Introduction
Many enterprise applications support multiple languages nowadays to cater to customers whose first language is not English. In nest application, we would like to add i18n support to return i18n messages to client-side applications such that users can respond with appropriate actions. Nest framework does not have i18n out of the box but we can install nestjs-i18n to handle translations and return the i18n texts to the client side.
In this post, we are going to create a new nestjs application and install nestjs-i18n to add i18n support. When request is successful, we perform translation by language and return the i18n string in the response. When request throws exception, we implement a global filter that extracts message key from the error object, translates the message and returns a new error object back to the client-side.
Create a new nest application
nest new nesti18n-poc
Add i18n support in nest
npm i --save nestjs-i18n
Create an i18n directory under src/ and create en
and es
folders under src/i18n/. The folder names, en and es, are locales of English and Spanish respectively.
src
├─src/i18n
├── en
│ └── error.json
└── es
└── error.json
Create JSON translation files
English language, src/i18n/en/error.json
{
"GOOD_MORNING": "Good Morning",
"SUBSCRIPTION_EXPIRED": "Contact {0.email} for support",
"SETUP": {
"WELCOME": "Welcome {0.username}",
"BYE": "Bye {0.username}",
"TOMATO": {
"one": "{0.username} buys {0.count} tomato",
"other": "{0.username} buys {0.count} tomatoes",
"zero": "{0.username} buys {0.count} tomato"
}
},
"EXCEPTION": "I don't know {0.language}"
}
Spanish language, src/i18n/es/error.json
{
"GOOD_MORNING": "Buenos Dias",
"SUBSCRIPTION_EXPIRED": "Contacto {0.email} para soporte",
"SETUP": {
"WELCOME": "Bienvenidos {0.username}",
"BYE": "Adios {0.username}",
"TOMATO": {
"one": "{0.username} compra {0.count} tomate",
"other": "{0.username} compra {0.count} tomates",
"zero": "{0.username} compra {0.count} tomate"
}
},
"EXCEPTION": "No se {0.language}"
}
Copy i18n folder to dist/ in watch mode
Edit nest-cli.json to copy i18n folder to dist/ in watch mode
{
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"assets": [
{
"include": "i18n/**/*",
"watchAssets": true
}
]
}
}
Inject I18nModule to enable i18n support
imports: [
I18nModule.forRoot({
fallbackLanguage: 'en',
parser: I18nJsonParser,
parserOptions: {
path: path.join(__dirname, '/i18n/'),
watch: true,
},
resolvers: [new HeaderResolver(['language'])],
}),
],
fallbackLanguage property indicates that the fallback language is English.
parserOptions: {
path: path.join(__dirname, '/i18n/'),
watch: true,
}
watch: true
flag enables live translation and monitors i18n folder for changes in translations.
resolvers: [new HeaderResolver(['language'])],
looks up language in HTTP request header. If request header specifies language, the language value is used to translate message and return it back to the client. If request header does not contain language, we use the fallback language for translation.
Add i18n support in simple cases
We are going to add new endpoints in AppController to show various i18n examples.
Example 1: Display simple i18n message
@I18nLang decorator extracts the language from request header and passes the value to AppService.
First, we inject I18nService
in the constructor of AppService. Then, we call this.i18n.translate('error.GOOD_MORNING', { lang })
to obtain the value of 'GOOD_MORNING'
key in error.json. The filename of the translation file is error
; therefore, the prefix of the key is error. If the filename of the translation file is payment
, the message key will change to payment.GOOD_MORNING
.
Next, we use CURL to test the endpoint to obtain i18n messages
curl --location --request GET 'http://localhost:3001/good-morning' \
--header 'language: en'
Response displays
Good Morning
To obtain the Spanish message, we modify the header value to es
curl --location --request GET 'http://localhost:3001/good-morning' \
--header 'language: es'
Response displays
Buenos Dias
Example 2: Replace variable in i18n message by JSON object
In translations files, SUBSCRIPTION_EXPIRED is an i18n message that accepts email variable. As the result, we have to replace the variable with an actual email during translation to get back a meaningful text.
Message in i18n/en/error.json
"SUBSCRIPTION_EXPIRED": "Contact {0.email} for support"
Message in i18n/es/error.json
"SUBSCRIPTION_EXPIRED": "Contacto {0.email} para soporte"
To achieve variable replacement, we pass args that is an optional JSON object into the second argument of this.i18n.translate()
.
Retrieve the English message for error.SUBSCRIPTION_EXPIRED
curl --location --request GET 'http://localhost:3001/translated-message' \
--header 'language: en'
Response should be
Contact abc@example.com for support
Obtain the Spanish counterpart
curl --location --request GET 'http://localhost:3001/translated-message' \
--header 'language: es'
Response should be
Contacto abc@example.com para soporte
Display nested i18n message
Example 3: Display nested i18n message
Displaying nested message is similar to displaying other messages except the message key has multiple parts separated by periods.
"SETUP": {
"WELCOME": "Bienvenidos {0.username}",
"BYE": "Adios {0.username}"
},
In this example, we concatenate the message of error.SETUP.WELCOME
and error.SETUP.BYE
and return the combined text to the client side.
Similarly, when the structure is nested N levels deep, the message key is in the form of error.<level1>.<level2>.....<levelN>.key
The implementation of getNestedTranslationMessage
is identical to getTranslatedMessage
except the message keys are error.SETUP.WELCOME
and error.SETUP.BYE
respectively.
Retrieve the English message
curl --location --request GET 'http://localhost:3001/nested-message?username=John Doe' \
--header 'language: en'
Response should be
Welcome John Doe, Bye John Doe
Retrieve the Spanish message
curl --location --request GET 'http://localhost:3001/nested-message?username=John Doe' \
--header 'language: es'
Response should be
Bienvenidos John Doe, Adios John Doe
Support Pluralization in i18n message
Example 4: Pluralization in i18n message
When a message requires to show count, we want to provide the correct plural form. For example, “0 tomato”, “1 tomato” and “2 tomatoes”.
"SETUP": {
"TOMATO": {
"one": "{0.username} buys {0.count} tomato",
"other": "{0.username} buys {0.count} tomatoes",
"zero": "{0.username} buys {0.count}| tomato"
}
}
When count is 0, nestjs-i18n translates the message of error.SETUP.TOMATO.zero
. When count is 1, nestjs-i18n translates the message of error.SETUP.TOMATO.one
and the rest uses error.SETUP.TOMATO.other
. The keywords, “one”, “other” and “zero”, are mandatory in pluralize translation.
Retrieve the English message when the count of tomato is 0
curl --location --request GET 'http://localhost:3001/pluralize-message?username=John&numOfTimes=0' \
--header 'language: en'
Response should be
John buys 0 tomato
Retrieve the English message when the count of tomato is 1
curl --location --request GET 'http://localhost:3001/pluralize-message?username=John&numOfTimes=1' \
--header 'language: en'
Response should be
John buys 1 tomato
Retrieve the English message when count of tomato is 2
curl --location --request GET 'http://localhost:3001/pluralize-message?username=John&numOfTimes=2' \
--header 'language: en'
Response should be
John buys 2 tomatoes
Spanish messages yield similar results when language header switches to es
.
Output when count is 0
curl --location --request GET 'http://localhost:3001/pluralize-message?username=John&numOfTimes=0' \
--header 'language: es'
John compra 0 tomate
Output when count is 1
curl --location --request GET 'http://localhost:3001/pluralize-message?username=John&numOfTimes=1' \
--header 'language: es'
John compra 1 tomate
For count that is greater than 1, the output is
curl --location --request GET 'http://localhost:3001/pluralize-message?username=John&numOfTimes=1' \
--header 'language: es'
John compra 2 tomates
We have wrapped up the happy path where HTTP requests execute successfully and return a response with translated message. The next section describes bad HTTP request where endpoint throws an exception and our global filter intercepts the exception to translate the error message.
Translate HttpException message with i18n support
The first step is to define a global filter that catches all HttpException.
We cast errorResponse
as { key: string; args?: Record<string, any> } and call this.i18n.translate(error.key, { lang: ctx.getRequest().i18nLang, args: error.args })
to obtain the translated message. Finally, we call this.httpAdapterHost.httpAdapter.reply(ctx.getResponse(), responseBody, statusCode)
to return the formatted error response.
The second step is to register the global filter in AppModule
On the third step, we add “EXCEPTION” key and message in translation files
{
"EXCEPTION": "I don't know {0.language}"
}
{
"EXCEPTION": "No se {0.language}"
}
Next, we add a new endpoint that always throws BadRequestException. In the constructor of BadRequestException, the argument is an error object consisted of key and args.
Finally, we can verify the translated exception message with CURL
curl --location --request GET 'http://localhost:3001/bad-translated-exception' \
--header 'language: en'
The error response in English becomes
{
"statusCode": 400,
"message": "I don't know German",
"error": "I don't know German"
}
curl --location --request GET 'http://localhost:3001/bad-translated-exception' \
--header 'language: es'
The error response in Spanish becomes
{
"statusCode": 400,
"message": "No se German",
"error": "No se German"
}
Final thoughts
When a Nest application needs to support i18n, we install nestjs-i18n to send i18n messages and exceptions depending on language. The language can be provided in request header, cookie or GraphQL resolver, or a fallback language is used.
nestjs-i18n supports many use cases: plain message, variable replacement in message, nested message and pluralize translation. Moreover, it is a scalable solution since we can split messages by domains and languages to maintain a hierarchy of JSON files.
This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular, Nest and other technologies.
Resources:
- Github Repository: https://github.com/railsstudent/nesti18n-poc
- nestjs-i18n: https://github.com/ToonvanStrijp/nestjs-i18n