EmbedPDF

Translations

The translations plugin holds your viewer’s text in every language you support, and switches between them while the app runs. Register it with the user’s language:

i18n-plugin.ts
import { Component } from '@angular/core';
import { negotiateLocale, withI18n } from '@embedpdf/angular/i18n';
import { provideEmbedPdf } from '@embedpdf/angular/runtime';
import en from './locales/en';
import { engine } from './pdf';

const locale = negotiateLocale(['en', 'nl', 'de'], navigator.languages) ?? 'en';

@Component({
  selector: 'app-document-viewer',
  providers: [
    provideEmbedPdf(
      { engine },
      /* … */
      withI18n({
        locale,
        locales: [{ code: 'en', name: 'English', translations: en }],
        loaders: {
          nl: () => import('./locales/nl').then((module) => module.default),
          de: () => import('./locales/de').then((module) => module.default),
        },
      }),
    ),
  ],
  template: `<!-- your toolbar and pages -->`,
})
export class DocumentViewer {}
Then translate with the epdfT pipe:
save-button.ts
import { Component } from '@angular/core';
import { EpdfTPipe } from '@embedpdf/angular/i18n';

@Component({
  selector: 'app-save-button',
  imports: [EpdfTPipe],
  template: `<button>{{ 'toolbar.save' | epdfT }}</button>`,
})
export class SaveButton {}

negotiateLocale picks the best language you have for what the browser asks, such as nl for a browser set to nl-BE, within the script it’s written in: zh-TW doesn’t get zh-CN, since Taiwan writes Traditional Chinese and zh-CN is Simplified. A language in loaders is loaded the first time someone switches to it, so your bundle carries only the one you start with.

Strings#

A language’s translations are your strings, grouped however you like:

export default {
  toolbar: { save: 'Opslaan', zoomIn: 'Inzoomen' },
  search: {
    results: { one: '{count} resultaat', other: '{count} resultaten' },
  },
};
t('toolbar.save'); // 'Opslaan'
t('search.results', { params: { count: 3 } }); // '3 resultaten'
In a template, the pipe takes the same options: {{ 'search.results' | epdfT: { params: { count: 3 } } }}.
  • {name} is filled in from params.
  • Plurals: a string with one and other picks by params.count.
  • Missing strings fall back to the fallback language (English unless you set another), then to fallback if you pass one, then to the key itself. You get a warning in the console once, so you notice.

Switch the language#

language-picker.ts
import { Component, inject } from '@angular/core';
import { EpdfI18n } from '@embedpdf/angular/i18n';

@Component({
  selector: 'app-language-picker',
  template: `
    <select #picker [disabled]="i18n.loading() !== null" (change)="i18n.setLocale(picker.value)">
      @for (language of i18n.locales(); track language.code) {
        <option [value]="language.code" [selected]="language.code === i18n.locale()">
          {{ language.name }}
        </option>
      }
    </select>
  `,
})
export class LanguagePicker {
  protected readonly i18n = inject(EpdfI18n);
}

Loading live preview…

import { ChangeDetectionStrategy, Component, inject, ViewEncapsulation } from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { EpdfCommand, standardCommands, withCommands } from '@embedpdf/angular/commands';
import { EpdfI18n, EpdfTPipe, withI18n } from '@embedpdf/angular/i18n';
import type { Locale } from '@embedpdf/angular/i18n';
import { localEngine } from '@embedpdf/engine';

// Your strings. The buttons' labels are the standard commands', which come translated.
const en: Locale = {
  code: 'en',
  name: 'English',
  translations: {
    app: { language: 'Language', pages: { one: '{count} page', other: '{count} pages' } },
  },
};
const nl: Locale = {
  code: 'nl',
  name: 'Nederlands',
  translations: {
    app: { language: 'Taal', pages: { one: '{count} pagina', other: "{count} pagina's" } },
  },
};

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

