Introduction
This project is a proof of concept (POC) to generate i18n pdf invoice using nestjs-i18n and html2pdf. In this use case, the i18n language comes from database instead of http request. It is because scheduled job generates and sends out invoices on a monthly basis that does not involve HTTP communication. After retrieving the language, it is stored in Continuation-Local Storage (CLS) namespace that is available in CLS context. Services can get the language from the context to perform translation and date localization to generate i18n pdf invoice.
Create a new nest application
nest new nest-pdfmake-poc
Download google font files that support English and Traditional Chinese
Navigate to https://fonts.google.com/noto/specimen/Noto+Sans+TC and download Noto Sans Traditional Chinese font files
Navigate to https://fonts.google.com/specimen/Roboto and download Roboto font files
Copy the font files to src/fonts folder
src/fonts
├── NotoSans
│ ├── NotoSansTC-Bold.otf
│ ├── NotoSansTC-Light.otf
│ ├── NotoSansTC-Medium.otf
│ └── NotoSansTC-Regular.otf
└── Roboto
├── Roboto-Italic.ttf
├── Roboto-Medium.ttf
├── Roboto-MediumItalic.ttf
└── Roboto-Regular.ttf
Add i18n support in nest
npm i --save nestjs-i18n
Create an i18n directory under src/ and create en
and hk
folders under src/i18n/. The folder names, en and hk, are locales of English and Chinese respectively.
i18n
├── en
│ └── invoice.json
└── hk
└── invoice.json
Create JSON translation files
English language, src/i18n/en/invoice.json
{
"invoice": {
"bill_to": "Bill To",
"date_of_issue": "Date of issue",
"payment_method": "Payment Method",
"credit_card": "Credit Card",
"description": "Description",
"unit_price": "Unit Price",
"quantity": "Quantity",
"total": "Total",
"total_amount": "Total Amount",
"title": "Invoice",
"page_number": "Page {currentPage} of {pageCount}"
}
}
Chinese language, src/i18n/hk/invoice.json
{
"invoice": {
"bill_to": "記賬到",
"date_of_issue": "簽發日期",
"payment_method": "付款方法",
"credit_card": "信用卡",
"description": "描述",
"unit_price": "單價",
"quantity": "數量",
"total": "金額",
"total_amount": "總金額",
"title": "發票",
"page_number": "第 {currentPage} 頁,共 {pageCount} 頁"
}
}
Copy i18n and fonts folders to dist/ in watch mode
Edit nest-cli.json to copy i18n and fonts folders to dist/ in watch mode. I18n json and font files are refreshed in dist/ folder without server restart
{
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"assets": [
{
"include": "fonts/**/*",
"watchAssets": true
},
{
"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,
},
}),
],
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.
Add dependencies to the project
Install dependencies to generate i18n pdf invoice
npm i html-to-pdfmake pdfmake jsdom date-fns
npm i --save-dev @types/html-to-pdfmake @types/jsdom @types/pdfmake
html-to-pdfmake
converts Html to pdfmake definition to render Pdf documents. pdfmake
library call generate pdf documents on the server or client side while date-fns
formats i18n date depending on locale.
Install dependencies of CLS hooked and other libraries
npm i cls-hooked cross-env class-transformer class-validator
npm i --save-exact lowdb@1.0.0
npm i --save-dev @types/cls-hooked @types/lowdb
cls-hooked allows developers to create CLS namespace to store variables in its CLS context. Any method that executes in the scope of the context can access the variables to perform their own logic until it terminates.
lowdb is a local JSON database that stores user profiles to generate pdf invoice based on the user language
Create user database to generate i18n pdf invoice
Create src/db.json and copy the file to dist/ in watch mode
// src/db.json
{
"users": [
{
"id": 1,
"name": "John Doe Customer",
"email": "john.doe@email.com",
"language": "en"
},
{
"id": 2,
"name": "Jane Doe Customer",
"email": "jane.doe@email.com",
"language": "hk"
}
]
}
nest-cli.json
{
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"assets": [
....
{
"include": "db.json",
"watchAssets": true
}
]
}
}
When user id is 1, the user language is English. When user id is 2, the user language is Chinese.
Use case of applying i18n translation to invoice and date
The use case is to retrieve user language from database and generate a pdf invoice. When language is “en”, we generate an English invoice with “MMM dd, yyyy” date format. Similarly, user receives Chinese invoice when language is “hk” and the invoice date has Chinese characters to represent year, month and day respectively.
English invoice
Chinese invoice
Architecture of the application
The architecture of the application has two modules: core module that is consisted of core services and invoices module that generates pdf invoice
src
├── core
│ ├── core.module.ts
│ ├── helpers
│ │ ├── cls-hook.helper.ts
│ │ ├── font.helper.ts
│ │ ├── i18n-date.helper.ts
│ │ └── translation.helper.ts
│ └── services
│ ├── date-fns.service.ts
│ ├── pdf.service.ts
│ └── translation.service.ts
├── invoice
│ ├── controllers
│ │ └── invoice.controller.ts
│ ├── invoice.module.ts
│ └── services
│ └── invoice.service.ts
└── main.ts
Responsibility of the components
- PdfService – A service that creates pdf document and streams to the bytes to client
- DateFnsService – A service that formats date based on locale
- TranslationService – A service that accesses language from cls context and translates message keys by language
- ClsHookHelper – A helper function that creates CLS namespace and sets the user language in the CLS context
- TranslationHelper – A helper that calls TranslationService to translate message keys
- FontHelper – A helper that determines the google font to use in pdf generation
- I18nDateHelper – A helper that uses the user language to return the locale and date format
- InvoiceController – A controller that routes http request to stream i18n Pdf invoice
- InvoiceService – A service that calls core services to generate pdf invoice with the correct language
Add PDF service to generate i18n pdf invoice
nest g mo core
nest g s core/services/pdf --flat
In order to inject pdfmake library in PdfService, we have to create a custom pdfmake provider in the module.
export const PDF_MAKER_SYMBOL = Symbol('PDF_MAKER')
Format date by locale and format string
nest g s core/services/dateFns --flat
The DateFnsService service has a formatDate
method that formats date object/milliseconds by format string and an optional locale
Add CLS hook helper and translation service
First, create a new CLS namespace to set arbitrary language and execute any callback
Second, use nest-cli to generate a TranslationService
nest g s core/services/translation --flat
TranslationService injects I18nService
of nestjs-18n to perform i18n translation.
getCurrentLanguage
finds the language in CLS context if it exists. Otherwise, the function returns the fallback language that is ‘en’
translate
assigns the current language to i18n option, passes it to i18nService
to perform translations.
Add other helpers
Since language is available in CLS context, we can define other helpers.
First, we define translation helper functions that return the user language and translate texts.
Then, we add a font helper function to return google font. The font keys of English and Chinese invoices are Roboto
and Noto
respectively.
Finally, the date helper provides locale and date format by the user language.
After the implementation of core module, we are ready to generate invoice module to generate i18n pdf invoice by user id
Generate invoice module for pdf generation
Similar to core module, we use nest-cli to generate invoice module, controller and service
nest g mo invoice
nest g co invoice/controllers/invoice --flat
nest g s invoice/services/invoice --flat
After scaffolding the boilerplate codes, we add a POST endpoint to generate invoice by user id
Next, we implement generate
that does not exist in InvoiceService
.
getUser
finds user data in the JSON database by user id
export interface User {
id: number
name: string
email: string
language: string
}
export interface DbSchema {
users: User[]
}
getInvoiceHtml
is the html codes of the dummy invoice
private getInvoiceTranslatedValues() {
const [
billTo,
dateOfIssue,
paymentMethod,
creditCard,
description,
unitPrice,
quantity,
total,
totalAmount,
title,
] = translates([
'invoice.invoice.bill_to',
'invoice.invoice.date_of_issue',
'invoice.invoice.payment_method',
'invoice.invoice.credit_card',
'invoice.invoice.description',
'invoice.invoice.unit_price',
'invoice.invoice.quantity',
'invoice.invoice.total',
'invoice.invoice.total_amount',
'invoice.invoice.title',
])
return {
billTo,
dateOfIssue,
paymentMethod,
creditCard,
description,
unitPrice,
quantity,
total,
totalAmount,
title,
}
}
Test i18n pdf invoice generation
Open a terminal and start nest server in development mode
npm run start:dev
Make a HTTP request to preview English invoice
curl --location --request POST 'http://localhost:3000/invoice/1' \
--data-raw ''
Make a HTTP request to preview Chinese invoice
curl --location --request POST 'http://localhost:3000/invoice/2' \
--data-raw ''
The static texts are in Chinese and the date, 2022年4月17日, is shown in Chinese format where
年 means year
月 means month
日 means day
This’s it. We have done a POC on i18n pdf invoice in Nest and user language controls what we actually see in the invoice. English invoice has English static texts and date format. Chinese invoice has Chinese static texts and date format.
Final thoughts
Another use case of nestjs-i18n is to render pdf invoice according to user preference. The use case retrieves the language from database and saves the value in a CLS namespace such that it is accessible by services and helpers that run in the CLS context. CLS context makes passing language to services and helpers very convenient and we can expand it to store additional variables when required.
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/nest-pdfmake-poc
- cls-hooked: https://github.com/jeff-lewis/cls-hooked
- nestjs-i18n: https://github.com/ToonvanStrijp/nestjs-i18n
- data-fns: https://date-fns.org/v2.28.0/docs/format
- lowdb: https://github.com/typicode/lowdb/tree/v1.0.0