# Internationalize the player

Translate player labels and announcements: shipped locale packs, custom translations, runtime switching, and server rendering.

Translate every player label, tooltip, and screen-reader announcement. The player ships translations for more than 50 locales and loads them on demand.

## Recommended approach

Wrap the player in the i18n provider. It resolves the locale from your page’s `lang` attribute (or an explicit override), lazy-loads the matching locale pack, and every component label follows.

```html
<media-i18n lang="ja">
  <video-player>
    <media-container>
      <video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" muted playsinline></video>
    </media-container>
  </video-player>
</media-i18n>
<script type="module">
  import '@videojs/html/i18n';
  import '@videojs/html/video/player';
</script>
```

Omit the `lang` attribute on `<media-i18n>` to follow the nearest ancestor `lang` (usually `<html lang>`).

Pick a language and every label follows — no remount:

**index.html**

```html
<div class="html-i18n-language">
  <label>
    Language
    <select></select>
  </label>
  <media-i18n lang="en">
    <video-player>
      <video-skin>
        <video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" muted playsinline></video>
      </video-skin>
    </video-player>
  </media-i18n>
</div>
```

**index.css**

```css
.html-i18n-language {
  display: grid;
  gap: 16px;
  padding: 16px;
}

.html-i18n-language label {
  display: flex;
  gap: 8px;
  align-items: center;
}

.html-i18n-language select {
  padding: 4px 8px;
}

.html-i18n-language video-skin,
.html-i18n-language video {
  display: block;
  width: 100%;
}

.html-i18n-language video-skin {
  aspect-ratio: 16 / 9;
}
```

**index.ts**

```ts
import { LOCALES } from '@videojs/html/i18n';
import '@videojs/html/video/player';
import '@videojs/html/video/skin';

const languageNames = new Intl.DisplayNames(['en'], { type: 'language' });
const initializedDemos = new WeakSet<HTMLElement>();

function initializeDemos(): void {
  document.querySelectorAll<HTMLElement>('.html-i18n-language').forEach((demo) => {
    if (initializedDemos.has(demo)) return;

    initializedDemos.add(demo);

    const select = demo.querySelector('select');
    const provider = demo.querySelector('media-i18n');

    for (const locale of ['en', ...LOCALES]) {
      const option = document.createElement('option');

      option.value = locale;
      option.textContent = languageNames.of(locale) ?? locale;
      select?.append(option);
    }

    select?.addEventListener('change', () => {
      provider?.setAttribute('lang', select.value);
    });
  });
}

initializeDemos();
document.addEventListener('astro:page-load', initializeDemos);
```

## How it works