@Component({
  selector: 'demo-root',
  imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, EpdfCommand, EpdfTPipe],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withCommands({ commands: standardCommands }),
      withI18n({
        locale: 'nl',
        locales: [en, nl],
        // Loaded the first time someone picks them; in your app, `() => import('./locales/de')`.
        loaders: {
          de: async () => ({
            code: 'de',
            name: 'Deutsch',
            translations: {
              app: { language: 'Sprache', pages: { one: '{count} Seite', other: '{count} Seiten' } },
            },
          }),
          ja: async () => ({
            code: 'ja',
            name: '日本語',
            translations: { app: { language: '言語', pages: { other: '{count} ページ' } } },
          }),
        },
      }),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './basic.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <div class="toolbar">
        <label class="picker">
          {{ 'app.language' | epdfT }}
          <select
            #picker
            class="select"
            [disabled]="i18n.loading() !== null"
            (change)="i18n.setLocale(picker.value)"
          >
            @for (language of i18n.locales(); track language.code) {
              <option [value]="language.code" [selected]="language.code === i18n.locale()">
                {{ language.name }}
              </option>
            }
          </select>
        </label>
        @for (id of commands; track id) {
          <button type="button" class="button" [epdfCommand]="id" #command="epdfCommand">
            {{ command.label() }}
          </button>
        }
        <output class="count">
          {{ 'app.pages' | epdfT: { params: { count: stage.pageCount() } } }}
        </output>
      </div>
      <epdf-stage #stage="epdfStage" class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
        </ng-template>
      </epdf-stage>
    </ng-container>

    <ng-template #loading><p class="loading">…</p></ng-template>
  `,
})
export class App {
  // The component that provides the viewer can inject its services itself.
  protected readonly i18n = inject(EpdfI18n);
  protected readonly commands = ['page:previous', 'page:next', 'zoom:in', 'document:print'];
}

setLocale() loads the language if it needs to, then switches. Every epdfT pipe updates to the new language.

Add strings later#

For a plugin of your own, or strings from your server, add them to a language while the app runs:

i18n.addTranslations('nl', { review: { approve: 'Goedkeuren' } });
i18n.addTranslations('nl', { 'review.reject': 'Afwijzen' }); // a dotted key works too

registerLocale() adds a whole new language, and returns a function that removes it again.

EmbedPDF’s own text#

Commands have labels, and the standard commands come translated in English, Dutch, German, French, Spanish, Chinese, Swedish and Japanese, the languages of the standard stamps. Change any of them with addTranslations, under the same keys.

Right-to-left languages#

A language can say it reads right to left with direction: 'rtl'. direction in State tells you, so you can set it on your viewer’s root:

direction.ts
import { Component, inject } from '@angular/core';
import { EpdfI18n } from '@embedpdf/angular/i18n';

@Component({
  selector: 'app-viewer-root',
  template: `
    <div [dir]="i18n.direction()">
      <!-- your viewer -->
    </div>
  `,
})
export class ViewerRoot {
  protected readonly i18n = inject(EpdfI18n);
}

Loading live preview…

import { ChangeDetectionStrategy, Component, inject, ViewEncapsulation } from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { EpdfCommand, standardCommands, withCommands } from '@embedpdf/angular/commands';
import { EpdfI18n, EpdfTPipe, withI18n } from '@embedpdf/angular/i18n';
import type { Locale } from '@embedpdf/angular/i18n';
import { localEngine } from '@embedpdf/engine';

const en: Locale = {
  code: 'en',
  name: 'English',
  translations: { app: { title: 'The ebook', switchTo: 'العربية' } },
};
// Arabic reads right to left. The standard commands have no Arabic labels, so the app gives
// them, under the same keys.
const ar: Locale = {
  code: 'ar',
  name: 'العربية',
  direction: 'rtl',
  translations: {
    app: { title: 'الكتاب الإلكتروني', switchTo: 'English' },
    commands: {
      page: { previous: 'الصفحة السابقة', next: 'الصفحة التالية' },
      zoom: { in: 'تكبير', out: 'تصغير' },
    },
  },
};

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// The toolbar follows the language: in Arabic it runs from right to left.
@Component({
  selector: 'demo-root',
  imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, EpdfCommand, EpdfTPipe],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withCommands({ commands: standardCommands }),
      withI18n({ locale: 'ar', locales: [en, ar] }),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './rtl.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <div *epdfDocumentGate="let document; fallback: loading" [dir]="i18n.direction()" class="root">
      <div class="toolbar">
        <strong class="title">{{ 'app.title' | epdfT }}</strong>
        @for (id of commands; track id) {
          <button type="button" class="button" [epdfCommand]="id" #command="epdfCommand">
            {{ command.label() }}
          </button>
        }
        <button type="button" class="button switch" (click)="switchLanguage()">
          {{ 'app.switchTo' | epdfT }}
        </button>
      </div>
      <epdf-stage class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
        </ng-template>
      </epdf-stage>
    </div>

    <ng-template #loading><p class="loading">…</p></ng-template>
  `,
})
export class App {
  protected readonly i18n = inject(EpdfI18n);
  protected readonly commands = ['page:previous', 'page:next', 'zoom:out', 'zoom:in'];

  protected switchLanguage() {
    void this.i18n.setLocale(this.i18n.locale() === 'ar' ? 'en' : 'ar');
  }
}

For documents that read right to left, such as a book in Arabic, set the Stage’s direction too, so pages and spreads run from right to left.

Methods#

Everything inject(EpdfI18n) gives you.

MethodWhat it does
t(key, { params?, fallback? })A string in the current language, with params filled in. In a template, use the epdfT pipe, which updates when the language changes. Strings
getTranslator()A t function that stays the same until the language or the strings change, for code that caches on it.
hasKey(key)Whether a key has a string in the current or the fallback language. Never warns.
setLocale(code)Loads the language if it needs to, then switches. Resolves once it shows; rejects when it can’t load, or when a newer call replaces it. Switch the language
addTranslations(code, translations)Adds strings to one of your languages, loaded or not, and replaces the ones with the same key. Add strings later
registerLocale(locale)Adds a whole language, { code, name, direction?, translations }, and returns a function that removes it again
getSettings(), updateSettings(changes), resetSettings()Reads the settings, changes them while the app runs, or goes back to what you registered. In a component, use inject(EpdfI18n).settings(). Settings

Every method that returns a promise also takes a signal in its options, to cancel it.

State#

Every value is a signal on inject(EpdfI18n): read it in a template or a computed(), and your view updates when it changes. The method in the last column reads the same value once.

FieldWhat it isRead once
locale()The current language’s codegetLocale()
direction()'ltr' or 'rtl', how the current language readsgetDirection()
locales()Every language you can switch to, loaded or not, each with code, name, direction and loaded. A language in loaders shows its code as its name until it’s loaded.listLocales()
loading()The code of the language being loaded, or nullgetLoadingLocale()

Settings#

Set them when you register the plugin, and change them while the app runs with i18n.updateSettings({ … }); i18n.resetSettings() goes back to what you registered, and inject(EpdfI18n).settings() gives you the current settings.

SettingWhat it doesDefault
localeThe language to start withfallbackLocale
fallbackLocaleThe language to use for strings another language misses'en'
localesLanguages loaded from the startnone
loadersLanguages loaded when someone switches to themnone

Events#

locale-events.ts
import { Component, DOCUMENT, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EpdfI18n } from '@embedpdf/angular/i18n';

@Component({
  selector: 'app-viewer-layout',
  template: `<!-- your toolbar and pages -->`,
})
export class ViewerLayout {
  private readonly i18n = inject(EpdfI18n);
  private readonly document = inject(DOCUMENT);

  constructor() {
    this.i18n.localeChanged$
      .pipe(takeUntilDestroyed())
      .subscribe(({ locale }) => this.document.documentElement.setAttribute('lang', locale));
  }
}
EventFires whenCarries
localeChanged$The language changedlocale, previousLocale
localeLoadFailed$A language couldn’t be loadedlocale, error
settingsChanged$The settings changedsettings, changed

Good to know#

Before a document opens#

The plugin needs no engine and no document, so your loading screen, a password prompt and an empty viewer can be translated too.

A language that fails to load#

When a language in loaders can’t be loaded, for example because the user is offline, the viewer stays in the language it had, and localeLoadFailed$ tells you why. Call setLocale() again to try once more.

Was this page helpful?

Your feedback goes directly to the documentation team.