---
title: "Translations — React"
description: "Show the viewer in the user's language, pick one, change any word, and translate your own buttons."
integration: "React"
source: "https://www.embedpdf.com/docs/viewer/react/customize/translations"
---

# Translations

The viewer speaks the user's language: it picks the best match from the browser's languages. To
choose one yourself, set `locale`:

```js
{ locale: 'de' }
```

`locale` follows its prop, so a language switch in your app switches the viewer too. Without it,
or with `'auto'`, the viewer follows the browser.

## The languages it speaks

English, German, French, Spanish, Italian, Dutch, Portuguese, Polish, Swedish, Japanese, Chinese
(Simplified and Traditional), Korean, Arabic and Hebrew. Each one loads only when it's needed. For
Arabic and Hebrew, the viewer is right-to-left: the bars mirror, and `start` and `end` swap sides.

A language matches in its own script: readers in Taiwan and Hong Kong (`zh-TW`, `zh-HK`) see
Traditional Chinese, and readers in China and Singapore (`zh-CN`, `zh-SG`) Simplified.

## Change a word

A built-in button's label has the command's id as its key. Give a new one, per language:

```js
{
  strings: {
    en: { 'document:download': 'Save a copy' },
    de: { 'document:download': 'Kopie speichern' },
  },
}
```

A panel's button and the title in its header are one word, under the button's command id:
`'panel:comments'` renames both. See [Panels](panels#a-panels-title).

A mode tab is a button too, with the command `mode:annotate` for **Annotate**. Here a picker in your
app sets `locale`, and `strings` gives two labels your own words in each language it offers:

**`languages.tsx`**

```tsx
import { useState } from 'react';
import { PDFViewer } from '@embedpdf/viewer-react';

import './languages.css';

const welcome = '/pdfs/welcome.pdf';

// Your own words for two of the viewer's labels, in each language.
const strings = {
  en: { 'mode:annotate': 'Review', 'document:download': 'Save a copy' },
  de: { 'mode:annotate': 'Prüfen', 'document:download': 'Kopie speichern' },
  fr: { 'mode:annotate': 'Relire', 'document:download': 'Enregistrer une copie' },
  nl: { 'mode:annotate': 'Nakijken', 'document:download': 'Kopie opslaan' },
  ar: { 'mode:annotate': 'مراجعة', 'document:download': 'حفظ نسخة' },
};

export default function App() {
  const [locale, setLocale] = useState('en');
  return (
    <div className="app">
      <header className="bar">
        <select
          aria-label="Language"
          value={locale}
          onChange={(event) => setLocale(event.target.value)}
        >
          <option value="en">English</option>
          <option value="de">Deutsch</option>
          <option value="fr">Français</option>
          <option value="nl">Nederlands</option>
          <option value="ar">العربية</option>
        </select>
      </header>
      <PDFViewer className="viewer" src={welcome} strings={strings} locale={locale} />
    </div>
  );
}
```

**`languages.css`**

```css
/* Your app: a bar with the language picker, and the viewer under it */
.app {
  display: flex;
  flex-direction: column;
  height: 100%;
}

.bar {
  display: flex;
  justify-content: flex-end;
  padding: 8px 12px;
  border-bottom: 1px solid #d1d5db;
  background: #ffffff;
  color: #111827;
  font-family: system-ui, sans-serif;
  font-size: 14px;
}

.bar select {
  font: inherit;
}

.viewer {
  flex: 1;
  min-height: 0;
}
```

> **Try it:** the second tab reads **Review** instead of Annotate, and the menu at the top left has
> **Save a copy**. Pick Deutsch above the viewer: every word changes, yours too. Pick العربية and
> the bars mirror.

## Translate your own buttons

Give a command a `labelKey` instead of a `label`, and its words per language:

```js
{
  commands: [{ id: 'acme:send', labelKey: 'acme.send', icon: 'signature', run: send }],
  strings: {
    en: { 'acme.send': 'Send for signature' },
    nl: { 'acme.send': 'Verstuur ter ondertekening' },
  },
}
```

Panels and mode bars take a `titleKey` the same way, for their title. A panel's title is its
button's label too.

The style panel's words have keys too: each property and option by what it says
(`annotation.style.stroke`, `annotation.style.opacity`, `annotation.lineEndings.openArrow`), and a
form field's settings by theirs (`form.settings.tooltip`). Change one like any other word:

```js
{ strings: { en: { 'annotation.style.stroke': 'Line' } } }
```

So do the page number's words, under `pageNumber.`, and the comments panel's, under `comments.`.
The ones you're most likely to change:

| Key                                                                                                                                 | What it says                                                                                    |
| :---------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------- |
| `pageNumber.page`                                                                                                                   | `{current} / {total}`                                                                           |
| `pageNumber.pages`                                                                                                                  | `{first}–{last} / {total}`, while a spread fits across the view (`spread`)                      |
| `pageNumber.goToPage`                                                                                                               | The tooltip of the page number, and the label of the field it turns into                        |
| `comments.empty`, `comments.emptyReadOnly`                                                                                          | What the panel says while the document has no comments, when the user can add some and when not |
| `comments.addPlaceholder`, `comments.replyPlaceholder`, `comments.reply`                                                            | The field for a new comment, and for a reply                                                    |
| `comments.statusAccepted`, `comments.statusRejected`, `comments.statusCancelled`, `comments.statusCompleted`, `comments.statusNone` | A thread's status                                                                               |

A word in braces is filled in as the viewer writes it: keep it in your words. The number that turns
into a field to type a page is `{current}`, or `{first}`:

```js
{ strings: { en: { 'pageNumber.page': 'Page {current} of {total}', 'comments.empty': 'No notes yet.' } } }
```

## A language of your own

Give `strings` for a language the viewer doesn't have, and it becomes one of its languages. Anything
you don't translate shows in English:

```js
{
  locale: 'da',
  strings: { da: { 'document:download': 'Hent', 'zoom:in': 'Zoom ind' } },
}
```

It reads right to left when its code says so: a right-to-left language such as `fa` or `ur`, or a
right-to-left script such as `ku-Arab`.

## Good to know

### Change the language from code

`viewer.get('i18n').setLocale('fr')` does the same as the `locale` prop, and returns a promise that
resolves once the language has loaded.

### Add words while the app runs

`viewer.get('i18n').addTranslations('nl', { … })` adds or replaces words in a language without
starting over, for words that arrive from your own translation service.

### Labels in the toolbar

The toolbar measures the words it shows, so a longer translation moves fewer or more buttons into
the "More" menu by itself.
