Tuesday, October 29, 2024

Simple Angular pipe for translation

Angular is a robust, front-end framework widely used for building dynamic web applications. Developed and maintained by Google, Angular provides a comprehensive suite of tools and features, from dependency injection to modular components, making it easier to create well-structured, scalable applications. One powerful feature of Angular is its ability to transform data directly within templates, thanks to pipes. Pipes are lightweight functions that can transform data, like formatting dates, currency, or other text content, in an efficient and reusable way.

The PipeTransform interface in Angular enables developers to create custom pipes that go beyond Angular's built-in transformations. This flexibility is particularly useful when working with tasks like translations, where dynamic text needs to be displayed in different languages depending on the user's preference. By implementing PipeTransform(https://angular.dev/guide/templates/pipes), developers can create a custom translation pipe that acts as a bridge between static text and translation services, allowing seamless localization without cluttering the template logic. This approach is not only clean and reusable but also keeps the application responsive and user-friendly for diverse audiences.

The Angular community already provides a library (ngx-translate) for text translation, but that library is quite large(304 kb in unpacked format!) and you might interested to create your own translation module to reduce the number of external dependencies and if you reach the end of this article you might realize that is easier than expected. :)

Tech stack

  • Angular 18

Inplementation

We will create the following parts:

  • i18n.json
  • TranslatorService
  • TranslatorPipe
  • TranslatorModule

Let's create a new project from scratch by running the following command: 

> ng new

 


Let's open the app.component.html file, and delete the content. We're going to add only a very simple HTML component:

<div>
  My greeting: <span>Hello</span>
</div>

Translation file

Let's define the translations in a JSON file:

{
    "en" : {
        "text.greeting": "Hello"
    },
    "de" : {
        "text.greeting": "Hallo"
    },
    "hu" : {
        "text.greeting": "Szia"
    }
}

Translator service

import { Injectable } from "@angular/core";
import * as i18n from '../../assets/i18n.json';

export const TRANSLATION_NOT_FOUND = 'Translation not found: $s';

@Injectable()
export class TranslatorService {

    public translate(key: string) : string {
        return this.getResolvedValue(key);
    }

    private getResolvedValue(key: string): string {
        return this.getLanguageMap()[key] ?? TRANSLATION_NOT_FOUND.replace("$s", key);
    }

    private getLanguageMap(): any  {
        return i18n[this.getLanguage() as keyof typeof i18n];
    }

    private getLanguage(): string {
        return localStorage.getItem('language') ?? 'en';
    }
}

NOTE: This implementation does not cover the scenario when we need custom arguments in a translation.

You might noticed that your IDE cannot resolve the JSON file:


"Cannot find module '../../assets/i18n.json'. Consider using '--resolveJsonModule' to import module with '.json' extension.ts(2732)"

In order to fix this error, you have to open the tsconfig.json configuration file, and add the following line into the "compilerOptions" object:

"resolveJsonModule": true

PipeTransform component

Create a new file called translator.pipe.ts under the same folder where you placed the service. This class has a single purpose: to return the value of the service. Of course you can do it in many ways, but personally I prefer to keep the business logic in a service:

import { Pipe, PipeTransform } from '@angular/core';
import { TranslatorService } from './translator.service';

@Pipe({
  name: 'translate'
})
export class TranslatorPipe implements PipeTransform {

  constructor(private readonly service: TranslatorService) { }

  transform(value: string): any {
    return this.service.translate(value);
  }
}

Module

You might noticed during the development the ng serve process did not refresh. This is because we just added the new application element, but did not wire into the application. The simplest way is to define a new module. If we define the pipe as a standalone, we have to import all other components one by one as well into each module/component where we use them. So, to make our life easier, let's create that module! :)

import { NgModule } from "@angular/core"; import { TranslatorService } from "../../../service/translator-service"; import { TranslatorPipe } from "./translator.pipe"; @NgModule({ declarations: [ TranslatorPipe ], providers: [ TranslatorService ], exports: [ TranslatorPipe ], imports: [ ] }) export class TranslatorModule { }

In the next step we have to add the recently created module into the AppModule:



Ok, so we have everything to use our new translator pipe! Let's open app.component.html again and change the code:

<div>
  My greeting: {{ 'text.greeting' | translate }}
</div>


Now let's set the language localStorage variable in the Developer Tools:



If you refresh the page, you will see the German version of the translation:


The entire source code is available on Github:

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.

Configure and use VSCode for Java web development

Embarking on Java web development often starts with choosing the right tools that streamline the coding process while enhancing productivity...