18.2.1 • Published 8 months ago

@my-ul/tod-angular-client v18.2.1

Weekly downloads
-
License
-
Repository
-
Last release
8 months ago

Changelog

18.2.1

  • Updates the README to reflect the latest changes in the library.

18.2.0

  • Adds initialization functions withLocaleFromMessageEvent and withLocaleFromQueryParam.
  • Removes useTranslationProxy implementations. Signal and Observable are the recommended hooks, because they are more performant and easier to debug.

18.1.0

  • Fixes some issues with the shared Observables used to propagate locale and label changes throughout the app.
  • Changes TranslationComponent to InterpolateComponent.
  • Adds unit tests to validate behaviors.
  • Removes @let usage from InterpolateComponent to ensure backwards compatibility with 16 and 17.

18.0.0

  • Introduces three implementations for useTranslation: useTranslationSignal, useTranslationObservable, and useTranslationProxy.
  • Adds support for Angular 16+.

Quick Start

Install the @my-ul/tod-angular-client package:

npm install @my-ul/tod-angular-client
# or yarn
yarn add @my-ul/tod-angular-client
  1. Add Translation on Demand providers to your app config:
// app/app.config.ts

import { provideTranslationOnDemand } from '@my-ul/tod-angular-client';

export const appConfig: AppConfig = {
    providers: [
        // provideRouter(routes),
        provideTranslationOnDemand(
            withDefaultLocale('en-US'),
            withLabelEndpoint('http://localhost:3000/i18n/{0}.json'),
        ),
    ],
};
  1. Add a useTranslation hook to your component.

Provide a dictionary of labels to use in the event of any translation failure, such as network, or missing labels.

// app/components/hello.component.ts

import { Component } from '@angular/core';
import { AsyncPipe } from '@angular/common';

// pick a hook that fits your needs
// for Angular 16+, useTranslationSignal likely provides
// the best performance and developer experience
import {
    useTranslationSignal,
    useTranslationObservable,
} from '@my-ul/tod-angular-client';

@Component({
    selector: 'app-hello',
    standalone: true,
    imports: [AsyncPipe]
    template: `
        @let labelsFromSignal = labelsSignal();
        @let labelsFromObservable = labels$ | async;

        <h1>{{ labelsFromSignal.HelloWorld }}</h1>
        <h1>{{ labelsFromObservable.HelloWorld }}</h1>
    `,
})
export class HelloComponent {
    // Using signal (Angular 16+)
    readonly labelsSignal = useTranslationSignal({
        HelloWorld: 'Hello, World!',
    });

    // Using observable
    readonly labels$ = useTranslationObservable({
        HelloWorld: 'Hello, World!'
    });
}

Configuration

Use the with* setup functions to configure Translation on Demand:

import {
    provideTranslationOnDemand,
    withDefaultLocale,
    withLocaleFromMessageEvent,
    withLocaleFromQueryParam,
    withLabelEndpoint,
    withJoinChar,
    withUrlLengthLimit,
    withLogging
    TodLogLevel
} from '@my-ul/tod-angular-client';

export const appConfig = {
    providers: [
        provideTranslationOnDemand(
            /* Set the default locale to 'en-US'. ToD will not translate until a locale is provided. */
            withDefaultLocale('en-US'),

            /* Set the locale when the window receives a message */
            withLocaleFromMessageEvent<T>(
                /* filter to messages that are the right type */
                (event: MessageEvent<unknown>): event is MessageEvent<T>
                    => event.data.type === 'locale' && typeof event.data.locale === 'string',

                /* extract the locale from the message */
                (data: T) => data.locale,
            ),

            /* Set the locale from the query parameter 'locale'. */
            withLocaleFromQueryParam('locale'),

            /**
             * Set the label endpoint to 'http://localhost:3000/i18n/{0}.json'.
             * The {0} will be replaced with the locale.
             * Add {1} to include a cache buster in the request url.
             */
            withLabelEndpoint('http://localhost:3000/i18n/{0}.json'),

            /* Label keys will be joined with the provided character */
            withJoinChar('_'),

            /* If the URL length exceeds the limit, the label keys will be requested with a POST request. */
            withUrlLengthLimit(2048),

            /* Log helpful information to the console. */
            withLogging(TodLogLevel.Trace),
        ),
    ],
};

