Skip to content

GuidePlayback

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.

Wrap the player in the i18n provider. It resolves the locale from your page’s lang attribute or an explicit override, then lazy-loads the matching locale pack. Every component label follows that locale.

<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';
  import '@videojs/html/ui/container';
</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:

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

How it works

Component labels carry a translation key and an English default. The provider loads the resolved locale’s pack on demand. Locale fallback follows BCP 47 parent tags: es-MX checks es, then en.

<media-i18n> uses its own lang, then the nearest ancestor’s lang, then 'en'. It follows changes to the ancestor language when its own lang is omitted.

Each label uses the first available value:

  1. The shared registry
  2. The lazy-loaded locale pack
  3. The component’s English default

registerI18n(locale, translations) adds or replaces keys in the shared registry. Mounted providers pick up later registrations. Registering any strings for a locale prevents its pack from loading automatically. For partial global overrides, import the locale’s /register entry first, as shown below. This keeps the full translation available after a locale switch or provider remount.

When no shipped pack matches, the provider can use the browser’s on-device Translation API. Supply your own translations when you need reviewed wording or consistent availability across browsers.

Translate your own UI with the same machinery:

<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 (es, pt-BR).
  • Translation keys are namespaced strings (buttons.play, errors.network, menu.quality). The full list is in Translation phrases. The keys are 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:
// ✅
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

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

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

Import the full pack before registering partial overrides so the remaining labels stay in Japanese. If you register overrides after the pack lazy-loads, they apply immediately. After a locale switch or remount, the provider will not reload the pack, so the remaining labels can fall back to English. Importing /register first preserves those labels.

Register your own locale

Register strings for a locale your application supports. Missing keys fall back to registered parent strings, then a shipped parent pack, then English. For example, register a few es-MX labels and the es pack supplies the rest. If the tag has its own shipped pack, registering strings for it prevents that pack from loading. Use the full-pack pattern above for that case.

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

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

registerI18n('es-MX', esMX);

Register a pack with the bundle

Skip lazy loading by registering a shipped locale at startup with a side-effect import. Labels render in that language on first paint:

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:

<script type="module" src="https://cdn.jsdelivr.net/npm/@videojs/cdn@10.0.1/locales/es.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/@videojs/cdn@10.0.1/video.js"></script>

Use the same pinned version for your locale pack, i18n.js, and player script. Copies loaded in the same window share a registry; matching versions keeps their translation keys and APIs compatible. Import the full locale pack before partial overrides, and register before the player loads to translate the first render:

import 'https://cdn.jsdelivr.net/npm/@videojs/cdn@10.0.1/locales/es.js';
import { registerI18n } from 'https://cdn.jsdelivr.net/npm/@videojs/cdn@10.0.1/i18n.js';

registerI18n('es', { buttons: { play: 'Reproducir', pause: 'Pausa' } });
<script type="module" src="/my-locale.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/@videojs/cdn@10.0.1/video.js"></script>

Switch locale at runtime

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

document.documentElement.lang = 'fr';

Switching through the document language works only when <media-i18n> has no explicit lang attribute. To switch explicitly, set the provider’s lang:

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:

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:

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 lang="ar" dir="rtl">

In RTL layouts, playback controls keep their order and time-related media icons keep their direction. Menu motion and navigation chevrons mirror. 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:

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

import '@videojs/html/i18n/locales/es/register';
import '@videojs/html/i18n';
import '@videojs/html/video/player';
import '@videojs/html/ui/container';

Troubleshooting

Labels stay in English

Check that a provider is mounted and that its locale/lang value matches a shipped pack. Regional tags fall back to their base language, so pt-BR works. An unmatched language falls back to English unless the browser’s Translation API supplies translations; register your own pack for consistent coverage.

Some strings are translated, others aren’t

If you registered partial global overrides before the locale pack loaded, the loader skips that pack. Import its /register entry before your overrides. For a custom locale, supply missing keys in your pack; they fall back to English until you do.

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.

Components

API

Guides