npm package: @my-ul/tod-angular-client
Description: ## 18.3.0
Latest version: 18.3.0

# Changelog

## 18.3.0

- Updates the withLocaleFromMessageEvent and withLocaleFromQueryParam
  to hook window initialization using `APP_INITIALIZER`.

## 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:

```bash
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:

```typescript
// app/app.config.ts

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

export const appConfig: AppConfig = {
  providers: [
    // other providers
    provideTranslationOnDemand(
      withDefaultLocale("en-US"),
      withLabelEndpoint("http://localhost:3000/i18n/{0}.json"),
    ),
  ],
};
```

2. 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.

```typescript
import { AsyncPipe } from "@angular/common";
import {
  Component,
  type ApplicationConfig,
} from "@angular/core";
import {
  provideFakeTranslationOnDemand,
  provideTranslationOnDemand,
  useTranslationObservable,
  useTranslationSignal,
  withDefaultLocale,
  withLabelEndpoint,
} from "@my-ul/tod-angular-client";

export const appConfig: ApplicationConfig = {
  providers: [
    provideTranslationOnDemand(
      withDefaultLocale("en-US"),
      withLabelEndpoint(
        "http://localhost:4200/assets/i18n/{0}.json",
      ),
    ),
  ],
};

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

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

  // Using observable (Angular 15)
  readonly labelsObservable = useTranslationObservable({
    HelloWorld: "Hello, World!",
  });
}
```

# Configuration

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

```typescript
import { provideHttpClient } from "@angular/common/http";
import {
  type ApplicationConfig,
  Component,
} from "@angular/core";
import { provideRouter } from "@angular/router";
import {
  provideTranslationOnDemand,
  TodLogLevel,
  TranslationService,
  withDefaultLocale,
  withJoinChar,
  withLabelEndpoint,
  withLocaleFromMessageEvent,
  withLocaleFromQueryParam,
  withLogging,
  withUrlLengthLimit,
} from "@my-ul/tod-angular-client";

interface NewLocaleEvent {
  type: "locale";
  locale: string;
}

const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(),
    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<NewLocaleEvent>(
        /** Filter to messages that are the right type */
        (
          event: MessageEvent<unknown>,
        ): event is MessageEvent<NewLocaleEvent> =>
          !!event.data &&
          (event.data as NewLocaleEvent).type === "locale",

        /** Extract the locale from the message */
        (data: NewLocaleEvent) => 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(256),

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

# Interpolation with InterpolateComponent

Use the `InterpolateComponent` to interpolate values into strings.

```typescript
import { Component } from "@angular/core";
import { InterpolateComponent } from "@my-ul/tod-angular-client";

@Component({
  selector: "app-hello",
  standalone: true,
  imports: [InterpolateComponent],
  template: `
    <p data-testid="result" [interpolate]="TheNamesBond">
      <ng-template>{{ firstName }}</ng-template>
      <ng-template>{{ lastName }}</ng-template>
    </p>
  `,
})
export class HelloComponent {
  firstName = "James";
  lastName = "Bond";

  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:

```typescript
import { type ApplicationConfig, inject } from "@angular/core";
import {
  createTokenizer,
  PlaceholderToken,
  TextToken,
  TOD_TOKENIZER,
} from "@my-ul/tod-angular-client";

const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: TOD_TOKENIZER,
      useValue: createTokenizer(/__(\d+)__/g),
    },
  ],
};
```

# Testing with Translation on Demand

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

```typescript:testing
// 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.

```typescript
// 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!",
    );
  });
});
```