Translation can drive you crazy, here's the cure!
The internationalization (i18n) library for Angular
π Clean and DRY templates
π΄ Support for Lazy Load
π Support for Multiple Languagues
π₯ Support for Multiple Fallbacks
π€ Support for Testing
π¦ Hackable
- Installation
- Transloco Config
- Translation in the Template
- Programmatical Translation
- Service API
- Lazy Load Translation Files
- Using Multiple Languages Simultaneously
- Custom Loading Template
- Hack the Library
- Prefetch the User Language
- Unit Testing
- Additional Functionality
- Plugins
- Comparison to other libraries
Install the library using Angular CLI:
ng add @ngneat/transloco
As part of the installation process you'll be presented with questions; Once you answer them, everything you need will automatically be created for you. Let's take a closer look at the generated files:
First, Transloco creates boilerplate files for the requested translations:
// assets/i18n/en.json
{
"hello": "transloco en",
"dynamic": "transloco {{value}}"
}
// assets/i18n/es.json
{
"hello": "transloco es",
"dynamic": "transloco {{value}}"
}
Next, it injects the TranslocoModule
into the AppModule
, and sets some default options for you:
// app.module
import { TRANSLOCO_CONFIG, TranslocoModule } from '@ngneat/transloco';
import { HttpClientModule } from '@angular/common/http';
import { httpLoader } from './loaders/http.loader';
import { environment } from '../environments/environment';
@NgModule({
imports: [TranslocoModule, HttpClientModule],
providers: [
httpLoader,
{
provide: TRANSLOCO_CONFIG,
useValue: {
prodMode: environment.production,
listenToLangChange: true,
defaultLang: 'en'
}
}
],
bootstrap: [AppComponent]
})
export class AppModule {}
Let's explain each one of the config
options:
listenToLangChange
: Subrscribes to the language change event, and allows you to change the active language. This is not needed in applications that don't allow the user to change the language in runtime (i.e., from a dropdown), so by setting it to false in these cases, you can save on memory by rendering the view once, and unsubscribing from the language changes event (defaults tofalse
).defaultLang
: Sets the default languagefallbackLang
: Sets the default language/s to use as a fallback. See theTranslocoFallbackStrategy
section if you need to customize it.failedRetries
: How many time should Transloco retry to load translation files, in case of a load failure (defaults to 2)prodMode
: Whether the application runs in production mode (defaults tofalse
).
It also injects the httpLoader
into the AppModule
providers:
import { HttpClient } from '@angular/common/http';
import { Translation, TRANSLOCO_LOADER, TranslocoLoader } from '@ngneat/transloco';
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class HttpLoader implements TranslocoLoader {
constructor(private http: HttpClient) {}
getTranslation(langPath: string) {
return this.http.get<Translation>(`/assets/i18n/${langPath}.json`);
}
}
export const httpLoader = { provide: TRANSLOCO_LOADER, useClass: HttpLoader };
The HttpLoader
is a class that implements the TranslocoLoader
interface. It's responsible for instructing transloco how to load the translation files. It uses Angular HTTP client to fetch the files, based on the given path (We'll see why it called path on the lazy load section).
Transloco provides three ways to translate your templates:
This is the recommended approach. It's DRY and efficient, as it creates one subscription per template:
<ng-container *transloco="let t">
<ul>
<li>{{ t.home }}</li>
<li>{{ t.alert | translocoParams: { value: dynamic } }}</li>
</ul>
</ng-container>
<ng-template transloco let-t>
{{ t.home }}
</ng-template>
You can use the read
input in your structural directive to get translations of a particular nested (including deeply nested) property.
Let's say you need to use the dashboard
scope all over the template. Given this translation object:
{
common: {
foo: 'Foo',
bar: 'Bar'
},
dashboard: {
title: 'Title',
desc: 'Desc'
}
}
you can do:
<ng-container *transloco="let t; read: 'dashboard'">
<h1>{{ t.title }}</h1>
<p>{{ t.desc }}</p>
</ng-container>
without having to repeat the dashboard
key in each translation.
<ul>
<li><span transloco="home"></span></li>
<li>
<span transloco="alert" [translocoParams]="{ value: dynamic }"></span>
</li>
<li><span [transloco]="key"></span></li>
</ul>
<span>{{ 'home' | transloco }}</span> <span>{{ 'alert' | transloco: { value: dynamic } }}</span>
<span [attr.alt]="'hello' | transloco">Attribute</span>
<span [title]="'hello' | transloco">Property</span>
Sometimes you may need to translate a key in a component or a service. To do so, you can inject the TranslocoService
and use its translate
method:
export class AppComponent {
constructor(private service: TranslocoService) {}
ngOnInit() {
// Please read the accompanying note before using it
this.service.translate('hello');
this.service.translate('hello', { value: 'world' });
this.service.translate(['hello', 'key']);
this.service.translate('hello', params, 'en');
this.service.translate<T>(translate => translate.someKey);
}
}
Note that in order to safely use this method, you are responsible for ensuring that the translation files have been successfully loaded by the time it's called. If you aren't sure, you can use the selectTranslate()
method instead:
this.service.selectTranslate('hello').subscribe(value => ...);
this.service.selectTranslate('hello', params, lang).subscribe(value => ...);
// When quering an object that should be transpiled
// For example: { a: { b: 'Hello {{ value }}', c: 'Hey {{ dynamic }}' }}
this.service.selectTranslate('a', {
'b': { value: '' },
'c': { dynamic: '' }
}).subscribe(obj => ...);
// Returns the active language translation
this.service.selectTranslation().subscribe(translation => ...);
// Load and returns the provided language
this.service.selectTranslation('es').subscribe(translation => ...);
getDefaultLang
- Returns the default languagesetDefaultLang
- Sets the default languagegetActiveLang
- Gets the current active languagesetActiveLang
- Sets the current active language
service.setActiveLang(lang);
getTranslation(lang?: string)
- Returns the selected language translation or, if a language isn't passed, all of them:
service.getTranslation();
service.getTranslation('en');
setTranslation()
: Manually sets a translations object to be used for a given language, setmerge
to true if you want to append the translations instead of replacing them.
service.setTranslation({ ... }); // defaults to current language
service.setTranslation({ ... }, 'es');
service.setTranslation({ ... }, 'en', { merge: false } );
setTranslationKey
- Sets the translated value of a key. If a language isn't specified in the third parameter, it sets the key value for the current active language:
service.setTranslationKey('key', 'value');
service.setTranslationKey('key.nested', 'value');
service.setTranslationKey('key.nested', 'value', 'en');
langChanges$
- Listens to the language change event:
service.langChanges$.subscribe(lang => lang);
events$
- Listens to the translation loading events:
service.events$.pipe(filter(e => e.type === 'translationLoadSuccess')).subscribe(payload => payload.lang);
service.events$.pipe(filter(e => e.type === 'translationLoadFailure')).subscribe(payload => payload.lang);
load(lang)
- Load the given language, and add it to the service
service.load('en').subscribe();
Let's say we have a todos page and we want to create separate translation files for this page, and load them only when the user navigates there. First, we need to create a todos
folder (or whatever name you choose); In it, we create a translation file for each language we want to support:
ββ i18n/
ββ en.json
ββ es.json
ββ todos/
ββ en.json
ββ es.json
There are 3 levels of setting the translation scope:
- We can set it inside the lazy module module providers :
const routes: Routes = [
{
path: '',
component: TodosComponent
}
];
@NgModule({
declarations: [TodosComponent],
providers: [{ provide: TRANSLOCO_SCOPE, useValue: 'todos' }],
imports: [CommonModule, RouterModule.forChild(routes), TranslocoModule]
})
export class TodosModule {}
- We can set it in a component's providers:
@Component({
selector: 'my-comp',
templateUrl: './my-comp.component.html',
providers: [
{
provide: TRANSLOCO_SCOPE,
useValue: 'todos'
}
]
})
export class MyComponent {}
- We can set the
scope
input in thetransloco
structural directive:
<ng-container *transloco="let t; scope: 'todos';">
<h1>{{ t.todos.keyFromTodo }}</h1>
</ng-container>
Each one of these options tells Transloco to load the corresponding scope
based on the current language and merge it under the scope
namespace into the active language translation object.
For example, if the current language is en
, it will load the todos/en.json
file, and will set the response to be the following:
{
header: '',
login: '',
todos: {
submit: '',
title: ''
}
}
Now we can access each one of the todos
keys by using the todos
namespace:
{{ 'todos.title' | transloco }}
<span transloco="toods.submit"></span>
By default, the namespace will be the scope name (camel cased), but we can override it by using the config.scopeMapping
config:
{
provide: TRANSLOCO_CONFIG,
useValue: {
defaultLang: 'en',
scopeMapping: {
todos: 'customName'
}
}
}
Now we can access it through customName
instead of the original scope name (todos
in our case):
{{ 'customName.title' | transloco }}
<span transloco="customName.submit"></span>
Note that to use it in the current version (1.x.x), we need to set config.scopeStrategy
to shared
. In the next major release, it will be the default.
There are times you may need to use a different language in a specific part of the template, or in a particular component or module. This can be achieved in a similar way to the previous example, except here set the TRANSLOCO_LANG
provider either in lazy module providers list, the component providers or in the template.
Here's an example of setting it in a component's providers:
@Component({
selector: 'my-comp',
templateUrl: './my-comp.component.html',
providers: [
{
provide: TRANSLOCO_LANG,
useValue: 'es'
}
]
})
export class MyComponent {}
Using Angular's DI rules, this will ensure that the language in this component's template and all of its children's templates is es
.
Alternatively, here is how to use it directly in the template:
<ng-container *transloco="let t; lang: 'en'">
<p>Inline (en) wins: {{ t.home }}</p>
</ng-container>
Note that it will be used as the initial language. If you need it to be static, you can use the static
pipe: en|static
.
Transloco provides you with a way to define a loading template, that will be used while the translation file is loading.
Similarly to the previous examples, set the TRANSLOCO_LOADING_TEMPLATE
provider either in lazy module providers, component providers, in the template, or even in the app.module
itself (affecting the entire app). For example:
@Component({
selector: 'my-comp',
templateUrl: './my-comp.component.html',
providers: [
{
provide: TRANSLOCO_LOADING_TEMPLATE,
useValue: '<p>loading...</p>'
}
]
})
export class MyComponent {}
It can take a raw HTML value, or a custom Angular component.
Alternatively, here is how to use it directly in the template:
<ng-container *transloco="let t; loadingTpl: loading">
<h1>{{ t.title }}</h1>
</ng-container>
<ng-template #loading>
<h1>Loading...</h1>
</ng-template>
Transloco provides you with an option to customize each one of its buliding blocks. Here's a list of the things you can customize:
The loader provides you with the ability to override the default handling of translation file loading.
export class CustomLoader implements TranslocoLoader {
getTranslation(lang: string): Observable<Translation> | Promise<Translation> {
if(langInLocalStorage) {
return of(langFromStorage);
}
return ...
}
}
export const custom = {
provide: TRANSLOCO_LOADER,
useClass: CustomLoader
}
The interceptor provides you with the ability to manipulate the translation object before it is saved by the service.
export class CustomInterceptor implements TranslocoInterceptor {
preSaveTranslation(translation: Translation, lang: string): Translation {
return translation;
}
preSaveTranslationKey(key: string, value: string, lang: string): string {
return value;
}
}
export const custom = {
provide: TRANSLOCO_INTERCEPTOR,
useClass: CustomInterceptor
};
The preSaveTranslation
method is called before the translation is saved by the service, and the preSaveTranslationKey
is called before a new key-value pair is saved by the service.setTranslationKey()
method.
The transpiler is responsible for resolving the given value. For example, the default transpiler transpiles Hello {{ key }}
and replaces the dynamic variable key
based on the given params, or the translation object.
export class CustomTranspiler implements TranslocoTranspiler {
transpile(value: any, params, translation: Translation): any {
return ...;
}
}
export const custom = {
provide: TRANSLOCO_TRANSPILER,
useClass: CustomTranspiler
}
This handler is responsible for handling missing keys. The default handler calls console.warn()
with the key when config.isProdMode
is set to false
, and returns an empty string to use as a replacement for the missing key's value.
export class CustomHandler implements TranslocoMissingHandler {
handle(key: string, params: HashMap, config: TranslocoConfig) {
return '...';
}
}
export const custom = {
provide: TRANSLOCO_MISSING_HANDLER,
useClass: CustomHandler
};
Note: The missing handler is not supported when using structural directive.
The fallback strategy is responsible for loading the fallback translation file, when the selected active language has failed to load. The default behavior is to load the language set in the config.fallbackLang
, and set it as the new active language.
When you need more control over this functionality, you can define your own strategy:
export class CustomFallbackStrategy implements TranslocoFallbackStrategy {
getNextLangs(failedLang: string) {
return ['langOne', 'langTwo', 'langThree'];
}
}
export const custom = {
provide: TRANSLOCO_FALLBACK_STRATEGY,
useClass: CustomFallbackStrategy
};
The getNextLangs
method is called with the failed language, and should return an array containing the next languages to load, in order of preference.
We recommend pre-emptively fetching the userβs data from the server, including internationalization settings, and making it available to the components, before we allow the user to interact with them.
We want to ensure the data is available, because we donβt want to incur a bad user experience, such as jumpy content or flickering CSS.
Here's how you can achieve this using the APP_INITIALIZER
token:
import { APP_INITIALIZER } from '@angular/core';
import { UserService } from './user.service';
import { TranslocoService } from '@ngneat/transloco';
export function preloadUser(userService: UserService, transloco: TranslocoService) {
return function() {
return userService.getUser().then(({ lang }) => {
transloco.setActiveLang(lang);
return transloco.load(lang).toPromise();
}
};
}
export const preLoad = {
provide: APP_INITIALIZER,
multi: true,
useFactory: preloadUser,
deps: [UserService, TranslocoService]
};
This will make sure the application doesn't bootstrap before Transloco loads the translation file based on the current user's language.
You can read more about it in this article.
When running specs, we want to have the languages available immediately, in a synchronous fashion. Transloco provides you with a TranslocoTestingModule
, where you can pass the languages you need in your specs. For example:
import { TranslocoTestingModule } from '@ngneat/transloco';
import en from '../../assets/i18n/en.json';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
TranslocoTestingModule.withLangs({
en
}, translocoConfig?)
],
declarations: [AppComponent]
}).compileComponents();
}));
it('should work', function() {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('h1')).nativeElement.innerText).toBe('hello');
});
});
Note that in order to import JSON files, you need to configure the TypeScript compiler by adding the following properties in tsconfig.json
:
{
"resolveJsonModule": true,
"esModuleInterop": true
}
- You can point to specific keys in other keys from the same translation file. For example:
{
"alert": "alert {{value}} english",
"home": "home english",
"fromList": "from {{home}}"
}
So the result of service.translate('fromList')
will be: "from home english".
- You don't have to inject the service each time you need to translate a key. Transloco has an exported
translate()
function:
import { translate } from '@ngneat/transloco';
translate('someKey');
getBrowserLang()
- Returns the language code name from the browser, e.g. "en"getBrowserCultureLang()
- Returns the culture language code name from the browser, e.g. "en-US"
import { getBrowserLang, getBrowserCultureLang } from '@ngneat/transloco';
Transloco provides a schematics command that will help you with the migration process.
Feature | @ngneat/transloco | @ngx-translate/core | Angular i18n |
---|---|---|---|
Actively Maintained | β | β See here | β |
Runtime Lang Change | β | β | β |
listenToLangChange | β | β | β |
Schematics | β | β | β |
Custom Loading Template | β | β | β |
Multiple Languages Simultaneously | β | β * | β |
Lazy Load Translations | β | β * | β |
Multiple Fallbacks | β | β | β |
Hackable | β | β | β |
Testing | β | β External library | β |
Structural Directive | β | β | β |
Attribute Directive | β | β | β |
Pipe | β | β | β |
Ivy support | β | β See here | β |
Additional Functionality | β See here | β | β |
Pluralization | β Official Plugin | β External library | β |
(*) Works only by creating a new service instance and mark it as isolated, and it's not supported at the directive level.
- Messageformat (official)
- Persist Language (official)
- Persist Translations (official)
- Preload Languages (official)
For any questions or deliberations join our Gitter channel
Netanel Basal |
Shahar Kazaz |
Itay Oded |
Thanks goes to these wonderful people (emoji key):
Rustam π |
Colum Ferry π» π π€ |
Levent Arman Γzak π» |
Inbal Sinai π |
Lars Kniep π» π€ |
AleΕ‘ π» π€ |
Koala π |
Oleg Teterin π» |
This project follows the all-contributors specification. Contributions of any kind welcome!