docs(site): add i18n concept, guides, and API reference pages

Documents opaque keys, registry APIs, React/HTML providers, and locale
workflows for #1373. Extends api-docs-builder discovery for core/react
i18n exports and adds matching reference MDX pages.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Sam Potts
2026-07-13 08:22:39 +10:00
co-authored by Cursor
parent 82b9e43bb5
commit fd54f36110
17 changed files with 945 additions and 0 deletions
+155
View File
@@ -0,0 +1,155 @@
---
title: Internationalization (i18n)
description: How Video.js translates player UI copy with opaque keys, a global registry, and locale providers
frameworkTitle:
html: Internationalization in HTML
react: Internationalization in React
---
import FrameworkCase from '@/components/docs/FrameworkCase.astro';
import Aside from '@/components/Aside.astro';
import DocsLink from '@/components/docs/DocsLink.astro';
Video.js translates control labels, ARIA text, tooltips, and error copy through a single **global registry**. Components ask for strings by **opaque key** (`play`, `pause`, `seekForward`) — not by English text — so locale files stay stable when copy changes.
```html title="html"
<html lang="es">
<video-player>
<video-skin>
<video src="..." playsinline></video>
</video-skin>
</video-player>
</html>
```
```tsx title="react"
<html lang="es">
<Provider>
<VideoSkin>
<Video src="..." playsInline />
</VideoSkin>
</Provider>
</html>
```
Register a locale once (or rely on lazy-loaded built-in packs), set `lang`, and skins pick up translated strings automatically.
## Opaque keys
Core controls expose keys, not visible labels. `PlayButtonCore.getLabel()` returns `'play'`; the translator turns that into `'Play'`, `'Reproducir'`, or your override.
Keys are typed in `TranslationParams` — TypeScript catches missing `{param}` placeholders and wrong argument names at compile time. See `packages/core/src/core/i18n/types.ts` for the full key list.
Parametric strings use `{placeholder}` tokens, for example `seekForward: 'Seek forward {seconds} seconds'` and `timeRemainingPhrase: '{duration} remaining'`.
## Global registry
`registerI18n(locale, translations)` merges strings into a process-wide map. English (`en`) is pre-registered when `@videojs/core/i18n` loads.
| API | Purpose |
| --- | --- |
| <DocsLink slug="reference/register-i18n">`registerI18n`</DocsLink> | Add or merge a locale layer |
| <DocsLink slug="reference/get-i18n-translations">`getI18nTranslations`</DocsLink> | Read the merged map for a locale |
| <DocsLink slug="reference/has-registered-i18n">`hasRegisteredI18n`</DocsLink> | Check whether a tag is in the registry |
| <DocsLink slug="reference/on-i18n-registry-change">`onI18nRegistryChange`</DocsLink> | Subscribe to registry updates |
Import from `@videojs/html/i18n`, `@videojs/react/i18n`, or `@videojs/core/i18n` depending on your bundle.
## When you need a provider
**You usually don't** — if the active locale is available and packs are registered (or lazy-loaded), built-in players already wire providers:
<FrameworkCase frameworks={["html"]}>
- `<video-player>`, `<audio-player>`, and skins include an i18n provider mixin.
- Set `<html lang="es">` (or `lang` on an ancestor) and ship/register Spanish strings.
Use <DocsLink slug="reference/media-i18n-provider">`<media-i18n-provider>`</DocsLink> when you render **standalone** controls outside a player, or when one page hosts players in different languages.
</FrameworkCase>
<FrameworkCase frameworks={["react"]}>
- Preset skins render inside <DocsLink slug="reference/player-container">`Container`</DocsLink>, which mounts <DocsLink slug="reference/i18n-provider">`I18nProvider`</DocsLink> with `langRootRef` on the player shell.
- Set `<html lang="es">` and register or lazy-load Spanish.
Wrap with an explicit <DocsLink slug="reference/i18n-provider">`I18nProvider`</DocsLink> when you need a forced locale, per-render overrides, or custom controls outside `Container`.
</FrameworkCase>
**You do need to register or lazy-load packs** before labels appear in the target language. `<html lang="fr">` alone does nothing if French strings were never registered and no built-in `fr` pack loads.
## Locale resolution
Providers resolve the active locale in order:
1. **Explicit** — `lang` on <DocsLink slug="reference/media-i18n-provider">`<media-i18n-provider>`</DocsLink> or `locale` on <DocsLink slug="reference/i18n-provider">`I18nProvider`</DocsLink>
2. **Ambient** — nearest ancestor `[lang]` (HTML) or `langRootRef` / `<html lang>` (React)
3. **Fallback** — English defaults
Changing `<html lang>` re-renders wired controls without remounting the player. An explicit `locale` / `lang` on a provider overrides ambient `<html lang>` until you remove or update that override.
## BCP 47 fallback
Lookups walk a **parent chain**, not sibling locales. `es-MX` falls back to `es`, then `en` — not to `es-419`.
```
es-MX → es → en
zh-Hant-HK → zh-hant → zh → en
```
`getI18nTranslations`, lazy `loadLocale`, and providers all use the same chain via `localeLookupChain`.
## Merge priority
Later layers win over earlier ones:
| Layer | Source |
| --- | --- |
| 1 (base) | English defaults (`en.ts`) |
| 2 | Browser Translation API (Chrome, pre-installed model only) |
| 3 | `registerI18n` / CDN locale modules |
| 4 | Lazy built-in packs (`loadLocale`) |
| 5 (top) | React `translations` prop on `I18nProvider` |
## Built-in locale packs
Video.js ships ~50 locale files under `@videojs/core/i18n/locales/*`, re-exported from `@videojs/html/i18n/locales/*` and `@videojs/react/i18n/locales/*`. Providers call `loadLocale` automatically when a pack is not already registered.
CDN consumers load self-registering modules:
```html
<script type="module" src="https://cdn.jsdelivr.net/npm/@videojs/html/cdn/video.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/@videojs/html/cdn/locales/es.js"></script>
```
## Common pitfalls
```tsx
// ❌ Don't — key is the English word; keys are opaque tokens
registerI18n('es', { Play: 'Reproducir' });
// ✅ Do
registerI18n('es', { play: 'Reproducir' });
```
```tsx
// ❌ Don't — parametric key without the placeholder
registerI18n('es', { seekForward: 'Adelante 10 segundos' }); // TS error: missing {seconds}
// ✅ Do
registerI18n('es', { seekForward: 'Adelantar {seconds} segundos' });
```
<Aside type="tip">
For zero flash-of-English on SSR or locale switches, pass `translations` directly to `I18nProvider` instead of relying on async `loadLocale`. See <DocsLink slug="how-to/i18n-ssr">SSR with locale</DocsLink>.
</Aside>
## See also
- <DocsLink slug="how-to/i18n-register-locale">Register a custom locale</DocsLink>
- <DocsLink slug="how-to/i18n-override-translations">Override individual keys</DocsLink>
- <DocsLink slug="how-to/i18n-switch-locale">Switch locale dynamically</DocsLink>
- <DocsLink slug="how-to/i18n-ssr">SSR and hydration</DocsLink>
- <DocsLink slug="how-to/i18n-add-built-in-locale">Add a built-in locale (contributors)</DocsLink>
- <DocsLink slug="concepts/accessibility">Accessibility</DocsLink> — translated ARIA labels
@@ -0,0 +1,85 @@
---
title: Add a built-in locale
description: Ship a new translation pack in @videojs/core for contributors
---
import Aside from '@/components/Aside.astro';
import DocsLink from '@/components/docs/DocsLink.astro';
Built-in locales live in `packages/core/src/core/i18n/locales/`. The build generates lazy loaders, CDN chunks, and HTML/React re-exports from that directory.
This guide is for **contributors** adding or updating shipped packs — app authors should use <DocsLink slug="how-to/i18n-register-locale">Register a custom locale</DocsLink> instead.
### Prerequisites
- Familiarity with <DocsLink slug="concepts/i18n">Internationalization</DocsLink> and `TranslationParams`
- English defaults in `packages/core/src/core/i18n/locales/en.ts` as the source of keys
## 1. Add the locale file
Create `packages/core/src/core/i18n/locales/{tag}.ts` using the BCP 47 tag as the filename (`pt-BR.ts`, `zh-CN.ts`):
```ts title="packages/core/src/core/i18n/locales/xx.ts"
import type { Translations } from '../types';
export default {
play: '…',
pause: '…',
// all keys from en.ts — each entry optional but completeness is preferred
} satisfies Partial<Translations>;
```
Use **opaque keys** from `en.ts`, not English sentences as keys. Parametric strings must include the same `{placeholder}` tokens (TypeScript enforces this via `satisfies Partial<Translations>`).
Copy guidelines:
- `liveBadge` — sentence case (`Live`); skins uppercase via CSS where needed
- `seek` — slider aria label (`Seek`), not legacy "Progress" wording
- `timeRemainingPhrase` — `'{duration} remaining'` pattern with `{duration}` placeholder
- Button and error strings — short aria-label style, aligned with V10 core label usage
## 2. Register the tag in built-in metadata
Add the tag to `BUILT_IN_LOCALES` in `packages/core/src/core/i18n/built-in-locales.ts`. Add alias tags (`pt`, `zh`) to `LOCALE_ALIAS_TAGS` only when intentional.
## 3. Regenerate loaders
From the repo root:
```bash
pnpm -F @videojs/core build
```
This runs `generate:locales` and updates:
- `packages/core/src/core/i18n/load-locale.ts`
- `packages/core/src/core/i18n/locales/all.ts`
- `packages/html/src/i18n/locales/*` and `packages/react/src/i18n/locales/*` re-exports
- CDN locale stubs via `packages/html/scripts/build-cdn-locales.ts` when building HTML CDN output
Do not hand-edit generated files.
## 4. Test
```bash
pnpm -F @videojs/core test src/core/i18n
```
Add or extend locale tests if the tag introduces alias or loader edge cases.
## 5. CDN locale chunk
CDN builds emit `cdn/locales/{tag}.js` that import the pack and call `registerI18n`. Verify with:
```bash
pnpm -F @videojs/html build:cdn
```
<Aside type="note">
Do not copy strings blindly from Video.js v8 `lang/` JSON — v10 uses different keys and aria-label semantics. Translate from `en.ts` keys and core `getLabel` usage.
</Aside>
## What's next?
- Open a PR with the locale file and `built-in-locales.ts` change only (generated outputs come from CI/build)
- <DocsLink slug="concepts/i18n">Internationalization concept</DocsLink>
@@ -0,0 +1,83 @@
---
title: Override translation keys
description: Patch individual i18n strings without replacing an entire locale pack
---
import FrameworkCase from '@/components/docs/FrameworkCase.astro';
import DocsLink from '@/components/docs/DocsLink.astro';
`registerI18n` **merges** — each call adds or replaces keys for that locale tag without wiping prior registrations. Use this to tweak shipped packs or A/B test copy.
Read <DocsLink slug="concepts/i18n">Internationalization</DocsLink> for merge order and key naming.
## Override after a built-in pack
```ts
import { registerI18n } from '@videojs/html/i18n';
import es from '@videojs/html/i18n/locales/es';
registerI18n('es', es);
registerI18n('es', { play: 'Comenzar' }); // only `play` changes
```
Later registrations win for the same key. The rest of the `es` pack stays intact.
## React provider overrides
Pass `translations` on <DocsLink slug="reference/i18n-provider">`I18nProvider`</DocsLink> to scope overrides to one subtree. This layer sits **above** registry and lazy packs:
<FrameworkCase frameworks={["react"]}>
```tsx
import { I18nProvider } from '@videojs/react/i18n';
import { Provider, VideoSkin, Video } from '@videojs/react/video';
<Provider>
<I18nProvider locale="es" translations={{ play: 'Comenzar' }}>
<VideoSkin>
<Video src="..." playsInline />
</VideoSkin>
</I18nProvider>
</Provider>
```
Nested providers inherit the parent locale when you only pass `translations`:
```tsx
<I18nProvider locale="de">
<I18nProvider translations={{ play: 'Override' }}>
{/* locale stays `de`; only `play` differs */}
</I18nProvider>
</I18nProvider>
```
</FrameworkCase>
## HTML reflected text
Use <DocsLink slug="reference/media-text">`<media-text>`</DocsLink> for static copy outside buttons:
```html
<media-i18n-provider lang="es">
<media-text key="play"></media-text>
</media-i18n-provider>
```
Override the key in the registry or set `lang` on the provider to a locale where you patched `play`.
## Parametric keys
Keep required placeholders when overriding:
```ts
// ✅
registerI18n('es', { seekForward: 'Adelantar {seconds} segundos' });
// ❌ TypeScript error — missing {seconds}
registerI18n('es', { seekForward: 'Adelantar' });
```
## What's next?
- <DocsLink slug="how-to/i18n-switch-locale">Switch locale dynamically</DocsLink>
- <DocsLink slug="reference/get-i18n-translations">`getI18nTranslations`</DocsLink> — inspect the merged map
@@ -0,0 +1,131 @@
---
title: Register a custom locale
description: Register translation packs for HTML, React, and CDN players
---
import FrameworkCase from '@/components/docs/FrameworkCase.astro';
import Aside from '@/components/Aside.astro';
import DocsLink from '@/components/docs/DocsLink.astro';
Register strings once with <DocsLink slug="reference/register-i18n">`registerI18n`</DocsLink>, then set `lang` on the page or pass `locale` to a provider. Read <DocsLink slug="concepts/i18n">Internationalization</DocsLink> first if you are new to opaque keys and the registry.
### Prerequisites
- A locale tag ([BCP 47](https://www.rfc-editor.org/rfc/rfc5646), e.g. `es`, `pt-BR`)
- A partial translation map keyed by camelCase tokens (`play`, `pause`, …)
## Use a shipped pack
Built-in packs live in `@videojs/*/i18n/locales/*`. Import and register before the player renders:
<FrameworkCase frameworks={["html"]}>
```ts title="main.ts"
import '@videojs/html/video';
import { registerI18n } from '@videojs/html/i18n';
import es from '@videojs/html/i18n/locales/es';
registerI18n('es', es);
```
```html title="index.html"
<html lang="es">
<video-player>
<video-skin>
<video src="..." playsinline></video>
</video-skin>
</video-player>
</html>
```
If you skip `registerI18n`, the built-in provider still **lazy-loads** `es` when `lang="es"` — explicit registration avoids the async gap on first paint.
</FrameworkCase>
<FrameworkCase frameworks={["react"]}>
```tsx title="app.tsx"
import { registerI18n } from '@videojs/react/i18n';
import es from '@videojs/react/i18n/locales/es';
import { Provider, VideoSkin, Video } from '@videojs/react/video';
registerI18n('es', es);
export function App() {
return (
<Provider>
<VideoSkin>
<Video src="..." playsInline />
</VideoSkin>
</Provider>
);
}
```
Preset skins use `Container`, which includes <DocsLink slug="reference/i18n-provider">`I18nProvider`</DocsLink>. Set `<html lang="es">` or wrap with `<I18nProvider locale="es">`.
</FrameworkCase>
## Register your own pack
```ts title="my-es.ts"
import type { Translations } from '@videojs/core/i18n';
const es: Partial<Translations> = {
play: 'Reproducir',
pause: 'Pausa',
mute: 'Silenciar',
unmute: 'Activar sonido',
};
export default es;
```
```ts
import { registerI18n } from '@videojs/html/i18n'; // or @videojs/react/i18n
import es from './my-es';
registerI18n('es', es);
```
All keys are optional — missing keys fall back through the BCP 47 chain to English.
## CDN
Load the player and a locale chunk. Locale modules call `registerI18n` on import:
```html
<script type="module" src="https://cdn.jsdelivr.net/npm/@videojs/html/cdn/video.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/@videojs/html/cdn/locales/es.js"></script>
<html lang="es">
<video-player>
<video-skin>
<video src="..." playsinline></video>
</video-skin>
</video-player>
</html>
```
<Aside type="note">
CDN locale files import `registerI18n` from the shared `cdn/i18n.js` bundle so every chunk shares one registry instance.
</Aside>
## Custom CDN locale
```js title="my-locale.js"
import { registerI18n } from 'https://cdn.jsdelivr.net/npm/@videojs/html/cdn/i18n.js';
registerI18n('es', {
play: 'Reproducir',
pause: 'Pausa',
});
```
Load your module after the player script, before playback starts.
## What's next?
- <DocsLink slug="how-to/i18n-override-translations">Override individual keys</DocsLink>
- <DocsLink slug="how-to/i18n-switch-locale">Switch locale at runtime</DocsLink>
- <DocsLink slug="reference/register-i18n">`registerI18n` reference</DocsLink>
+100
View File
@@ -0,0 +1,100 @@
---
title: SSR and locale
description: Render translated player UI on the server without a flash of English
---
import FrameworkCase from '@/components/docs/FrameworkCase.astro';
import Aside from '@/components/Aside.astro';
import DocsLink from '@/components/docs/DocsLink.astro';
Server-rendered pages should output the correct `lang` **and** supply translations on the first client render. Lazy `loadLocale` runs after hydration — without preloaded copy, controls briefly show English.
Read <DocsLink slug="concepts/i18n">Internationalization</DocsLink> for provider resolution and merge order.
## Set `<html lang>`
Render the document language on the server:
```html
<html lang="es">
```
React providers use a server snapshot of `undefined` for ambient lang, then read `<html lang>` on hydration. Passing `locale` explicitly avoids any mismatch.
## Preload translations (React)
Import the locale pack on the server (or in a server component) and pass it to <DocsLink slug="reference/i18n-provider">`I18nProvider`</DocsLink>:
<FrameworkCase frameworks={["react"]}>
```tsx
import { I18nProvider } from '@videojs/react/i18n';
import { Provider, VideoSkin, Video } from '@videojs/react/video';
export async function Player({ locale }: { locale: string }) {
const { default: translations } = await import(`@videojs/react/i18n/locales/${locale}`);
return (
<Provider>
<I18nProvider locale={locale} translations={translations}>
<VideoSkin>
<Video src="..." playsInline />
</VideoSkin>
</I18nProvider>
</Provider>
);
}
```
`translations` on the first render skips the async lazy layer — labels match on server and client.
</FrameworkCase>
## Register on the server
For custom packs, call <DocsLink slug="reference/register-i18n">`registerI18n`</DocsLink> in server bootstrap code before rendering players:
```ts
import { registerI18n } from '@videojs/react/i18n';
import es from './locales/es';
registerI18n('es', es);
```
The registry is module singleton state — registration in server entry points carries into the client bundle when shared.
## next-intl and app routers
```tsx
import { getLocale } from 'next-intl/server';
const locale = await getLocale();
const { default: translations } = await import(`@videojs/react/i18n/locales/${locale}`);
<I18nProvider locale={locale} translations={translations}>
...
</I18nProvider>
```
Match your app's locale negotiation — Video.js does not replace framework i18n; it consumes the tag you pass.
## HTML custom elements
SSR HTML players should emit `lang` on `<html>` or on <DocsLink slug="reference/media-i18n-provider">`<media-i18n-provider>`</DocsLink>. Register packs in the entry module loaded before custom elements upgrade:
```ts
import '@videojs/html/video';
import { registerI18n } from '@videojs/html/i18n';
import es from '@videojs/html/i18n/locales/es';
registerI18n('es', es);
```
<Aside type="caution">
Do not rely on browser-only APIs (`getBrowserTranslations`) during SSR. Ship packs or register strings explicitly.
</Aside>
## What's next?
- <DocsLink slug="how-to/i18n-switch-locale">Switch locale at runtime</DocsLink>
- <DocsLink slug="how-to/i18n-register-locale">Register a custom locale</DocsLink>
@@ -0,0 +1,121 @@
---
title: Switch locale dynamically
description: Change the player language at runtime in HTML and React
---
import FrameworkCase from '@/components/docs/FrameworkCase.astro';
import Aside from '@/components/Aside.astro';
import DocsLink from '@/components/docs/DocsLink.astro';
Video.js re-resolves translations when the active locale changes. How you trigger that depends on the platform.
### Prerequisites
- <DocsLink slug="how-to/i18n-register-locale">Locale packs registered or lazy-loadable</DocsLink>
## Ambient `<html lang>`
The simplest switch — update the document language and let providers pick it up:
```ts
document.documentElement.lang = 'fr';
```
<Aside type="note">
Ambient switching only applies when no provider sets an explicit locale. React `I18nProvider locale={…}`, HTML `<media-i18n-provider lang="…">`, and `element.lang` on a provider all override `<html lang>` until you remove or update that override.
</Aside>
<FrameworkCase frameworks={["html"]}>
Player elements and skins observe `lang` on `<html>` and ancestors. No remount required.
</FrameworkCase>
<FrameworkCase frameworks={["react"]}>
`Container`'s <DocsLink slug="reference/i18n-provider">`I18nProvider`</DocsLink> reads `langRootRef` from the player shell. Updating `<html lang>` is enough when you are not forcing `locale` on an outer provider.
</FrameworkCase>
## Explicit provider locale
<FrameworkCase frameworks={["react"]}>
```tsx
import { useState } from 'react';
import { I18nProvider } from '@videojs/react/i18n';
import { Provider, VideoSkin, Video } from '@videojs/react/video';
function App() {
const [locale, setLocale] = useState<'en' | 'es' | 'fr'>('en');
return (
<>
<button type="button" onClick={() => setLocale('es')}>
Español
</button>
<Provider>
<I18nProvider locale={locale}>
<VideoSkin>
<Video src="..." playsInline />
</VideoSkin>
</I18nProvider>
</Provider>
</>
);
}
```
Changing `locale` triggers lazy `loadLocale` for built-in packs.
</FrameworkCase>
<FrameworkCase frameworks={["html"]}>
```html
<media-i18n-provider lang="es" id="provider">
<video-player>...</video-player>
</media-i18n-provider>
<script type="module">
document.querySelector('#lang-picker').addEventListener('change', (e) => {
document.getElementById('provider').lang = e.target.value;
});
</script>
```
Or set `document.documentElement.lang` if the player inherits ambient language.
</FrameworkCase>
## Avoid flash while switching
Async lazy loads can briefly show English. Pre-import the pack and pass `translations`:
<FrameworkCase frameworks={["react"]}>
```tsx
async function switchTo(next: string) {
const { default: translations } = await import(`@videojs/react/i18n/locales/${next}`);
setState({ locale: next, translations });
}
<I18nProvider locale={state.locale} translations={state.translations}>
...
</I18nProvider>
```
</FrameworkCase>
<Aside type="tip">
For server-rendered first paint, see <DocsLink slug="how-to/i18n-ssr">SSR and hydration</DocsLink>.
</Aside>
## CDN
Reload or navigate with an updated `locale` query parameter, or load a different locale module before playback. CDN shells typically full-reload on locale change because locale script tags are not hot-swappable.
## What's next?
- <DocsLink slug="how-to/i18n-ssr">SSR with locale</DocsLink>
- <DocsLink slug="reference/use-locale">`useLocale`</DocsLink> — read the active locale in React
@@ -0,0 +1,32 @@
---
title: createI18n
description: Factory that creates an isolated React i18n context stack with I18nProvider and hooks
---
import UtilReference from "@/components/docs/api-reference/UtilReference.astro";
import DocsLink from "@/components/docs/DocsLink.astro";
import FrameworkCase from "@/components/docs/FrameworkCase.astro";
<FrameworkCase frameworks={["react"]}>
`createI18n` mirrors <DocsLink slug="reference/create-player">`createPlayer`</DocsLink>: it returns a dedicated `I18nProvider`, `useTranslator`, and `useLocale` for apps that need an isolated i18n stack. Most apps import the default exports from `@videojs/react/i18n` instead.
```tsx
import { createI18n } from '@videojs/react/i18n';
const { I18nProvider, useTranslator } = createI18n({
loadLocale: async (tag) => import(`./locales/${tag}.json`),
});
function App() {
return (
<I18nProvider locale="ja">
<Controls />
</I18nProvider>
);
}
```
<UtilReference util="createI18n" />
</FrameworkCase>
@@ -0,0 +1,20 @@
---
title: getI18nTranslations
description: Read the merged translation map for a locale using BCP 47 parent-chain fallback
---
import UtilReference from "@/components/docs/api-reference/UtilReference.astro";
import DocsLink from "@/components/docs/DocsLink.astro";
`getI18nTranslations` walks the BCP 47 lookup chain (`es-MX` → `es` → `en`) and merges registry layers into one map. Providers and `createTranslator` use this internally; call it directly when building custom UI outside the built-in mixins.
See <DocsLink slug="concepts/i18n">Internationalization</DocsLink> for merge order and fallback rules.
```ts
import { createTranslator, getI18nTranslations } from '@videojs/core/i18n';
const t = createTranslator(getI18nTranslations('pt-BR'), 'pt-BR');
t('play'); // merged Portuguese string
```
<UtilReference util="getI18nTranslations" />
@@ -0,0 +1,19 @@
---
title: hasRegisteredI18n
description: Check whether an exact locale tag exists in the global i18n registry
---
import UtilReference from "@/components/docs/api-reference/UtilReference.astro";
import DocsLink from "@/components/docs/DocsLink.astro";
`hasRegisteredI18n` returns whether a normalized locale tag has an explicit registry layer from `registerI18n`. It does **not** indicate whether a lazy built-in pack exists — only registry entries count.
```ts
import { hasRegisteredI18n, registerI18n } from '@videojs/html/i18n';
hasRegisteredI18n('fr'); // false until registered
registerI18n('fr', { play: 'Lecture' });
hasRegisteredI18n('fr'); // true
```
<UtilReference util="hasRegisteredI18n" />
@@ -0,0 +1,28 @@
---
title: I18nProvider
description: React provider that resolves locale and supplies a typed translator to descendants
---
import UtilReference from "@/components/docs/api-reference/UtilReference.astro";
import DocsLink from "@/components/docs/DocsLink.astro";
import FrameworkCase from "@/components/docs/FrameworkCase.astro";
<FrameworkCase frameworks={["react"]}>
`I18nProvider` resolves the active locale, merges registry and lazy pack layers, and exposes a translator through context. Preset skins mount it inside <DocsLink slug="reference/player-container">`Container`</DocsLink> with `langRootRef` on the player shell.
Wrap custom controls or force a locale explicitly:
```tsx
import { I18nProvider } from '@videojs/react/i18n';
<I18nProvider locale="de" translations={{ play: 'Abspielen' }}>
<MyControls />
</I18nProvider>
```
Omit `locale` to inherit the nearest `lang` attribute (via `langRootRef` or `<html lang>`). See <DocsLink slug="concepts/i18n">Internationalization</DocsLink> for merge priority and SSR guidance.
<UtilReference util="I18nProvider" />
</FrameworkCase>
@@ -0,0 +1,40 @@
---
title: media-i18n-provider
description: HTML custom element that resolves locale and supplies translations to descendants
---
import DocsLink from "@/components/docs/DocsLink.astro";
import FrameworkCase from "@/components/docs/FrameworkCase.astro";
<FrameworkCase frameworks={["html"]}>
`<media-i18n-provider>` applies the i18n provider mixin to a standalone subtree. Built-in `<video-player>` and skins already include this mixin — use the element when rendering **controls outside a player** or when sibling players need different locales.
Register locale strings with <DocsLink slug="reference/register-i18n">`registerI18n`</DocsLink>, then set `lang` on the provider or an ancestor. See <DocsLink slug="concepts/i18n">Internationalization</DocsLink> for opaque keys and fallback behavior.
```html
<script type="module">
import '@videojs/html/i18n';
import { registerI18n } from '@videojs/html/i18n';
import es from '@videojs/html/i18n/locales/es';
registerI18n('es', es);
</script>
<media-i18n-provider lang="es">
<media-text key="play"></media-text>
</media-i18n-provider>
```
## Attributes
| Attribute | Description |
| --- | --- |
| `lang` | Forces the active BCP 47 locale for this subtree. Omit to inherit the nearest ancestor `[lang]`. |
## Related
- <DocsLink slug="reference/media-text">`<media-text>`</DocsLink> — renders a translated string by key
- <DocsLink slug="how-to/i18n-register-locale">Register a custom locale</DocsLink>
</FrameworkCase>
@@ -0,0 +1,27 @@
---
title: media-text
description: HTML custom element that renders a translated string by opaque key
---
import DocsLink from "@/components/docs/DocsLink.astro";
import FrameworkCase from "@/components/docs/FrameworkCase.astro";
<FrameworkCase frameworks={["html"]}>
`<media-text>` renders the translated value for a registry key inside a <DocsLink slug="reference/media-i18n-provider">`<media-i18n-provider>`</DocsLink> (or any ancestor with the i18n provider mixin). Use it for standalone labels, tooltips, or demos — skin controls resolve keys internally.
```html
<media-i18n-provider lang="ja">
<media-text key="play"></media-text>
</media-i18n-provider>
```
## Attributes
| Attribute | Description |
| --- | --- |
| `key` | Opaque translation key (`play`, `pause`, `timeRemainingPhrase`, …). |
Parametric keys are not yet supported on `<media-text>` — use `createTranslator` in script for interpolated strings.
</FrameworkCase>
@@ -0,0 +1,12 @@
---
title: onI18nRegistryChange
description: Subscribe to global i18n registry mutations
---
import UtilReference from "@/components/docs/api-reference/UtilReference.astro";
`onI18nRegistryChange` registers a callback that runs whenever any locale layer changes — for example after `registerI18n` or browser translation prefetch. Returns an unsubscribe function.
React `I18nProvider` uses this to invalidate translators when the registry updates.
<UtilReference util="onI18nRegistryChange" />
@@ -0,0 +1,22 @@
---
title: registerI18n
description: Register or merge translation strings for a BCP 47 locale tag in the global i18n registry
---
import UtilReference from "@/components/docs/api-reference/UtilReference.astro";
import DocsLink from "@/components/docs/DocsLink.astro";
`registerI18n` merges a partial translation map into the process-wide registry for a locale tag. English defaults are registered when `@videojs/core/i18n` loads; call `registerI18n` for custom locales or to patch shipped packs before the player renders.
Import from `@videojs/core/i18n`, `@videojs/html/i18n`, or `@videojs/react/i18n`. Keys are opaque camelCase tokens (`play`, `pause`) — see <DocsLink slug="concepts/i18n">Internationalization</DocsLink>.
```ts
import { registerI18n } from '@videojs/react/i18n';
registerI18n('es', {
play: 'Reproducir',
pause: 'Pausar',
});
```
<UtilReference util="registerI18n" />
@@ -0,0 +1,27 @@
---
title: useLocale
description: React hook that returns the active BCP 47 locale from the nearest I18nProvider
---
import UtilReference from "@/components/docs/api-reference/UtilReference.astro";
import DocsLink from "@/components/docs/DocsLink.astro";
import FrameworkCase from "@/components/docs/FrameworkCase.astro";
<FrameworkCase frameworks={["react"]}>
`useLocale` returns the resolved BCP 47 tag from the nearest <DocsLink slug="reference/i18n-provider">`I18nProvider`</DocsLink>, or `'en'` when none is mounted.
Use it when UI copy depends on the active locale outside the translator — for example formatting or caption language hooks.
```tsx
import { useLocale } from '@videojs/react/i18n';
function LocaleBadge() {
const locale = useLocale();
return <span lang={locale}>{locale}</span>;
}
```
<UtilReference util="useLocale" />
</FrameworkCase>
@@ -0,0 +1,27 @@
---
title: useTranslator
description: React hook that returns the typed translator for the nearest I18nProvider
---
import UtilReference from "@/components/docs/api-reference/UtilReference.astro";
import DocsLink from "@/components/docs/DocsLink.astro";
import FrameworkCase from "@/components/docs/FrameworkCase.astro";
<FrameworkCase frameworks={["react"]}>
`useTranslator` returns the translator from the nearest <DocsLink slug="reference/i18n-provider">`I18nProvider`</DocsLink>. Control components pass opaque keys from core `getLabel()` — for example `t('play')` and `t('seekForward', { seconds: 10 })`.
When no provider is mounted, the hook falls back to English registry strings so standalone demos do not throw.
```tsx
import { useTranslator } from '@videojs/react/i18n';
function PlayLabel() {
const t = useTranslator();
return <span>{t('play')}</span>;
}
```
<UtilReference util="useTranslator" />
</FrameworkCase>
+16
View File
@@ -42,6 +42,7 @@ export const sidebar: Sidebar = [
{ slug: 'concepts/ui-components' },
{ slug: 'concepts/accessibility' },
{ slug: 'concepts/cast', sidebarLabel: 'Google Cast' },
{ slug: 'concepts/i18n', sidebarLabel: 'Internationalization' },
],
},
{
@@ -52,6 +53,11 @@ export const sidebar: Sidebar = [
{ slug: 'how-to/customize-skins' },
{ slug: 'how-to/build-your-own-component' },
{ slug: 'how-to/self-host-the-player', frameworks: ['html'] },
{ slug: 'how-to/i18n-register-locale', sidebarLabel: 'Register a locale' },
{ slug: 'how-to/i18n-override-translations', sidebarLabel: 'Override translations' },
{ slug: 'how-to/i18n-switch-locale', sidebarLabel: 'Switch locale' },
{ slug: 'how-to/i18n-ssr', sidebarLabel: 'SSR with locale' },
{ slug: 'how-to/i18n-add-built-in-locale', sidebarLabel: 'Add a built-in locale' },
],
},
{
@@ -105,6 +111,10 @@ export const sidebar: Sidebar = [
frameworks: ['react'],
contents: [
{ slug: 'reference/create-player' },
{ slug: 'reference/create-i18n' },
{ slug: 'reference/i18n-provider' },
{ slug: 'reference/use-translator' },
{ slug: 'reference/use-locale' },
{ slug: 'reference/use-player' },
{ slug: 'reference/use-media' },
{ slug: 'reference/use-store' },
@@ -129,6 +139,8 @@ export const sidebar: Sidebar = [
frameworks: ['html'],
contents: [
{ slug: 'reference/html-create-player', sidebarLabel: 'createPlayer' },
{ slug: 'reference/media-i18n-provider', sidebarLabel: 'media-i18n-provider' },
{ slug: 'reference/media-text', sidebarLabel: 'media-text' },
{ slug: 'reference/player-controller' },
{
sidebarLabel: 'Advanced',
@@ -149,6 +161,10 @@ export const sidebar: Sidebar = [
llmsDescription: 'API reference for feature modules that provide player capabilities and state.',
contents: [
{ slug: 'reference/create-selector' },
{ slug: 'reference/register-i18n', sidebarLabel: 'registerI18n' },
{ slug: 'reference/get-i18n-translations', sidebarLabel: 'getI18nTranslations' },
{ slug: 'reference/has-registered-i18n', sidebarLabel: 'hasRegisteredI18n' },
{ slug: 'reference/on-i18n-registry-change', sidebarLabel: 'onI18nRegistryChange' },
{ slug: 'reference/feature-buffer' },
{ slug: 'reference/feature-controls' },
{ slug: 'reference/feature-error' },