Internationalization
How Video.js chooses a language and translates built-in player text
Video.js displays its controls in English by default. Use internationalization when the player should match another language used by your site or application.
Set the page language and add an internationalization provider around the player. Video.js then translates built-in labels, tooltips, status messages, and error text.
Choose the player language
The provider reads the nearest lang attribute unless you give it a language directly. Setting <html lang="es"> lets the page and player share Spanish.
Wrap the player with <media-i18n>. Choose any shipped language to lazy-load its translations and update the player’s control labels.
<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>
.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-player,
.html-i18n-language video-skin,
.html-i18n-language video {
display: block;
width: 100%;
}
.html-i18n-language video-skin {
aspect-ratio: 16 / 9;
}
import { LOCALES } from '@videojs/html/i18n';
import '@videojs/html/video/player';
import '@videojs/html/video/skin';
const root = document.querySelector<HTMLElement>('.html-i18n-language');
const select = root?.querySelector('select');
const provider = root?.querySelector('media-i18n');
const languageNames = new Intl.DisplayNames(['en'], { type: 'language' });
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);
});
Set lang on <media-i18n> when one player needs a different language from the rest of the page.
Mount I18nProvider inside the player Provider. Choose any shipped language to lazy-load its translations and update the player’s control labels.
import { createPlayer } from '@videojs/react';
import { I18nProvider, LOCALES } from '@videojs/react/i18n';
import { Video, VideoSkin, videoFeatures } from '@videojs/react/video';
import { useState } from 'react';
import '@videojs/react/video/skin.css';
import './Language.css';
const Player = createPlayer({ features: videoFeatures });
const locales = ['en', ...LOCALES] as const;
type Locale = (typeof locales)[number];
const languageNames = new Intl.DisplayNames(['en'], { type: 'language' });
export default function Language() {
const [locale, setLocale] = useState<Locale>('en');
return (
<div className="react-i18n-language">
<label>
Language
<select value={locale} onChange={(event) => setLocale(event.currentTarget.value as Locale)}>
{locales.map((value) => (
<option key={value} value={value}>
{languageNames.of(value) ?? value}
</option>
))}
</select>
</label>
<Player.Provider>
<I18nProvider locale={locale}>
<VideoSkin className="react-i18n-language__player">
<Video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" muted playsInline />
</VideoSkin>
</I18nProvider>
</Player.Provider>
</div>
);
}
.react-i18n-language {
display: grid;
gap: 16px;
padding: 16px;
}
.react-i18n-language label {
display: flex;
gap: 8px;
align-items: center;
}
.react-i18n-language select {
padding: 4px 8px;
}
.react-i18n-language__player {
width: 100%;
aspect-ratio: 16 / 9;
}
Pass locale="es" to I18nProvider when one player needs a different language from the rest of the page.
Changing the active language updates mounted controls without remounting the player. See Switch locale dynamically for complete examples.
How translated text works
Each built-in piece of text has two parts:
- A stable key that identifies its meaning, such as
buttons.play - English fallback text, such as
Play
Translation packs replace the fallback for a language:
{
buttons: {
play: 'Reproducir',
},
}You can count on this key staying the same, even if we change the English wording. For example, if we changed “Play” to “Banana” in the UI, the key would still be buttons.play.
You don’t have to include every key in a translation. Missing keys display their readable English fallback.
Some translations include values supplied by the component. For example, seek.forward uses the English fallback Seek forward {seconds} seconds. When you write a translation, make sure to include {seconds} where the number should appear.
See Translation keys for the complete list of keys, English defaults, and the controls that use them.
Where translations come from
Video.js looks for translated text in these places:
- Overrides supplied to the current player
- Registered custom translations
- A built-in locale pack
- The component’s English fallback
Choose a source, then hover the play button to see its resolved label.
import { createPlayer } from '@videojs/react';
import { I18nProvider } from '@videojs/react/i18n';
import { Video, VideoSkin, videoFeatures } from '@videojs/react/video';
import '@videojs/react/video/skin.css';
import './Sources.css';
const Player = createPlayer({ features: videoFeatures });
export default function Override() {
return (
<Player.Provider>
<I18nProvider locale="en" translations={{ buttons: { play: 'Player override' } }}>
<div className="react-i18n-source">
<VideoSkin className="react-i18n-source__player">
<Video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" muted playsInline preload="none" />
</VideoSkin>
</div>
</I18nProvider>
</Player.Provider>
);
}
import { createPlayer } from '@videojs/react';
import { I18nProvider, registerI18n } from '@videojs/react/i18n';
import { Video, VideoSkin, videoFeatures } from '@videojs/react/video';
import '@videojs/react/video/skin.css';
import './Sources.css';
registerI18n('en-x-demo', {
buttons: {
play: 'Registered custom translation',
},
});
const Player = createPlayer({ features: videoFeatures });
export default function Registered() {
return (
<Player.Provider>
<I18nProvider locale="en-x-demo">
<div className="react-i18n-source">
<VideoSkin className="react-i18n-source__player">
<Video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" muted playsInline preload="none" />
</VideoSkin>
</div>
</I18nProvider>
</Player.Provider>
);
}
import { createPlayer } from '@videojs/react';
import { I18nProvider } from '@videojs/react/i18n';
import { Video, VideoSkin, videoFeatures } from '@videojs/react/video';
import '@videojs/react/video/skin.css';
import './Sources.css';
const Player = createPlayer({ features: videoFeatures });
export default function BuiltIn() {
return (
<Player.Provider>
<I18nProvider locale="es">
<div className="react-i18n-source">
<VideoSkin className="react-i18n-source__player">
<Video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" muted playsInline preload="none" />
</VideoSkin>
</div>
</I18nProvider>
</Player.Provider>
);
}
import { createPlayer } from '@videojs/react';
import { I18nProvider } from '@videojs/react/i18n';
import { Video, VideoSkin, videoFeatures } from '@videojs/react/video';
import '@videojs/react/video/skin.css';
import './Sources.css';
const Player = createPlayer({ features: videoFeatures });
export default function Fallback() {
return (
<Player.Provider>
<I18nProvider locale="en">
<div className="react-i18n-source">
<VideoSkin className="react-i18n-source__player">
<Video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" muted playsInline preload="none" />
</VideoSkin>
</div>
</I18nProvider>
</Player.Provider>
);
}
- Registered custom translations
- A built-in locale pack
- The component’s English fallback
Choose a source, then hover the play button to see its resolved label.
import { registerI18n } from '@videojs/html/i18n';
import '@videojs/html/video/player';
import '@videojs/html/video/skin';
registerI18n('en-x-demo', {
buttons: {
play: 'Registered custom translation',
},
});
<div class="html-i18n-source">
<media-i18n lang="es">
<video-player>
<video-skin>
<video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" muted playsinline preload="none"></video>
</video-skin>
</video-player>
</media-i18n>
</div>
<div class="html-i18n-source">
<media-i18n lang="en">
<video-player>
<video-skin>
<video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" muted playsinline preload="none"></video>
</video-skin>
</video-player>
</media-i18n>
</div>
Built-in packs load when the provider first encounters their language. You do not need to import Spanish, French, or another shipped language for normal client-rendered players.
The translations prop on I18nProvider overrides text for that provider subtree:
<I18nProvider locale="es" translations={{ buttons: { play: 'Comenzar' } }}>
<VideoSkin>...</VideoSkin>
</I18nProvider>Use registerI18n when you have a custom locale or want an override to apply across players:
registerI18n('es', {
buttons: {
play: 'Comenzar',
},
});If registered and built-in translations still leave text missing, Video.js automatically tries Chrome’s Browser Translation API. You do not need to call the API. This fallback only runs when Chrome exposes the API and the required on-device model is already installed. Video.js does not download the model. Use reviewed locale packs for production-critical text.
Locale fallback
Language tags follow the BCP 47 parent chain. A player using Mexican Spanish checks es-MX, then es, before falling back to English:
es-MX → es → enThis lets a regional pack override only the text that differs from its parent language.
Avoid a flash of English
Built-in packs normally load after the provider mounts. A player can briefly show English during server rendering or the first switch to a language.
Preload or register translations before rendering when the first translated paint matters: