This is an Angular client for working with Translation on Demand servers. Translation on Demand refers to a low-latency client-server technique for retrieving trimmed translation dictionaries that load quickly and can be switched easily and at runtime.
What this library does:
- Provides a management mechanism for setting locale.
- Provides a robust mechanism for changing translations at runtime.
This library does NOT:
- Implement or provide a language switcher UI. Language/locale switching is limited to the
setLocale(locale)
function. - Enumerate or validate locales available on the ToD server. As a result, the library also does not preload locale dictionaries.
- Persist locale selection in cookies, local storage or session storage.
- Cache, as ToD is intended to obtain its performance from server-side optimizations.
At a low level within your Angular application (AppComponent
or your app's root component is best), configure the service by setting the urlTemplate
and locale
with the setUrlTemplate(urlTemplate)
and setLocale(locale)
functions, respectively. The URL template is used to build URLs that can build your locales.
The urlTemplate
within TranslationService is interpolated with two values: the current locale, {0}
and the current time as milliseconds, {1}
. Using the current time allows for implementations using cache busters, which can be useful during development and benchmarking.
The subscription associated with subscribeToLabels
does not unsubscribe automatically, nor does it ever complete. Please keep a reference to the subscriptions you create and unsubscribe to them in the ngOnDestroy
lifecycle hook. Consider using a library like subsink to make this a little easier.
import { TranslationService } from '@my-ul/tod-angular-client';
export class AppComponent implements OnDestroy
{
constructor(public translation: TranslationService)
{
/*
the format of the locale codes is not terribly important...but adhering to the IETF BCP 47 standard makes working with translations from other teams easier.
good places to get user's locale...
- `navigator.language`
- HTML lang attribute: `<html lang="">`
*/
const defaultLocale = getDefaultLocaleFromSomewhere() || "en-US";
// If these values are not set, TranslationService will not emit.
// If urlTemplate doesn't get set, an error will be thrown.
// adding ?t={1} will set an appropriate cache-buster; it can be omitted.
translation
.setUrlTemplate("https://my-tod-server.com/locales/RF_{0}.json?t={1}")
.setLocale(defaultLocale);
}
}
Once the TranslationService is initialized, it can be used. If the urlTemplate
is not set, calling subscribeToLabels(labels)
will throw an error.
Each component should be aware of the labels it needs upon instantiation. Although not necessary, providing default, hard-coded labels is a good practice to ensure users don't see empty pages prior to the translations loading.
It is not required to provide an array of label keys to the subscribeToLabels
function. Your TOD server will receive the query parameter labels=
. It is up to you to determine how this is handled. For "fail-safe" behavior, most TOD implementations should return the entire dictionary.
import { OnDestroy } from '@angular/core';
import { TranslationService } from '@my-ul/tod-angular-client';
export class MyChildComponent implements OnDestroy
{
// using a short variable name for the translation dictionary keeps the
// template files looking clean.
t: Record<string,string> = {
label_Welcome: 'Welcome',
label_YouMustAcceptTheTermsAndConditions: 'You must accept the terms and conditions.',
label_Accept: 'Accept',
label_Decline: 'Decline',
}
// unsubscribe to the subscription when the component unloads
translationSubscription: Subscription<any>;
constructor( public translation: TranslationService ) {
this.translationSubscription = translation
.subscribeToLabels(Object.keys(t))
.subscribe( (dictionary: Record<string, string>) => {
// By using Object.assign, this ensures that if the new dictionary
// is missing any label, the old label will stay in place, avoiding undefined labels.
this.t = Object.assign(this.t, dictionary);
})
}
ngOnDestroy() {
if(this.translationSubscription) {
this.translationSubscription.unsubscribe();
}
}
accept() { console.log('User has ACCEPTED the Terms and Conditions.'); }
decline() { console.log('User has DECLINED the Terms and Conditions.'); }
}
<!-- use the dictionary in your templates -->
<h2>{{ t.label_Welcome }}</h2>
<p>{{ t.label_YouMustAcceptTheTermsAndConditions }}</p>
<button (click)="accept()">{{ t.label_Accept }}</button>
<button (click)="decline()">{{ t.label_Decline }}</button>
Switching languages is easy! Any component in your application can call setLocale(locale)
. Anywhere a component has used subscribeToLabels
, it will update its labels automatically.
import { TranslationService } from '@my-ul/tod-angular-client';
export class MyChildComponent
{
// ... truncated ...
setLocale(locale: string) {
// this will trigger an application-wide update of translations
this.translation.setLocale(locale);
}
// ... truncated ...
}
And in your templates...
<button (click)="setLocale('en-US')">English (US)</button>
<button (click)="setLocale('fr-CA')">Français (CA)</button>
<button (click)="setLocale('de')">Deutsch</button>
To make working with translation more straightforward, tod-angular-client
includes some utility pipes that can be used to interpolate user data, or links and other markup safely.
import { PipesModule } from '@my-ul/tod-angular-client`;
@NgModule({
declarations: [],
imports: [PipesModule]
})
export class AppModule {}
The format
Pipe allows C#-style interpolation of strings. Using placeholders allows translators to reorder items in the interpolated string, which makes for robust translation. Please note that this example does NOT need to use the sanitize
pipe, since it isn't directly binding to [innerHTML]
.
<!-- Good Morning, Alice! -->
<p>{{ "Good Morning, {0}!" | format : user.name }}</p>
At times, you may want to interpolate HTML into strings to allow for translated hyperlinks, or bold/italicised info. You need to let Angular know the generated html is safe by using the safe
Pipe.
All templates binding to [innerHTML] must use the safe
pipe at a minimum. If user data is being interpolated into the string, sanitize
should be used so that user data isn't rendered as HTML.
<!-- To finish, click <strong>Close</strong>. -->
<p innerHTML="{{
'To finish, click {0}.'
| format
: ('<strong>{0}</strong>' | format : t.label_Close)
| safe : 'html'
}}"></p>
An advanced example allowing the user to click a Contact Us link.
export class MyComponent {
emailLinkTemplate = '<a href="mailto:{0}">{1}</a>';
emailAddress = 'support@example.com';
t = {
label_ContactUs: "Contact Us"
}
}
In the template...
<!--
Result:
If you need assistance, please <a href="mailto:support@example.com">Contact Us</a>.
-->
<p innerHTML="{{
'If you need assistance, please {0}.'
| format :
(emailLinkTemplate | format : emailAddress : label_ContactUs)
| safe : 'html'
}}"></p>
If untrusted user data is going to be interpolated into the string, use the sanitize
Pipe.
export class MyComponent
{
user = {
first_name: '<script>alert("hacked!");</script>'
}
}
Multi-line use and indentation is not required, but recommended, as it makes the data flow through the pipe easier to follow.
<!--
Approximate Result:
`Good morning, <script>alert("hacked!");</script>`
-->
<p [innerHTML]="{{
'Good morning, {0}'
| format
: ( user.first_name | sanitize : 'html' )
| safe : 'html'
}}"></p>
- Ensure you have called
setLocale()
at least once.setLocale
can be called before any subscriptions are made. No values will be emitted to the subscriber until the locale is set. It is recommended to callsetLocale()
early in your app's instantiation so that theTranslationService
is ready to translate as components initialize (on demand!). - Ensure you have set the
urlTemplate
correctly by callingsetUrlTemplate()
. Any attempts to usesubscribeToLabels()
will throw an error (check the console) if you don't have a URL template set. If you do not include the placeholder{0}
, your generated URLs will not include the current locale. If you are struggling with cached data, include the{1}
token somewhere in your URL as a cache buster. - Check the Network tab of your Developer Tools to make sure your URL is getting built properly.
- Ensure you have imported the
PipesModule
to whatever module you are working in.