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:
import { i18nPlugin, negotiateLocale } from '@embedpdf/react/i18n';
import en from './locales/en';
const locale = negotiateLocale(['en', 'nl', 'de'], navigator.languages) ?? 'en';
export const plugins = [
/* … */
i18nPlugin({
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),
},
}),
];useT():
import { useT } from '@embedpdf/react/i18n';
export function SaveButton() {
const t = useT();
return <button>{t('toolbar.save')}</button>;
}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'{name}is filled in fromparams.- Plurals: a string with
oneandotherpicks byparams.count. - Missing strings fall back to the fallback language (English unless you set another), then to
fallbackif you pass one, then to the key itself. You get a warning in the console once, so you notice.
Switch the language#
import { useI18n, useI18nState } from '@embedpdf/react/i18n';
export function LanguagePicker() {
const i18n = useI18n();
const { locale, locales, loading } = useI18nState();
return (
<select value={locale} disabled={loading !== null} onChange={(event) => i18n.setLocale(event.target.value)}>
{locales.map((language) => (
<option key={language.code} value={language.code}>
{language.name}
</option>
))}
</select>
);
}Loading live preview…
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStageState } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { commandsPlugin, standardCommands, useCommand } from '@embedpdf/react/commands';
import { i18nPlugin, useI18n, useI18nState, useT } from '@embedpdf/react/i18n';
import type { Locale } from '@embedpdf/react/i18n';
import { localEngine } from '@embedpdf/engine';
import './basic.css';
const engine = localEngine();
// 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 plugins = [
stagePlugin(),
renderPlugin(),
commandsPlugin({ commands: standardCommands }),
i18nPlugin({
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} ページ' } } },
}),
},
}),
];
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()) };
};
function LanguagePicker() {
const i18n = useI18n();
const t = useT();
const { locale, locales, loading } = useI18nState();
return (
<label className="picker">
{t('app.language')}
<select
className="select"
value={locale}
disabled={loading !== null}
onChange={(event) => void i18n.setLocale(event.target.value)}
>
{locales.map((language) => (
<option key={language.code} value={language.code}>
{language.name}
</option>
))}
</select>
</label>
);
}
function CommandButton({ id }: { id: string }) {
const command = useCommand(id);
if (!command.visible) return null;
return (
<button type="button" className="button" disabled={!command.enabled} onClick={command.run}>
{command.label}
</button>
);
}
function PageCount() {
const t = useT();
const { pageCount } = useStageState();
return <output className="count">{t('app.pages', { params: { count: pageCount } })}</output>;
}
export default function App() {
return (
<Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate fallback={<p className="loading">…</p>}>
<div className="toolbar">
<LanguagePicker />
<CommandButton id="page:previous" />
<CommandButton id="page:next" />
<CommandButton id="zoom:in" />
<CommandButton id="document:print" />
<PageCount />
</div>
<Stage className="stage">{() => <RenderLayer />}</Stage>
</DocumentGate>
</Viewer>
);
}setLocale() loads the language if it needs to, then switches. Everything that uses useT()
re-renders in 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 tooregisterLocale() 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:
import { useI18nState } from '@embedpdf/react/i18n';
export function ViewerRoot() {
const { direction } = useI18nState();
return <div dir={direction}>{/* your viewer */}</div>;
}Loading live preview…
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { commandsPlugin, standardCommands, useCommand } from '@embedpdf/react/commands';
import { i18nPlugin, useI18n, useI18nState, useT } from '@embedpdf/react/i18n';
import type { Locale } from '@embedpdf/react/i18n';
import { localEngine } from '@embedpdf/engine';
import './rtl.css';
const engine = localEngine();
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 plugins = [
stagePlugin(),
renderPlugin(),
commandsPlugin({ commands: standardCommands }),
i18nPlugin({ locale: 'ar', locales: [en, ar] }),
];
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()) };
};
function CommandButton({ id }: { id: string }) {
const command = useCommand(id);
if (!command.visible) return null;
return (
<button type="button" className="button" disabled={!command.enabled} onClick={command.run}>
{command.label}
</button>
);
}
// The toolbar follows the language: in Arabic it runs from right to left.
function ViewerRoot() {
const i18n = useI18n();
const t = useT();
const { direction, locale } = useI18nState();
return (
<div dir={direction} className="root">
<div className="toolbar">
<strong className="title">{t('app.title')}</strong>
<CommandButton id="page:previous" />
<CommandButton id="page:next" />
<CommandButton id="zoom:out" />
<CommandButton id="zoom:in" />
<button
type="button"
className="button switch"
onClick={() => void i18n.setLocale(locale === 'ar' ? 'en' : 'ar')}
>
{t('app.switchTo')}
</button>
</div>
<Stage className="stage">{() => <RenderLayer />}</Stage>
</div>
);
}
export default function App() {
return (
<Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate fallback={<p className="loading">…</p>}>
<ViewerRoot />
</DocumentGate>
</Viewer>
);
}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 useI18n() gives you.
| Method | What it does |
|---|---|
t(key, { params?, fallback? }) | A string in the current language, with params filled in. In a component, use useT(), which re-renders 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. useT() returns 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 useI18nSettings(). Settings |
Every method that returns a promise also takes a signal in its options, to cancel it.
State#
Everything useI18nState() gives you. Your component re-renders when a value changes. The method in the last column reads the same value once.
| Field | What it is | Read once |
|---|---|---|
locale | The current language’s code | getLocale() |
direction | 'ltr' or 'rtl', how the current language reads | getDirection() |
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 null | getLoadingLocale() |
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
useI18nSettings() gives you the current settings.
| Setting | What it does | Default |
|---|---|---|
locale | The language to start with | fallbackLocale |
fallbackLocale | The language to use for strings another language misses | 'en' |
locales | Languages loaded from the start | none |
loaders | Languages loaded when someone switches to them | none |
Events#
import { useI18nEvent } from '@embedpdf/react/i18n';
export function DocumentLanguage() {
useI18nEvent(
(i18n) => i18n.onLocaleChanged,
({ locale }) => document.documentElement.setAttribute('lang', locale),
);
return null;
}| Event | Fires when | Carries |
|---|---|---|
onLocaleChanged | The language changed | locale, previousLocale |
onLocaleLoadFailed | A language couldn’t be loaded | locale, error |
onSettingsChanged | The settings changed | settings, 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 onLocaleLoadFailed tells you why. Call setLocale() again to
try once more.
Your feedback goes directly to the documentation team.