Vay & Angular
Angular makes it hard to correctly integrate a strongly typed provider on a library level. However, integrating Vay yourself is fairly straightforward and requires only a service and a pipe.
Structuring the Project
Create a new directory i18n
inside your core directory / module. This directory will hold the service, as well as the provider and the dictionaries. Below you'll see a suggestion on how your setup might look like.
core
├─ localization
│ └─ core.localization.en.ts # A localization file
├─ services
│ ├─ i18n
│ │ ├─ i18n.dictionary.en.ts # A dictionary file
│ │ ├─ i18n.provider.ts # The provider
│ │ └─ i18n.service.ts # The service
│ └─ <other services>
├─ pipes
│ ├─ translation.pipe.ts # The pipe used for translating
│ └─ <other pipes>
└─ <other code>
INFO
You are free to structure your code as you like and as your project requires. The structure presented here is just a suggestion and best practice learned from sizeable projects.
There are 5 main building blocks used in this structure.
- Localization files: (
<domain>.localization.<lang>.ts
) Used for breaking down large dictionaries into domain specific sizeable chunks, that can be easily maintained. - Dictionary files: (
<domain>.dictionary.<lang>.ts
) Used to collect all localization files, and integrate them into the Vay dictionary system. - The Provider: (
services/i18n/i18n.provider.ts
) The configured Provider that will be used inside the service. - The Service: (
services/i18n/i18n.service.ts
) A Angular service that handles locale selection as well as translating. - The Pipe: (
pipes/translation.pipe.ts
) A Angular pipe used to translate tokens to phrases.
Providers, Dictionaries and localizations
You can set up Vay as describes in the getting started section.
- Create a file for the
Dictionary
andlocalizations
you want to use:
import { defineDictionary } from '@vayjs/vay';
import core from '../localizations/core.localization.en';
export default defineDictionary('en', {
core, // Add the localization object to the dictionary.
});
export default {
greeting: 'Hello, Vay & Angular.',
};
- Create the
Provider
:
import { createProvider, defineConfig, getDefaultBrowserLanguage } from '@vayjs/vay';
import dict_en from './i18n.dictionary.en';
export const provider = createProvider(
// Configure the provider as desired
defineConfig({ defaultLocale: getDefaultBrowserLanguage() }),
// Add the dictionaries you created
dict_en,
);
The I18nService
The core element of Vay's Angular integration is the I18nService
. The service controls the set locale, methods to set the locale, and holds the translation method that will be used by the pipe later.
import { EventEmitter, Inject, Injectable, DestroyRef, takeUntilDestroyed } from '@angular/core';
import { ISO639Code } from '@vayjs/vay';
import { BehaviorSubject } from 'rxjs';
import { provider } from './i18n.provider';
@Injectable({
providedIn: 'root',
})
export class I18nService {
private readonly destroyed$ = inject(DestroyRef);
private readonly provider = provider;
// The current locale
private readonly _locale = new BehaviorSubject(this.provider.getLanguage());
public locale$ = this._locale.asObservable();
constructor() {
// Subscribe to the locale to the set the language of the provider accordingly
this._locale.pipe(takeUntilDestroyed(this.destroyed$)).subscribe((locale) => {
this.provider?.setLanguage(locale);
});
}
// Reassign the translate method
translate = this.provider.translate;
setLanguage(locale: ISO639Code) {
// Change the locale if it has changed
if (this._locale.value !== locale) {
this._locale.next(locale);
}
}
getLanguage() {
return this._locale.value;
}
}
The Service is a simple wrapper for Vay's functionality. You can extend it in any way you like.
The Pipe
The pipe is then finally used to transform the tokens into a corresponding phrase. As Angular's pure pipes are memoized by default (meaning no rerenders on locale change), we need to use an impure pipe and implement the memoization part ourselves.
import { Pipe, inject, type PipeTransform, EventEmitter, DestroyRef, takeUntilDestroyed } from '@angular/core';
import { I18nService } from './i18n.service';
@Pipe({
// You can choose any name you want. We choose `t` because it's short.
name: 't',
// The pipe cannot be pure, as the input parameters won't change.
// We implement the memoization for performance improvements ourself.
pure: false,
// Depending on your setup, you might want to change this to false.
standalone: true,
})
export class TranslatePipe implements PipeTransform {
private readonly destroyed$ = inject(DestroyRef);
private readonly i18nProvider = inject(I18nService);
// Set up the properties used for memoization
private dirty = true;
private phrase: string | null = null;
private memoized: string = '[]';
constructor() {
// Subscribe to the locale to set the pipe to dirty.
this.i18nProvider.locale$.pipe(takeUntilDestroyed(this.destroyed$)).subscribe(() => {
this.dirty = true;
});
}
// The method will return true if the pipe should rerender
// Either because there is no phrase, the locale has changed,
// or the arguments have changed.
private isDirty(args: Parameters<(typeof this.i18nProvider)['translate']>) {
const serialized = JSON.stringify(args);
if (serialized !== this.memoized) {
this.memoized = serialized;
return true;
}
return this.dirty || this.phrase === null;
}
transform(...args: Parameters<(typeof this.i18nProvider)['translate']>) {
// Check if the pipe can return the stored phrase or needs
// to recalculate the phrase
if (!this.isDirty(args)) {
return this.phrase;
}
// Translate the token and store the phrase
this.phrase = this.i18nProvider.translate(...args);
this.dirty = false;
return this.phrase;
}
ngOnDestroy(): void {
this.terminated.emit();
}
}
Using the I18nService
The service can be used to control the locale. A simple setup could look like this:
@Component({
// Component meta data
})
export class LanguageComponent {
i18nService = inject(I18nService);
// Method to change the locale
setLanguage(locale: ISO639Code) {
this.i18nService.setLanguage(locale);
}
// Extract the locale$ Observable from the service
locale$ = this.i18nService.locale$;
}
<div>
<!-- Create buttons to change the language -->
<button (click)="setLanguage('en')">EN</button>
<button (click)="setLanguage('es')">ES</button>
<!-- Display the current locale -->
<span>{{ locale$ | async }}</span>
</div>
Using the TranslatePipe
With all elements created it's time to use the created pipe in one of our components. Generally speaking, there will be two different locations where you'll want to use the pipe. Classes and Templates.
Classes
You can use the pipe by directly injecting it into your component.
@Component({
// component meta data
})
export class LanguageComponent {
// Inject the pipe into your component
translate = inject(TranslatePipe);
// Use the pipe to translate a token
greeting = this.translate(`core.greeting`);
}
Templates
Using the pipe inside a template is straightforward, and works like using all other pipes. You can pass additional data to the pipe as usual. The strongly typed character should be preserved, making Vay a valuable part of your Angular application.
<div [title]="'core.greeting' | t">{{ 'core.greeting' | t }}</div>
@Component({
// Import the pipe if it's standalone, otherwise the module will handle it.
imports: [TranslatePipe],
})
export class LanguageComponent {
// Logic
}
Best practices
- Keep your localization files with your pages / components: This allows you to find and update phrases easier in case of requirement changes.
- Keep your localization files small: The larger the file, the harder it gets to maintain. In larger applications, maintainability is key to sustained growth.
- Namespace your Localization files per module: Giving your tokens a semantic structure makes it easier to infer tokens.
- Interpolation Mechanism: Vay's usual interpolation, pluralization and contextualization mechanisms work the same way in angular as they do in vanilla JavaScript. Use them to your advantage.