Abstract text content in Angular

mirzaleka

Mirza Leka

Posted on September 8, 2024

Abstract text content in Angular

If you're developing websites, you're likely writing a lot of text in the component templates:

batman-component

Writing text like this is not broken or buggy but rather tiresome to maintain for long-running projects. To improve this, you can create a file containing all the text for a particular feature and reuse it throughout the app by importing the correct keys.

This file can be:

  • JSON file
  • TypeScript file

I'll describe the pros and cons of both approaches.

I) JSON translations file

(1) Create a JSON file

In the root directory of your project, go to src/assets and create a new folder (wording) and a JSON file (wording.json):

šŸ“ src 
|__ šŸ“ assets
|_____ šŸ“ wording
|_______ wording.json
Enter fullscreen mode Exit fullscreen mode

And add your translations:

{
  "APP": {
    "TITLE": "Movies App",
    "DESCRIPTION": "The best site for movies"
  },
  "COMMON": {
    "BUTTON": "Peek In"
  },
  "MOVIES": {
    "BATMAN": {
      "TITLE": "Batman",
      "SERIES": {
        "THE_DARK_KNIGHT": {
          "TITLE": "The Dark Knight Series",
          "MOVIES": {
            "BATMAN_BEGINS": {
              "TITLE": "Batman Begins",
              "DESCRIPTION": "Bruce learns the art of fighting to confront injustice."
            },
            "THE_DARK_KNIGHT": {
              "TITLE": "The Dark Knight",
              "DESCRIPTION": "Lorem Ipsum"
            },
            "THE_DARK_KNIGHT_RISES": {
              "TITLE": "The Dark Knight Rises",
              "DESCRIPTION": "Lorem Ipsum"
            }
          }
        }
      }

    }
  }
}
Enter fullscreen mode Exit fullscreen mode

(2) Update TSConfig

If needed, add resolveJsonModule: true to tsconfig.json compilerOptions to allow importing JSON files into ECMAScript modules:

{
  "compilerOptions": {
    "resolveJsonModule": true, // Add this line to tsconfig.json
  }
}
Enter fullscreen mode Exit fullscreen mode

(3) Use the JSON file

Import the file directly into the component

// component file
import wording from '../../assets/wording/wording.json';

@Component({...}) 
export class HomeComponent implements OnInit {

  public pageText!: any;

  ngOnInit(): void {
    this.pageText = wording.MOVIES.BATMAN;
  }

}
Enter fullscreen mode Exit fullscreen mode

Or create a service that imports all wording globally:

// translations.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class TranslationsService {

  public wording!: any;

  constructor(private http: HttpClient) {
    this.setTranslations();
  }

  private setTranslations() {
    this.http.get('./assets/wording/wording.json').subscribe(data => {
      this.wording = data;
     });
  }

}

Enter fullscreen mode Exit fullscreen mode

And then inject the service into your component.

@Component({...}) 
export class HomeComponent implements OnInit {

  public pageText!: any;

  constructor(private readonly translationsService: TranslationsService) {}

  ngOnInit(): void {
    this.pageText = this.translationsService.wording.MOVIES.BATMAN;
  }

}
Enter fullscreen mode Exit fullscreen mode

However, the downside of this approach is that you don't have any intellisense for the text content.

  <div class="movie-main-container">
    <span class="heading0">{{ pageText.TITLE }}</span>
    <!-- ..............No complaints here šŸ‘‡ -->
    <div class="heading4">{{ pageText.HELLO_WORLD }}</div>
  </div>
Enter fullscreen mode Exit fullscreen mode

To resolve this, you'd have to build a custom type or an interface around the whole wording.json file or the particular object ("Batman") you're using in the component.

II) TypeScript translations file

Another way to do this is to ditch the JSON file and create a Typescript file instead.

(1)

Create new wording.ts file anywhere in the src/app

