The localization module is built on top of an open source ngx-translate library.
Date, currency, decimal number, pluralization and selection can be easily handled by built-in Angular pipes and by providing them a locale.
Multiple locales support, switchable at runtime
RTL (right-to-left) text direction support
Translations loader using a specific endpoint (external URL source such as a CMS for example) or defaulting to local translation bundles in the src/assets/locales folder of your application. The assets folder for bundles can be changed by configuration (via a CLI option).
Intelligent fallback support. For any unsupported language request, the localization service will try to fetch the translation bundle from the fallback locale map OR the first closest supported language. More information in the intelligent fallback section below.
Resource keys can be translated from templates (*.html
) via a pipe or directive as well as from typescript (*.ts
) via a service.
Support for resource keys with parameters.
Custom LocalizationConfiguration
can be injected from the application.
Ability to toggle translations on and off to help identify the key corresponding to a translation.
We provide an Angular module in @o3r/localization called LocalizationModule which comes with a translations loader.
In your AppModule
, you need to import the LocalizationModule
and TranslateModule
(from @ngx-translate/core). The LocalizationModule
could be imported calling its forRoot
method
with a custom configuration factory to specify the language of the application. This configuration is of type LocalizationConfiguration
and describes:
All the possible configuration options can be found in the LocalizationConfiguration
interface.
You can also import the LocalizationModule
without a configuration parameter in its forRoot
method. In this case, you will be provided with some default LocalizationConfiguration
.
// app.module.ts
import { registerLocaleData } from '@angular/common';
import localeAR from '@angular/common/locales/ar';
import localeEN from '@angular/common/locales/en';
import localeFR from '@angular/common/locales/fr';
import { TranslateModule } from '@ngx-translate/core';
import { LocalizationModule, translateLoaderProvider } from '@o3r/localization';
// ...
registerLocaleData(localeAR, 'ar-AR');
registerLocaleData(localeEN, 'en-GB');
registerLocaleData(localeFR, 'fr-FR');
export function localizationConfigurationFactory() {
return {
supportedLocales: ['en-GB', 'fr-FR', 'ar-AR'],
fallbackLocalesMap: {
'en-CA': 'en-GB',
'en-US': 'en-GB',
'de': 'fr-FR',
'zh': 'en-GB'
},
fallbackLanguage: 'en-GB',
bundlesOutputPath: environment.LOCALIZATION_BUNDLES_OUTPUT,
useDynamicContent: true,
debugMode: false
};
}
@NgModule({
imports: [
LocalizationModule.forRoot(localizationConfigurationFactory),
TranslateModule.forRoot({
loader: translateLoaderProvider,
}),
// ...
],
providers: [],
// ...
})
export class AppModule {}
If you leave the property endPointUrl
blank in the localization configuration, the loader will try to fetch translation bundles from your application's src/assets/assets-otter/i18n
folder.
You can specify a language in the configuration, it will be loaded at the bootstrap time of your application.
A language can be specified asynchronously by using the LocalizationService.useLanguage(...)
function.
LOCALIZATION_CONFIGURATION_TOKEN
with your custom factory and dependencies.// app.module.ts
import localeAR from '@angular/common/locales/ar';
import localeEN from '@angular/common/locales/en';
import localeFR from '@angular/common/locales/fr';
import { TranslateModule } from '@ngx-translate/core';
import { LOCALIZATION_CONFIGURATION_TOKEN, LocalizationModule, translateLoaderProvider } from '@o3r/localization';
// ...
registerLocaleData(localeEN, 'en-EN');
registerLocaleData(localeFR, 'fr');
registerLocaleData(localeAR, 'ar');
@NgModule({
imports: [
LocalizationModule,
TranslateModule.forRoot({
loader: translateLoaderProvider,
})
// ...
],
// ...
providers: [
{provide: LOCALIZATION_CONFIGURATION_TOKEN, useFactory: customCreateLocalizationConfiguration, deps: [YourServiceNeededByTheFactory]}
]
})
class AppModule {}
You may also need to set up your http server to accept CORS and update the connect-src
property of csp.json
in your application.
// configs/csp.json
{
"connect-src": "http://example.com"
}
Now we are ready to localize the components of the application.
You can directly generate a localized component with the following command:
Example :ng g component ComponentName --use-localization
The --use-localization
parameter triggers the localization-to-component
schematic, which adds the localization architecture to a component.
This parameter is only mandatory if it is not provided in the workspace configuration file (such as angular.json
, nx.json
or project.json
)
under the schematics
property, like this:
{
"schematics": {
"@o3r/core:component": {
// ...
"useLocalization": true
},
}
}
[!NOTE] The
Example :ngAdd
of the @o3r/localization package will set theuseLocalization
parameter to true for all schematic packages beginning with'@o3r/core:component*'
(including@o3r/core:component
,@o3r/core:component-presenter
,@o3r/core:component-container
).ng add @o3r/localization
You can also localize a component later with:
Example :ng g localization-to-component --path='path/to/the/component/class/file'
As mentioned above, the localization-to-component
schematic adds the localization architecture to a component. For an existing component, the schematic
first checks that the component does not already contain localization files or properties, otherwise an error is thrown.
[!NOTE] Later you can add a localization key to a component with the following command:
Example :ng g localization-key-to-component --path='path/to/the/component/class/file' --key='localizationKey' --value='Default value for localizationKey'
You can also do this using the Otter VSCode Extension that contains the
Otter: Add localization key to component
feature, which largely simplifies this command.
*.localization.json
)The JSON localization file defines an object of key/value pairs. Each value is a JSON object with the properties description
and defaultValue
.
Eventually, you can reference a global key via $ref
using a relative path to a global localization JSON file or in a different package in the dependencies.
The purpose of this file is to provide a default localization for a component so that the developer can start building pages using components without worrying about localization.
The *.localization.json
file specifies default values in English only.
{
"o3r-my-component-pres.somekey1": {
"description": "This is somekey1 description for translators",
"defaultValue": "This is my default value 1"
},
"o3r-my-component-pres.somekey2": {
"description": "This is somekey2 description for translators",
"defaultValue": "This is my default value 2"
},
"o3r-my-component-pres.someglobalkey1": {
"$ref": "../global.localization.json#/someglobalkey1"
},
"o3r-my-component-pres.someglobalkey2": {
"$ref": "@scope/common/global.localization.json#/someglobalkey2"
}
}
To ensure the uniqueness of localization keys, it's a good idea to follow naming conventions. For components, we recommend prefixing each key by the component selector.
Hence, the naming convention for keys is the component selector followed by .
(dot character) and any string of your choice.
For example, if your SimpleHeaderPresComponent
has the selector o3r-simple-header-pres
, then your keys may look like this:
{
"o3r-simple-header-pres.airline": {
"description": "Airline name",
"defaultValue": "My Airline"
},
"o3r-simple-header-pres.motto": {
"description": "Airline motto phrase",
"defaultValue": "Let's shape the future of travel"
},
"o3r-simple-header-pres.locWithArg": {
"description": "Hello message to the user (passed as a parameter)",
"defaultValue": "Hello, { user }!"
}
}
For VSCode users, we provide an Otter extension that allows to create localization keys (and their corresponding descriptions) directly from some selected text in the component's template.
More details on how to do this in the documentation.
*.translation.ts
)The translation file is used to define the localization variables used by the component template.
It typically defines an interface which extends Translation
from @o3r/core
with all possible variable names used by your component template.
It also exports a constant (translations
) that satisfies the above contract. The values for each property are localization keys (real keys from the localization bundle).
import {Translation} from '@o3r/core';
export interface MyComponentPresTranslation extends Translation {
prop1: string;
prop2: string;
}
export const translations: MyComponentPresTranslation = {
prop1: 'o3r-my-component-pres.somekey1',
prop2: 'o3r-my-component-pres.somekey2'
};
For the SimpleHeaderPresComponent
example, the translation file could look something like this:
import type { Translation } from '@o3r/core';
export interface SimpleHeaderPresTranslation extends Translation {
/** Airline name */
airline: string;
/** Airline motto phrase */
motto: string;
/** Hello message to the user (passed as a parameter) */
locWithArg: string;
}
export const translations: SimpleHeaderPresTranslation = {
airline: 'o3r-simple-header-pres.airline',
motto: 'o3r-simple-header-pres.motto',
locWithArg: 'o3r-simple-header-pres.locWithArg'
};
Now to localize your component, you will start by implementing the Translatable
interface which forces you to declare the translations
property.
This property requires 2 decorators (@Input()
and @Localization(url)
, with url
corresponding to the relative path of the localization file).
This will let you override localization keys from the template and give your component some default localization if you don't have your own to start with.
import { Component, Input } from '@angular/core';
import { Localization, LocalizationModule, Translatable } from '@o3r/localization';
import { SimpleHeaderPresTranslation, translations } from './simple-header-pres.translation';
@Component({
selector: 'o3r-simple-header-pres',
imports: [LocalizationModule],
styleUrls: ['./simple-header-pres.style.scss'],
templateUrl: './simple-header-pres.template.html'
})
export class SimpleHeaderPresComponent implements Translatable<SimpleHeaderPresTranslation>, ... {
/**
* Localization of the component
*/
@Input()
@Localization('./simple-header-pres.localization.json')
public translations: SimpleHeaderPresTranslation = translations;
}
We can now use the translations
property (declared in the component file) in the template.
o3rTranslate
pipe with/without parameters as follows:<!-- output: "Let's shape the future of travel" -->
{{ translations.motto | o3rTranslate }}
<!-- use binding for a resource with HTML markup -->
<span [innerHTML]="translations.airline | o3rTranslate"></span>
<!-- output: "Hello, otter friend!" -->
{{ "o3r-simple-header-pres.locWithArg" | o3rTranslate: {user: 'otter friend'} }}
[!NOTE] The code to the
o3rTranslate
pipe can be found in the tools of the @o3r/localization module.
ng run project-name:i18n
It will create an i18n
folder in the component folder, with an en-GB.json
file containing the mapping between the localization key and the associated default value.
Check the list of options available by running:
Example :ng run project-name:i18n --help
ng run project-name:generate-translations
It will create the application bundle for each supported language.
Check the list of options available by running:
Example :ng run project-name:generate-translations --help
Use Angular's built-in DatePipe, DecimalPipe and CurrencyPipe pass the current locale as the last parameter.
The locale is read from this.localizationService.getCurrentLanguage()
. To be able to use this, your component must import the LocalizationModule
. You will then be able to inject the LocalizationService
in the component in order to get the current language.
import { Component, inject, Input } from '@angular/core';
import { Localization, LocalizationModule, LocalizationService, Translatable } from '@o3r/localization';
import { SimpleHeaderPresTranslation, translations } from './simple-header-pres.translation';
@Component({
selector: 'o3r-simple-header-pres',
imports: [LocalizationModule],
styleUrls: ['./simple-header-pres.style.scss'],
templateUrl: './simple-header-pres.template.html'
})
export class SimpleHeaderPresComponent implements Translatable<SimpleHeaderPresTranslation>, ... {
// ...
private readonly localizationService = inject(LocalizationService);
public readonly currentLanguage = toSignal(
this.localizationService.getTranslateService().onLangChange.pipe(map(({ lang }) => lang)),
{ initialData: this.localizationService.getCurrentLanguage() }
)
/**
* Called upon language change to set current language
* @param language
*/
public useLanguage(language: string) {
this.localizationService.useLanguage(language);
}
}
Now we can start using pipes:
Example :<p>{{today | date: 'fullDate'}}</p>
<p>{{1.5487 | number: ''}}</p>
<p>{{1.5487 | currency: 'EUR' : 'symbol' : ''}}</p>
[!NOTE] A locale parameter can be added to the pipe, such as the following:
Example :<p>{{today | date: 'fullDate' : '' : currentLanguage()}}</p>
However, the format of the pipes is based on the current locale so adding the
Example :currentLanguage()
parameter is optional. Another locale can be added in order to override the current one, for example:<p>{{1.5487 | currency: 'CAD' : 'symbol' : '' : 'fr-FR'}}</p>
A container can have its own translations (for error messages for example). In this case, the container becomes Translatable
as follows:
import { Translatable } from '@o3r/localization';
import { MyContTranslation } from '../my.translation.ts';
@Component({
selector: 'o3r-my-cont',
templateUrl: './my.template.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyContComponent implements Translatable<MyContTranslation>, Block {
/**
* Localization of the component
*/
@Input()
@Localization('./my.localization.json')
public translations: MyContTranslation;
}
There is also a possibility for the container to override the presenter's translations and pass them as input to the presenter.
// app.component.ts
import { inject } from '@angular/core';
import { LocalizationService } from '@o3r/localization';
// Inject LocalizationService which will take care of configuring TranslateService using LocalizationConfiguration and call configure() method
constructor() {
inject(LocalizationService).configure();
}
The TextDirectionService
has to be injected in app.component.ts
as follows:
// app.component.ts
import { inject } from '@angular/core';
import { TextDirectionService } from '@o3r/localization';
private readonly textDirectionService = inject(TextDirectionService);
public ngOnInit() {
this.subscriptions.push(this.textDirectionService.onLangChangeSubscription());
}
To be able to handle a large amount of ICU translations, a lazy compiler is provided in the @o3r/localization
package.
// app.module.ts
import localeAR from '@angular/common/locales/ar';
import localeEN from '@angular/common/locales/en';
import localeFR from '@angular/common/locales/fr';
import { TranslateCompiler, TranslateModule } from '@ngx-translate/core';
import {
LocalizationModule,
MESSAGE_FORMAT_CONFIG,
TranslateMessageFormatLazyCompiler
} from '@o3r/localization';
// ...
registerLocaleData(localeEN, 'en-EN');
registerLocaleData(localeFR, 'fr');
registerLocaleData(localeAR, 'ar');
@NgModule({
imports: [
// ...
LocalizationModule.forRoot({ ... }),
TranslateModule.forRoot({
// ...
compiler: {provide: TranslateCompiler, useClass: TranslateMessageFormatLazyCompiler}
})
],
providers: [
// Optional configuration:
{provide: MESSAGE_FORMAT_CONFIG, useValue: {enableCache: false}}
]
})
class AppModule {}
[!NOTE] The token
MESSAGE_FORMAT_CONFIG
implements theLazyMessageFormatConfig
interface from@o3r/localization
.
For pluralization, we are using the TranslateMessageFormatCompiler
, coming from the ngx-translate-messageformat-compiler package, which is a compiler for ngx-translate
that uses messageformat.js
to compile translations using ICU syntax for handling pluralization and gender.
ICU Message Format is a standardized syntax for dealing with the translation of user-visible strings into various languages that may have different requirements for the correct declension of words (e.g. according to number, gender, case) - or to simplify: pluralization.
Simple pluralization rules like 0, 1 or other work well for English, but may not be suitable for many other languages (Eastern European languages, Asian languages) where pluralization rules are much more complex. If this does not meet your requirements, we recommend reformulating your text so that you do not need to use pluralization. Example: instead of saying 'You have added 2 bags' you may want to say 'Number of bags: 2', which should suit most languages, no matter which number is considered plural.
You need to configure the TranslateModule
for it to use TranslateMessageFormatCompiler
as a compiler. We will use TranslateMessageFormatLazyCompiler
, which is an Otter extension of the base compiler. See the Lazy Compiler for ICU Translation support section above for details.
// app.module.ts
import { TranslateCompiler, TranslateModule } from '@ngx-translate/core';
import { MESSAGE_FORMAT_CONFIG, TranslateMessageFormatLazyCompiler } from '@o3r/localization';
// ...
@NgModule({
imports: [
TranslateModule.forRoot({
// ...
compiler: {
provide: TranslateCompiler,
useClass: TranslateMessageFormatLazyCompiler
}
}),
// ...
],
providers: [
// Optional compiler configuration:
{provide: MESSAGE_FORMAT_CONFIG, useValue: {locales: ['ar', 'fr']}}
]
})
class AppModule {}
{
// the key has to be defined in the localization bundle
"o3r-list-inline-messages-pres.nbOfErrors": "{count, plural, =0{No errors} one{# error} other{# errors}}"
// where 'count' is the parameter received
}
<!-- component HTML template -->
</span> {{translations.nbOfErrors | o3rTranslate: {count: countMessages} }}
The value of translations.nbOfErrors
is the translation key o3r-list-inline-messages-pres.nbOfErrors
. The next step translates the key by passing some parameters to the translate pipe.
The output will be:
No errors
if countMessages
is 01 error
if countMessages
is 1'Value of countMessages' errors
if countMessages
is greater than 1 (ex: 10 errors
)Sometimes you may want to display a different resource based on the value of a property that is not a number.
Example :{
// the key has to be defined in the localization bundle
"global.people": "{gender, select, male{He is} female{She is} other{They are}} {how}"
// where 'gender' is the parameter used for choice and 'how' it's a parameter used only for display
}
<!-- in component HTML -->
<ul>
<li>{{ translations.people | o3rTranslate: { gender: 'female', how: 'influential' } }}</li>
<li>{{ translations.people | o3rTranslate: { gender: 'male', how: 'funny' } }}</li>
<li>{{ translations.people | o3rTranslate: { how: 'affectionate' } }}</li>
</ul>
Note again that translations.people
matches the global.people
key:
- She is influential
- He is funny
- They are affectionate
The localization service supports a fallback strategy described in the details below.
fallbackLocalesMap
and supportedLocales
In case fallbackLocalesMap
is provided and the targeted translation language is unavailable in the list of supported locales,
the first priority goes to fallbackLocalesMap
to see if the targeted translation language can be mapped with the configured fallback map.
The second priority goes to supportedLocales
to match the targeted translation language to the first closest possible language, the locale may be different.
If none match, it will fall back to the default language.
Let's assume:
Example :return {
// ...
supportedLocales: ['en-GB', 'en-US', 'fr-FR', 'ar-AR'],
fallbackLocalesMap: {
'en-CA': 'en-US',
'fr-CA': 'fr-FR',
'de-CH': 'ar-AR',
'de': 'fr-FR',
'it': 'fr-FR',
'hi': 'en-GB',
'zh': 'en-GB'
},
fallbackLanguage: 'ar-AR'
}
Fallback scenarios:
en-CA
fallbacks to en-US
, as direct mapping available in fallback locales map.de-CH
fallbacks to ar-AR
, as direct mapping available in fallback locales map.de-AT
fallbacks to fr-FR
, as language mapping available in fallback locales map.zh-CN
fallbacks to en-GB
, as language mapping available in fallback locales map.en-AU
fallbacks to en-GB
, as fallback locales mapping unavailable, first nearest language available in supported locales.fr-BE
fallbacks to fr-FR
, as fallback locales mapping unavailable, first nearest language available in supported locales.bn-BD
fallbacks to ar-AR
, as it is the default fallback.supportedLocales
In case fallbackLocalesMap
is not provided and the targeted translation language is unavailable in the list of supported locales,
the targeted translation language will be compared to the supported locales to find the first closest match, the locale may be different.
If none match, it will fall back to the default language.
Let's assume:
Example :return {
// ...
supportedLocales: ['en-GB', 'fr-FR', 'fr-CA', 'ar-AR'],
fallbackLanguage: 'en-GB'
}
Fallback scenarios:
en-US
fallbacks to en-GB
, as en-GB has the same language with a different region.fr-BE
fallbacks to fr-FR
, as fr-FR is first in the supported locales list.it-IT
fallbacks to en-GB
, as it is the default fallback.To make it easier to identify which key corresponds to a given text, the LocalizationService
exposes a function toggleShowKeys()
that can be called to deactivate or reactivate the translation mechanism at runtime.
While deactivated, the translate
pipe and directive will output the translation keys instead of their resolved values.
[!NOTE] This mechanism only applies to the pipe and directive exported by Otter's
LocalizationModule
. The original ones fromngx-translate
do not support it.
First, this mechanism has to be activated via the LocalizationConfiguration
that you can provide in your AppModule
.
[!IMPORTANT] This is mainly for performance reasons, as it adds a new subscription to every
translate
pipe and directive in order to know when translations are turned on or off. Not enabling it prevents all these subscriptions and should be the baseline for a production environment.
Example:
Example :// app.module.ts
export function localizationConfigurationFactory(): LocalizationConfiguration {
return {
...DEFAULT_LOCALIZATION_CONFIGURATION,
supportedLocales: ['en-GB', 'fr-FR', 'ar-AR'],
fallbackLocalesMap: {
'en-CA': 'en-GB',
'en-US': 'en-GB',
'de': 'fr-FR',
'zh': 'en-GB'
},
fallbackLanguage: 'en-GB',
bundlesOutputPath: environment.LOCALIZATION_BUNDLES_OUTPUT,
useDynamicContent: true,
// dummy example
enableTranslationDeactivation: window.location.search.indexOf('debug=true') >= 0
};
}
@NgModule({
imports: [LocalizationModule],
providers: [{provide: LOCALIZATION_CONFIGURATION_TOKEN, useFactory: localizationConfigurationFactory}]
})
export class AppModule {}
Example of usage in a debug component:
Example :// Component class
@Component({
selector: 'debug',
templateUrl: './debug.template.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DebugComponent {
constructor(private localizationService: LocalizationService) {}
public toggleTranslation() {
this.localizationService.toggleShowKeys();
}
}
<!-- Component template -->
<div>
<h3>Debug:</h3>
<button (click)="toggleTranslation()">Toggle translation</button>
</div>
<key> - <translation>
You can enable debug mode by setting debugMode
property of LocalizationConfiguration
to true (the default value is false).
registerLocaleData(localeEN, 'en-EN');
registerLocaleData(localeFR, 'fr');
registerLocaleData(localeAR, 'ar');
@NgModule({
imports: [
LocalizationModule.forRoot({
language: 'fr',
endPointUrl: 'http://example.com',
bundlesOutputPath: config.LOCALIZATION_BUNDLES_OUTPUT,
supportedLocales: ['en-EN', 'fr', 'ar'],
debugMode: true
})
// ...
]
})
This way, all your translations will be prefixed by the corresponding localization key, so you can easily map the text to the key.
Examples:
simpleHeader.airline - My Airline
simpleHeader.motto - Let's shape the future of travel
The Otter framework provides a Chrome Extension to help debug the application. To enable the communication with the Otter Devtools, the two following steps are required:
import { LocalizationDevtoolsModule } from '@o3r/localization';
@NgModule({
imports: [
// ...,
LocalizationDevtoolsModule
]
})
export class AppModule {}
LocalizationDevtoolsMessageService
in the main.ts
file of the application (we recommend doing this to avoid polluting the app.component.ts
file):// main.ts
import { inject, runInInjectionContext } from '@angular/core';
import '@angular/localize/init';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { LocalizationDevtoolsMessageService } from '@o3r/localization';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule).then((m) => runInInjectionContext(m.injector, () => inject(ConfigurationDevtoolsConsoleService)))
You can do this in the main.ts
file:
// main.ts
platformBrowserDynamic().bootstrapModule(AppModule).then((m) => runInInjectionContext(m.injector, () => inject(ConfigurationDevtoolsConsoleService).activate()));
Or in the AppModule
:
// app.module.ts
import { OTTER_LOCALIZATION_DEVTOOLS_OPTIONS } from '@o3r/localization';
@NgModule({
// ...
providers: [
// ...
{provide: OTTER_LOCALIZATION_DEVTOOLS_OPTIONS, useValue: {isActivatedOnBootstrap: true}}
]
})
export class AppModule {}
[!NOTE] Get more details on Otter Chrome DevTools session.