Guides Localization Translations

Translations

Elemental provides a reactive translation engine built with Angular Signals, allowing for dynamic updates to your application's text when the language changes or new translations are loaded. This guide covers how to use the translation functions, manage languages, and handle dynamic label replacements.

All translation functionalities are available from @elemental/common.

Core Concepts

The translation system revolves around a few key functions and concepts:

  1. translate(module, phrase, replacements?, delimiters?): This is the primary function. It returns an Angular Signal<string> that automatically updates with the correct translation whenever the current language changes, new translations are provided for that phrase, or any Signal-based replacements are updated.
  2. addTranslations(language, translations): This function is used to load your translation strings into the Elemental translation engine. Translations are organized by language, then by module, then by phrase.
  3. currentLanguage() and setLanguage(language): These allow you to get the current language Signal or set a new language for the entire Elemental environment, respectively.
  4. Label Replacements: The system supports dynamic placeholders in your translation strings (e.g., {userName}) and provides utilities to replace them with actual values.

Setting Up and Loading Translations

To make translations available to your application, you need to load them using addTranslations.

addTranslations(language: LmnLanguage, translations: LmnTranslations): void

  • language: The ISO code of the language for these translations (e.g., 'en', 'it', 'fr'). The LmnLanguage type is typically a string like 'en', 'es', etc.
  • translations: An object where keys are module names (strings), and values are objects where keys are the source phrases (in your base language, usually English) and values are their translations.

Example: Loading Translations

Typically, you would load translations for various languages when your application initializes or when a language pack is fetched.

//
// translations.service.ts or app.config.ts
//
import { addTranslations, LmnTranslations, LmnLanguage } from '@elemental/common';

const englishTranslations: LmnTranslations = {
  userProfile: {
    'Welcome back, {userName}!': 'Welcome back, {userName}!',
    'View Profile': 'View Profile',
  },
  course: {
    'Course completed': 'Course completed',
    'Start Learning': 'Start Learning',
  },
};

const italianTranslations: LmnTranslations = {
  userProfile: {
    'Welcome back, {userName}!': 'Bentornato, {userName}!',
    'View Profile': 'Vedi Profilo',
  },
  course: {
    'Course completed': 'Corso completato',
    // 'Start Learning' might not be translated yet for 'it' in this set
  },
};

// Call this during your application setup
export function loadAppTranslations() {
  addTranslations('en', englishTranslations);
  addTranslations('it', italianTranslations);
  // Add more languages as needed
}

Important: It's common for an integrating application (like an LMS) to have its own mechanism for fetching and loading a comprehensive set of translations. Elemental's addTranslations function is designed to integrate with such systems, allowing the application to feed its translations into the Elemental engine.

Using Translations in Your Components

The translate function is the core of consuming translations.

translate(module: string, phrase: string, replacements?: LmnLabelReplacements, variableDelimiters?: [string, string]): Signal<string>

  • module: A string identifying the module or domain of the phrase (e.g., 'userProfile', 'coursePlayer'). This helps organize translations.
  • phrase: The key for the translation, typically the original phrase in the base language (e.g., 'Welcome back, {userName}!').
  • replacements (optional): An object for dynamic values. Keys are placeholder names in the phrase, and values can be strings, numbers, or Signals of strings/numbers.
  • variableDelimiters (optional): An array of two strings defining the start and end delimiters for placeholders in the phrase. Defaults to ['{', '}'].

Basic Translation

To get a translated string, call translate and then access the value of the returned Signal.

//
// my-component.ts
//
import { Component } from '@angular/core';
import { translate } from '@elemental/common';

@Component({
  selector: 'app-my-component',
  template: `
    <h2>{{ pageTitle() }}</h2>
    <p>{{ welcomeMessage() }}</p>
  `,
})
export class MyComponent {
  // Assuming 'en' translations for 'common' module are loaded with:
  // { common: { 'My Page Title': 'My Page Title', 'Hello World': 'Hello World' } }
  pageTitle = translate('common', 'My Page Title');
  welcomeMessage = translate('common', 'Hello World');
}

If the current language changes (e.g., via setLanguage('it')) and Italian translations for 'common.My Page Title' are available, pageTitle() will automatically update. If a translation is not found for the current language, the original phrase string is returned.

Translations with Replacements

Often, translations include dynamic parts.

1. Using translate with replacements (Recommended for reactive updates):

If your replacement values are Signals, the translated string will update reactively when these Signals change.

//
// user-greeting.component.ts
//
import { Component, signal } from '@angular/core';
import { translate } from '@elemental/common';

@Component({
  selector: 'app-user-greeting',
  template: `<p>{{ greeting() }}</p>`,
})
export class UserGreetingComponent {
  userName = signal('Alice');
  // Assumes 'userProfile.Welcome back, {userName}!' is a loaded phrase
  greeting = translate('userProfile', 'Welcome back, {userName}!', { userName: this.userName });

  changeUser() {
    this.userName.set('Bob'); // greeting() will automatically update
  }
}

2. Using applyLabelReplacements for non-Signal or complex cases:

If you have translations with placeholders but the replacement values are not Signals, or if you need to apply replacements to an already translated Signal value, you can use applyLabelReplacements.

applyLabelReplacements(label: string, replacements: LmnLabelReplacements, replacementDelimiters?: [string, string]): string

//
// course-status.component.ts
//
import { Component, computed, signal } from '@angular/core';
import { translate, applyLabelReplacements } from '@elemental/common';

@Component({
  selector: 'app-course-status',
  template: `<p>{{ completionMessage() }}</p>`,
})
export class CourseStatusComponent {
  // Phrase: 'Congratulations, you finished {courseName} in {duration} minutes!'
  baseCompletionMessage = translate('course', 'Congratulations, you finished {courseName} in {duration} minutes!');
  courseName = 'Introduction to Elemental';
  durationMinutes = signal(45); // Duration might be reactive

  completionMessage = computed(() => {
    const translatedBase = this.baseCompletionMessage(); // Get current translation
    return applyLabelReplacements(translatedBase, {
      courseName: this.courseName,          // Static replacement
      duration: this.durationMinutes(),     // Reactive replacement (value extracted here)
    });
  });
}

Using Translations in Templates with the lmnReplace Pipe

For applying replacements directly in the template, especially when working with Signals, the lmnReplace pipe is very convenient.

LmnLabelReplacementPipe (lmnReplace)

<!--
  Assuming in your component's TypeScript:
  import { translate } from '@elemental/common';
  ...
  progressMessage = translate('course', 'Progress: {percentComplete}%');
  percentComplete = signal(0);
-->

<p>{{ progressMessage() | lmnReplace: { percentComplete: percentComplete() } }}</p>

The pipe will handle unwrapping Signal-based replacements.

Managing Language

The translation system reacts to changes in the current language.

  • currentLanguage(): Signal<LmnLanguage>: A read-only Signal representing the currently active language (e.g., 'en').
  • setLanguage(language: LmnLanguage): void: Function to change the application's current language. Calling this will trigger updates in all Signals created by the translate function.
//
// language-switcher.component.ts
//
import { Component } from '@angular/core';
import { currentLanguage, setLanguage, LmnLanguage } from '@elemental/common';

@Component({
  selector: 'app-language-switcher',
  template: `
    <p>Current language: {{ currentLang() }}</p>
    <button (click)="switchToItalian()">Switch to Italian</button>
    <button (click)="switchToEnglish()">Switch to English</button>
  `,
})
export class LanguageSwitcherComponent {
  currentLang = currentLanguage();

  switchToItalian() {
    setLanguage('it');
  }

  switchToEnglish() {
    setLanguage('en');
  }
}

Global Label Replacements

Sometimes, you might have placeholders that should be globally replaced across many translations (e.g., application name, user's company name).

  • globalLabelReplacements(): Signal<LmnLabelReplacements>: A read-only Signal of the currently defined global replacements.
  • setGlobalLabelReplacements(replacements: LmnLabelReplacements): void: Sets or updates the global replacements.
  • GLOBAL_LABEL_REPLACEMENT_DELIMITERS: [string, string]: The delimiters used for global replacements, which defaults to ['', ''] (meaning no delimiters, the key itself is replaced, e.g., AppName directly, not {{AppName}}).

The translate function automatically applies these global replacements after local ones.

Example:

//
// app.config.ts or similar setup file
//
import { setGlobalLabelReplacements } from '@elemental/common';

setGlobalLabelReplacements({
  AppName: 'Elemental Showcase App',
  SupportEmail: 'support@elemental.com'
});
//
// translations.ts (example loaded with addTranslations)
//
const englishTranslations: LmnTranslations = {
  footer: {
    'Contact SupportEmail for help.': 'Contact SupportEmail for help.',
    'Welcome to AppName!': 'Welcome to AppName!'
  }
};
//
// footer.component.ts
//
import { Component } from '@angular/core';
import { translate } from '@elemental/common';

@Component({
  selector: 'app-footer',
  template: `
    <p>{{ contactHelp() }}</p>
    <p>{{ appWelcome() }}</p>
  `,
})
export class FooterComponent {
  contactHelp = translate('footer', 'Contact SupportEmail for help.');
  appWelcome = translate('footer', 'Welcome to AppName!');
  // contactHelp() will become "Contact support@elemental.com for help."
  // appWelcome() will become "Welcome to Elemental Showcase App!"
}

Workflow Summary

  1. Load Translations: During application setup, use addTranslations to load all necessary language strings, typically organized by module. This might be done by a central translation management system in larger applications.
  2. Set Initial Language: Use setLanguage to establish the default language.
  3. Consume Translations:
    • In your components, use translate('module', 'phrase') to get a Signal for a static translated string.
    • For dynamic content, pass a replacements object to translate, ideally using Signals for reactive updates: translate('module', 'phrase with {param}', { param: mySignal }).
    • Alternatively, use the lmnReplace pipe in templates: {{ myTranslatedSignal() | lmnReplace: { param: myValue } }}.
    • Or use applyLabelReplacements in TypeScript if needed.
  4. Switch Language: When the user changes language, call setLanguage with the new language code. All translation Signals will update automatically.