- Component labels are text descriptors — a translation key plus its English default — so everything renders in English with no setup and switches language when a translator is present. Keys are stable: `buttons.play` stays `buttons.play` even if the English wording changes.
- The provider resolves the locale: explicit `lang` on `<media-i18n>` first, then the nearest ancestor `lang` attribute, then `'en'`. It re-resolves when the page’s `lang` changes.
- The provider lazy-loads the pack for the resolved locale automatically and keeps it in a layer above the shared registry ([`registerI18n`](https://videojs.org/docs/framework/html/reference/api/register-i18n)). Translations merge along a fallback chain that follows the [BCP 47](https://www.rfc-editor.org/rfc/rfc5646) parent tags — `es-MX` checks `es`, then `en` — so a regional pack only has to carry the text that differs from its parent.
- Each piece of text resolves from the first source that has it:
  1. Registered custom translations
  2. A built-in locale pack
  3. The component’s English fallback
- `registerI18n(locale, translations)` **merges**: each call adds or replaces keys for that locale without wiping prior registrations, and later registrations win for the same key. Register a locale before the provider resolves it — a pack the provider already lazy-loaded sits above the registry and hides registry entries for that locale. Overriding a handful of strings is the same call as registering a full locale.
- When no shipped pack matches the locale, the provider can fall back to the browser’s on-device Translation API to machine-translate the English catalogue, with placeholders preserved.

Translate your own UI with the same machinery:

```html
<media-text token="buttons.play">Play</media-text>
```

The element’s text content is the English default; the `token` selects the translation.

## Availability and constraints

- English is built in; every other locale is a lazy-loaded pack.
- Locale tags are [BCP 47](https://www.rfc-editor.org/rfc/rfc5646) (`es`, `pt-BR`).
- Translation keys are namespaced strings (`buttons.play`, `errors.network`, `menu.quality`); the full list lives at [Translation phrases](https://videojs.org/docs/framework/html/reference/api/translation-phrases) and is typed, so typos in `registerI18n` objects fail in TypeScript. The translator itself accepts any string; a mistyped `t()` key falls back to its `default`.
- Some strings interpolate parameters; custom translations must keep the placeholders:

```ts
// ✅
registerI18n('es', { seek: { forward: 'Adelantar {seconds} segundos' } });

// ❌ The interpolated value is lost
registerI18n('es', { seek: { forward: 'Adelantar' } });
```

- The Browser Translation API fallback needs Chrome with `globalThis.Translator` and downloads an on-device model on first use. It’s best-effort; ship a real pack for languages you support officially.

## Common variations

### Override specific strings

```js
import '@videojs/html/i18n/locales/ja/register';
import { registerI18n } from '@videojs/html/i18n';

registerI18n('ja', { buttons: { play: '再生', pause: '一時停止' } });
```

Only the keys you pass change; the rest of the `ja` pack stays intact. Keep the `/register` import first: it must run before `<media-i18n>` upgrades, or the provider lazy-loads the shipped pack and that pack hides your overrides.

### Register your own locale

Registered packs are partial: missing keys fall back through the locale chain to a shipped pack or English.

```ts
import { registerI18n, type Translations } from '@videojs/html/i18n';

const es = {
  buttons: {
    play: 'Reproducir',
    pause: 'Pausa',
    mute: 'Silenciar',
    unmute: 'Activar sonido',
  },
} satisfies Partial<Translations>;

registerI18n('es', es);
```

### Register a pack with the bundle

Skip lazy loading by registering a shipped locale at startup with a side-effect import. First paint is synchronous in that language:

```ts
import '@videojs/html/i18n/locales/ja/register';
```

### Load locales from the CDN

CDN locale modules call `registerI18n` on import. Pin every URL to the same version:

```html
<script type="module" src="https://cdn.jsdelivr.net/npm/@videojs/cdn@10.0.0-rc.4/locales/es.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/@videojs/cdn@10.0.0-rc.4/video.js"></script>
```

For custom strings, import `registerI18n` from the version-pinned `i18n.js` that matches your player script. The player’s chunks import that exact URL, so a different or unversioned URL loads a second copy of the module with its own registry, and your strings never reach the player. Load your module before the player script so the strings are registered when the elements upgrade:

**my-locale.js**

```js
import { registerI18n } from 'https://cdn.jsdelivr.net/npm/@videojs/cdn@10.0.0-rc.4/i18n.js';

registerI18n('es', { buttons: { play: 'Reproducir', pause: 'Pausa' } });
```

```html
<script type="module" src="/my-locale.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/@videojs/cdn@10.0.0-rc.4/video.js"></script>
```

### Switch locale at runtime

The simplest switch updates the document language and lets providers pick it up — no remount required:

```ts
document.documentElement.lang = 'fr';
```

Ambient switching only applies when `<media-i18n>` has no explicit `lang` attribute. To switch explicitly, set the provider’s `lang`:

```js
document.querySelector('media-i18n').lang = 'fr';
```

Switching lazy-loads the pack, which can briefly show English. Preload the locales your picker offers with side-effect imports:

```ts
import '@videojs/html/i18n/locales/es/register';
import '@videojs/html/i18n/locales/fr/register';
```

### Set text direction

`lang` identifies the content language. `dir` controls text and layout direction. Browsers do not infer one from the other, so set both when the locale applies to the whole document:

```ts
document.documentElement.lang = 'ar';
document.documentElement.dir = 'rtl';
```

Set `dir` back to `ltr` when you switch to a left-to-right language. This prevents the previous direction from remaining active.

`<media-i18n lang>` sets the provider locale and derives its direction. An explicit `dir` on `<media-i18n>` overrides that derived direction.

Providers without an explicit locale inherit ambient document language and direction. In that case, set `lang` and `dir` on `<html>` or another ancestor:

```html
<html lang="ar" dir="rtl">
```

RTL preserves playback control ordering and time-semantic media icons while mirroring menu motion and navigation chevrons. Horizontal time and volume sliders retain a physical left-to-right value scale: Left Arrow decreases the value, Right Arrow increases it, and the minimum remains on the left.

### Replace a component’s visible text

Translation overrides change built-in labels everywhere they appear. To replace only one component’s visible content, set its children — and use the translation machinery when that content should still follow the active locale:

```html
<media-live-button>
  <media-text token="live.badge">On air</media-text>
</media-live-button>
```

For state-dependent text, add one `<media-text>` per state and show or hide them with the component’s `data-*` state attributes (the same pattern used for icons).

The component still derives its accessible name from media state unless you also set its label API. Keep custom visible text consistent with that name.

### Server rendering

Render the document language on the server (`<html lang="es">`) and make translations available before first paint so labels match on server and client:

Emit `lang`, wrap the player with `<media-i18n>`, and register packs in the entry module loaded before custom elements upgrade:

```ts
import '@videojs/html/video/player';
import '@videojs/html/i18n/locales/es/register';
```

> **Caution**
>
> Don’t rely on browser-only translation APIs during SSR. Ship packs or register strings explicitly.

## Troubleshooting

### Labels stay in English

No provider is mounted, or the locale never resolved: check the `locale`/`lang` value and that it matches a shipped pack (regional tags fall back — `pt-BR` works; a bare unknown tag falls through to English).

### Some strings are translated, others aren’t

A partial override or partial pack: missing keys fall back through the locale chain to English. Fill in the missing keys with `registerI18n`.

### Translations flash in after load or after switching

The lazy pack loads after first paint (or after the switch). Register the pack with the bundle (the `/register` side-effect import), or prefetch and register before changing the locale.

### Server-rendered labels don’t match the client

The provider resolved different locales on server and client. Pass an explicit `locale` and first-render `translations` instead of relying on ambient `lang`.

## Related pages

### Components

- [media-i18n](https://videojs.org/docs/framework/html/reference/api/media-i18n): HTML custom element that resolves locale and supplies translations to descendants
- [media-text](https://videojs.org/docs/framework/html/reference/api/media-text): HTML custom element that renders a translated string by semantic key

### API

- [registerI18n](https://videojs.org/docs/framework/html/reference/api/register-i18n): Register or merge translation strings for a BCP 47 locale tag in the global i18n registry
- [Translation phrases](https://videojs.org/docs/framework/html/reference/api/translation-phrases): Semantic i18n keys, their English defaults, and the player UI that uses them
- [I18nController](https://videojs.org/docs/framework/html/reference/api/i18n-controller): Reactive controller for consuming translator and locale values from an HTML i18n context
- [getI18nTranslations](https://videojs.org/docs/framework/html/reference/api/get-i18n-translations): Read the merged translation map for a locale using BCP 47 parent-chain fallback

### Guides

- [Show captions and subtitles](https://videojs.org/docs/framework/html/guides/captions): Show captions and subtitles, and let users turn them on and pick a language.
- [Accessibility](https://videojs.org/docs/framework/html/guides/accessibility): How Video.js approaches accessibility, and what you should consider if you're deeply customizing your player

---

HTML documentation: https://videojs.org/docs/framework/html/llms.txt
All documentation: https://videojs.org/llms.txt
