Generate i18n pdf invoice in nestjs app with nestjs-i18n

Reading Time: 7 minutes

 128 total views

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.

let's go

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:

  1. Github Repository: https://github.com/railsstudent/nest-pdfmake-poc
  2. cls-hooked: https://github.com/jeff-lewis/cls-hooked
  3. nestjs-i18n: https://github.com/ToonvanStrijp/nestjs-i18n
  4. data-fns: https://date-fns.org/v2.28.0/docs/format
  5. lowdb: https://github.com/typicode/lowdb/tree/v1.0.0