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
- 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>
<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>
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() {
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').subscribe(value => {}, 'es');
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 you have a todos page and you want to create separate translation files for this page, and load them only when the user navigates there. First, you need to create a todos
folder (or whatever name you choose); In it, create a translation file for each language you want to support:
โโ i18n/
โโ en.json
โโ es.json
โโ todos/
โโ en.json
โโ es.json
You have several ways of telling Transloco to use them, via setting the TRANSLOCO_SCOPE
provider based on Angular's DI rules.
You can set it the providers list of a lazy module:
const routes: Routes = [
{
path: '',
component: TodosComponent
}
];
@NgModule({
declarations: [TodosComponent],
providers: [{ provide: TRANSLOCO_SCOPE, useValue: 'todos' }],
imports: [CommonModule, RouterModule.forChild(routes), TranslocoModule]
})
export class TodosModule {}
You 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 {}
Or you can set the scope
input in the transloco
structural directive:
<ng-container *transloco="let t; scope: 'todos';">
<h1>{{ t.keyFromTodos }}</h1>
</ng-container>
Each one of these options tells Transloco to load the corresponding scope
based on the current language. For example, if the current language is en
, it will load the file todos/en.json
and set the response to be the context in this module, component, or template, respectively.
You can think of it as a virtual directory, todos/{langName}
, so when you need to use the API, you can refer to it as such, for example:
service.getTranslation(`todos/en`);
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>
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: string, params, translation: Translation): string {
return ...;
}
}
export const custom = {
provide: TRANSLOCO_TRANSPILER,
useClass: CustomTranspiler
}
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 the 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
})
],
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');
});
});
- 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');
Transloco provides a schematics command that will help you with the migration process.
Feature | @ngneat/transloco | @ngx-translate/core |
---|---|---|
Multiple Languages | โ | โ |
Lazy load translation scopes | โ | โ |
Multiple Fallbacks | โ | โ |
Hackable | โ | โ |
Testing | โ | โ External library |
Structural Directive | โ | โ |
Attribute Directive | โ | โ |
Pipe | โ | โ |
Ivy support | โ | โ |
Additional Functionality | โ See here | โ |
Plugins | WIP | โ See here |
For any questions or deliberations join our Gitter channel
Thanks goes to these wonderful people (emoji key):
Netanel Basal ๐ ๐ผ ๐ป ๐ ๐จ ๐ ๐ก ๐ค ๐ ๐ง ๐ |
Shahar Kazaz ๐ป ๐ ๐ ๐ค ๐ ๐ง |
Itay Oded ๐ป ๐ค ๐ ๐ง |
This project follows the all-contributors specification. Contributions of any kind welcome!