Interpolation with InterpolateComponent

Use the InterpolateComponent to interpolate values into strings.

// app/components/hello.component.ts
import { InterpolateComponent } from '@my-ul/tod-angular-client';

@Component({
    selector: 'app-hello',
    standalone: true,
    imports: [InterpolateComponent],
    template: `
        @let i18n = translate();

        <h1>{{ labels.HelloWorld }}</h1>
        <p [interpolate]="i18n.TheNamesBond">
            <ng-template>{{ firstName }}</ng-template>
            <ng-template>{{ lastName }}</ng-template>
        </p>
    `,
})
export class HelloComponent {
    firstName = 'James';
    lastName = 'Bond';

    translate = useTranslationSignal({
        TheNamesBond: 'The name is {1}, {0} {1}.',
    });
}

Custom Tokenizer

By default, the InterpolateComponent will tokenize using C# style placeholders. You can override this behavior by providing a custom tokenizer:

// app.config.ts

import {
    provideTranslationOnDemand,
    sprintfTokenizer,
    Tokenizer,
} from '@my-ul/tod-angular-client';

const sprintf: Tokenizer = sprintfTokenizer;

// tokenize on __0__ instead of {0}
const customTokenizer: Tokenizer = createTokenizer(/__(\d+)__/g);

export const appConfig: AppConfig = {
    providers: [
        provideRouter(routes),
        {
            provide: TOD_TOKENIZER,
            useValue: customTokenizer,
        },
        provideTranslationOnDemand(
            withDefaultLocale('en-US'),
            withLabelEndpoint('http://localhost:3000/i18n/{0}.json'),
        ),
    ],
};

Testing with Translation on Demand

Inject a mock translation service into your test. Provide the labels needed for the test.

// app/components/hello.component.spec.ts

describe('HelloComponent', () => {
    let component: HelloComponent;
    let fixture: ComponentFixture<HelloComponent>;

    beforeEach(async () => {
        await TestBed.configureTestingModule({
            declarations: [HelloComponent],
            providers: [
                provideFakeTranslationOnDemand({
                    HelloWorld: 'Hello, World!',
                }),
            ],
        }).compileComponents();

        fixture = TestBed.createComponent(HelloComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
    });

    it('should render "Hello, World!"', () => {
        expect(fixture.nativeElement.textContent).toContain('Hello, World!');
    });
});

If your repository has the translation dictionaries, you can use import to provide the entire dictionary to the test harness.

// app/components/hello.component.spec.ts

describe('HelloComponent', () => {
    let component: HelloComponent;
    let fixture: ComponentFixture<HelloComponent>;

    beforeEach(async () => {
        await TestBed.configureTestingModule({
            declarations: [HelloComponent],
            providers: [
                provideFakeTranslationOnDemand(import('../../assets/i18n/en-US.json')),
            ],
        }).compileComponents();

        fixture = TestBed.createComponent(HelloComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
    });

    it('should render "Hello, World!"', () => {
        expect(fixture.nativeElement.textContent).toContain('Hello, World!');
    });
});
18.2.1

8 months ago

18.2.0

8 months ago

18.0.0

9 months ago

10.0.0

2 years ago

14.0.0

2 years ago

13.0.0

2 years ago

11.0.0

2 years ago

1.2.0-1

2 years ago

1.2.0-0

3 years ago

1.2.0-3

2 years ago

1.2.0-2

2 years ago

1.2.0-4

2 years ago

1.1.1

3 years ago

1.1.0

3 years ago

1.1.2

3 years ago

1.0.0

3 years ago