0.0.1 • Published 2 years ago

tod-angular-client v0.0.1

Weekly downloads
-
License
-
Repository
-
Last release
2 years ago

Translation-on-Demand Angular Client

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.

Expectations

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.

Getting Started

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.

Consuming Labels

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 Locales

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>

Utility Pipes

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 {}

format Pipe

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>

safe Pipe

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>

sanitize Pipe

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, &#x3C;script&#x3E;alert(&#x22;hacked!&#x22;);&#x3C;/script&#x3E;`
-->
<p [innerHTML]="{{
    'Good morning, {0}'
    | format 
        : ( user.first_name | sanitize : 'html' )
    | safe : 'html'
}}"></p>

Troubleshooting

If labels are not loading...

  • 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 call setLocale() early in your app's instantiation so that the TranslationService is ready to translate as components initialize (on demand!).
  • Ensure you have set the urlTemplate correctly by calling setUrlTemplate(). Any attempts to use subscribeToLabels() 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.

Pipes are not working

  • Ensure you have imported the PipesModule to whatever module you are working in.