// wording.ts
const WORDING = {
  APP: {
    TITLE: 'Movies App',
    DESCRIPTION: 'The best site for movies',
  },
  COMMON: {
    BUTTON: 'Peek In',
  },
  MOVIES: {
    BATMAN: {
      TITLE: 'Batman',
      SERIES: {
        THE_DARK_KNIGHT: {
          TITLE: 'The Dark Knight Series',
          MOVIES: {
            BATMAN_BEGINS: {
              TITLE: 'Batman Begins',
              DESCRIPTION:
                'Bruce learns the art of fighting to confront injustice.',
            },
            THE_DARK_KNIGHT: {
              TITLE: 'The Dark Knight',
              DESCRIPTION: 'Lorem Ipsum',
            },
            THE_DARK_KNIGHT_RISES: {
              TITLE: 'The Dark Knight Rises',
              DESCRIPTION: 'Lorem Ipsum',
            },
          },
        },
      },
    },
  },
};

export default WORDING;
Enter fullscreen mode Exit fullscreen mode

(2) Create a class that reads from this file

You could import a new wordings.ts file in any desired component. However, I like to create a custom (UseWording) class that reads from this file.

// use-wording.ts

import WORDING from './wording';

/**
 * Wrapper for translation wording
 */
export default class UseWording {

  get useWording() {
    return WORDING
  }
}
Enter fullscreen mode Exit fullscreen mode

(3) Inerit the UseWording class in your components

import { Component } from '@angular/core';
import UseWording from '../../../shared/translations/use-wording';

@Component({...})
export class HomeComponent extends UseWording {
  readonly pageText = this.useWording.MOVIES.BATMAN
}
Enter fullscreen mode Exit fullscreen mode

With this, you can immediately see the intellisense in the template.

template-intellisense

Additionally, you can create more class properties that target specific keys in the wording object:

@Component({...})
export class HomeComponent extends UseWording {
  readonly pageText = this.useWording.MOVIES.BATMAN;
  readonly movies = this.useWording.MOVIES.BATMAN.SERIES.THE_DARK_KNIGHT.MOVIES;
  readonly common = this.useWording.COMMON;
}
Enter fullscreen mode Exit fullscreen mode
<div class="movie-main">
  <div class="movie-main-container">
    <span class="heading0">{{ pageText.TITLE }}</span>
    <div class="heading4">{{ pageText.SERIES.THE_DARK_KNIGHT.TITLE }}</div>
  </div>

  <div class="movie-main-cards">
    <div class="layout-centered">
      <div class="heading1">{{ movies.BATMAN_BEGINS.TITLE }}</div>
      <div class="heading4">
        {{ movies.BATMAN_BEGINS.DESCRIPTION }}
      </div>
      <button class="button-primary">{{ common.BUTTON }}</button>
    </div>
    <div class="layout-centered">
      <div class="heading1">{{ movies.THE_DARK_KNIGHT.TITLE }}</div>
      <div class="heading4">
        {{ movies.THE_DARK_KNIGHT.DESCRIPTION }}
      </div>
      <button class="button-primary">{{ common.BUTTON }}</button>
    </div>
    <div class="layout-centered">
      <div class="heading1">
        {{ movies.THE_DARK_KNIGHT_RISES.TITLE }}
        <div class="heading4">
          {{ movies.THE_DARK_KNIGHT_RISES.DESCRIPTION }}
        </div>
        <button class="button-primary">{{ common.BUTTON }}</button>
      </div>
    </div>
  </div>
</div>

Enter fullscreen mode Exit fullscreen mode

Note that if your class component injects dependencies via the constructor, the constructor must contain a 'super' call.

export class MyComponent extends UseWording {

  searchAccountsForm!: FormGroup;

  constructor(
    private fb: FormBuilder
  ) {
    super(); // <-- this is mandatory
  }
Enter fullscreen mode Exit fullscreen mode

And just like with JSON, if you need to change a title or a description, you do it in one place (wording.ts) instead of changing multiple files/components.

Wrapping up

This article demonstrates two ways of using wording in Angular components. Both methods have advantages and disadvantages.

Using the TypeScript file speeds things up and takes care of the intellisense, but it may not be suitable for working with multiple languages.

Using the JSON file requires some extra work, but it's beneficial when the exact translations are used across various apps built with different technologies (that support JSON format).

If you learned something new, don't forget to hit the follow button. Also, follow me on Twitter to stay updated with my upcoming content.

Bye for now šŸ‘‹

šŸ’– šŸ’Ŗ šŸ™… šŸš©
mirzaleka
Mirza Leka

Posted on September 8, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related