diff --git a/site/src/content/docs/concepts/i18n.mdx b/site/src/content/docs/concepts/i18n.mdx
new file mode 100644
index 00000000..e0b5393d
--- /dev/null
+++ b/site/src/content/docs/concepts/i18n.mdx
@@ -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"
+
+
+
+
+
+
+
+```
+
+```tsx title="react"
+
+
+
+
+
+
+
+```
+
+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 |
+| --- | --- |
+| `registerI18n` | Add or merge a locale layer |
+| `getI18nTranslations` | Read the merged map for a locale |
+| `hasRegisteredI18n` | Check whether a tag is in the registry |
+| `onI18nRegistryChange` | 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:
+
+
+
+- ``, ``, and skins include an i18n provider mixin.
+- Set `` (or `lang` on an ancestor) and ship/register Spanish strings.
+
+Use `` when you render **standalone** controls outside a player, or when one page hosts players in different languages.
+
+
+
+
+
+- Preset skins render inside `Container`, which mounts `I18nProvider` with `langRootRef` on the player shell.
+- Set `` and register or lazy-load Spanish.
+
+Wrap with an explicit `I18nProvider` when you need a forced locale, per-render overrides, or custom controls outside `Container`.
+
+
+
+**You do need to register or lazy-load packs** before labels appear in the target language. `` 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 `` or `locale` on `I18nProvider`
+2. **Ambient** — nearest ancestor `[lang]` (HTML) or `langRootRef` / `` (React)
+3. **Fallback** — English defaults
+
+Changing `` re-renders wired controls without remounting the player. An explicit `locale` / `lang` on a provider overrides ambient `` 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
+
+
+```
+
+## 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' });
+```
+
+
+
+## See also
+
+- Register a custom locale
+- Override individual keys
+- Switch locale dynamically
+- SSR and hydration
+- Add a built-in locale (contributors)
+- Accessibility — translated ARIA labels
diff --git a/site/src/content/docs/how-to/i18n-add-built-in-locale.mdx b/site/src/content/docs/how-to/i18n-add-built-in-locale.mdx
new file mode 100644
index 00000000..3cb383ef
--- /dev/null
+++ b/site/src/content/docs/how-to/i18n-add-built-in-locale.mdx
@@ -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 Register a custom locale instead.
+
+### Prerequisites
+
+- Familiarity with Internationalization 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;
+```
+
+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`).
+
+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
+```
+
+
+
+## What's next?
+
+- Open a PR with the locale file and `built-in-locales.ts` change only (generated outputs come from CI/build)
+- Internationalization concept
diff --git a/site/src/content/docs/how-to/i18n-override-translations.mdx b/site/src/content/docs/how-to/i18n-override-translations.mdx
new file mode 100644
index 00000000..977873b9
--- /dev/null
+++ b/site/src/content/docs/how-to/i18n-override-translations.mdx
@@ -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 Internationalization 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 `I18nProvider` to scope overrides to one subtree. This layer sits **above** registry and lazy packs:
+
+
+
+```tsx
+import { I18nProvider } from '@videojs/react/i18n';
+import { Provider, VideoSkin, Video } from '@videojs/react/video';
+
+
+
+
+
+
+
+
+```
+
+Nested providers inherit the parent locale when you only pass `translations`:
+
+```tsx
+
+
+ {/* locale stays `de`; only `play` differs */}
+
+
+```
+
+
+
+## HTML reflected text
+
+Use `` for static copy outside buttons:
+
+```html
+
+
+
+```
+
+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?
+
+- Switch locale dynamically
+- `getI18nTranslations` — inspect the merged map
diff --git a/site/src/content/docs/how-to/i18n-register-locale.mdx b/site/src/content/docs/how-to/i18n-register-locale.mdx
new file mode 100644
index 00000000..9e83c1d6
--- /dev/null
+++ b/site/src/content/docs/how-to/i18n-register-locale.mdx
@@ -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 `registerI18n`, then set `lang` on the page or pass `locale` to a provider. Read Internationalization 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:
+
+
+
+```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"
+
+
+
+
+
+
+
+```
+
+If you skip `registerI18n`, the built-in provider still **lazy-loads** `es` when `lang="es"` — explicit registration avoids the async gap on first paint.
+
+
+
+
+
+```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 (
+
+
+
+
+
+ );
+}
+```
+
+Preset skins use `Container`, which includes `I18nProvider`. Set `` or wrap with ``.
+
+
+
+## Register your own pack
+
+```ts title="my-es.ts"
+import type { Translations } from '@videojs/core/i18n';
+
+const es: Partial = {
+ 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
+
+
+
+
+
+
+
+
+
+
+```
+
+
+
+## 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?
+
+- Override individual keys
+- Switch locale at runtime
+- `registerI18n` reference
diff --git a/site/src/content/docs/how-to/i18n-ssr.mdx b/site/src/content/docs/how-to/i18n-ssr.mdx
new file mode 100644
index 00000000..6e6895c6
--- /dev/null
+++ b/site/src/content/docs/how-to/i18n-ssr.mdx
@@ -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 Internationalization for provider resolution and merge order.
+
+## Set ``
+
+Render the document language on the server:
+
+```html
+
+```
+
+React providers use a server snapshot of `undefined` for ambient lang, then read `` 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 `I18nProvider`:
+
+
+
+```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 (
+
+
+
+
+
+
+
+ );
+}
+```
+
+`translations` on the first render skips the async lazy layer — labels match on server and client.
+
+
+
+## Register on the server
+
+For custom packs, call `registerI18n` 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}`);
+
+
+ ...
+
+```
+
+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 `` or on ``. 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);
+```
+
+
+
+## What's next?
+
+- Switch locale at runtime
+- Register a custom locale
diff --git a/site/src/content/docs/how-to/i18n-switch-locale.mdx b/site/src/content/docs/how-to/i18n-switch-locale.mdx
new file mode 100644
index 00000000..0f9356b5
--- /dev/null
+++ b/site/src/content/docs/how-to/i18n-switch-locale.mdx
@@ -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
+
+- Locale packs registered or lazy-loadable
+
+## Ambient ``
+
+The simplest switch — update the document language and let providers pick it up:
+
+```ts
+document.documentElement.lang = 'fr';
+```
+
+
+
+
+
+Player elements and skins observe `lang` on `` and ancestors. No remount required.
+
+
+
+
+
+`Container`'s `I18nProvider` reads `langRootRef` from the player shell. Updating `` is enough when you are not forcing `locale` on an outer provider.
+
+
+
+## Explicit provider locale
+
+
+
+```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 (
+ <>
+
+
+
+
+
+
+
+
+ >
+ );
+}
+```
+
+Changing `locale` triggers lazy `loadLocale` for built-in packs.
+
+
+
+
+
+```html
+
+ ...
+
+
+
+```
+
+Or set `document.documentElement.lang` if the player inherits ambient language.
+
+
+
+## Avoid flash while switching
+
+Async lazy loads can briefly show English. Pre-import the pack and pass `translations`:
+
+
+
+```tsx
+async function switchTo(next: string) {
+ const { default: translations } = await import(`@videojs/react/i18n/locales/${next}`);
+ setState({ locale: next, translations });
+}
+
+
+ ...
+
+```
+
+
+
+
+
+## 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?
+
+- SSR with locale
+- `useLocale` — read the active locale in React
diff --git a/site/src/content/docs/reference/create-i18n.mdx b/site/src/content/docs/reference/create-i18n.mdx
new file mode 100644
index 00000000..9a18ea27
--- /dev/null
+++ b/site/src/content/docs/reference/create-i18n.mdx
@@ -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";
+
+
+
+`createI18n` mirrors `createPlayer`: 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 (
+
+
+
+ );
+}
+```
+
+
+
+
diff --git a/site/src/content/docs/reference/get-i18n-translations.mdx b/site/src/content/docs/reference/get-i18n-translations.mdx
new file mode 100644
index 00000000..e7415086
--- /dev/null
+++ b/site/src/content/docs/reference/get-i18n-translations.mdx
@@ -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 Internationalization 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
+```
+
+
diff --git a/site/src/content/docs/reference/has-registered-i18n.mdx b/site/src/content/docs/reference/has-registered-i18n.mdx
new file mode 100644
index 00000000..5bae5e58
--- /dev/null
+++ b/site/src/content/docs/reference/has-registered-i18n.mdx
@@ -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
+```
+
+
diff --git a/site/src/content/docs/reference/i18n-provider.mdx b/site/src/content/docs/reference/i18n-provider.mdx
new file mode 100644
index 00000000..a6b84242
--- /dev/null
+++ b/site/src/content/docs/reference/i18n-provider.mdx
@@ -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";
+
+
+
+`I18nProvider` resolves the active locale, merges registry and lazy pack layers, and exposes a translator through context. Preset skins mount it inside `Container` with `langRootRef` on the player shell.
+
+Wrap custom controls or force a locale explicitly:
+
+```tsx
+import { I18nProvider } from '@videojs/react/i18n';
+
+
+
+
+```
+
+Omit `locale` to inherit the nearest `lang` attribute (via `langRootRef` or ``). See Internationalization for merge priority and SSR guidance.
+
+
+
+
diff --git a/site/src/content/docs/reference/media-i18n-provider.mdx b/site/src/content/docs/reference/media-i18n-provider.mdx
new file mode 100644
index 00000000..a8699c08
--- /dev/null
+++ b/site/src/content/docs/reference/media-i18n-provider.mdx
@@ -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";
+
+
+
+`` applies the i18n provider mixin to a standalone subtree. Built-in `` 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 `registerI18n`, then set `lang` on the provider or an ancestor. See Internationalization for opaque keys and fallback behavior.
+
+```html
+
+
+
+
+
+```
+
+## Attributes
+
+| Attribute | Description |
+| --- | --- |
+| `lang` | Forces the active BCP 47 locale for this subtree. Omit to inherit the nearest ancestor `[lang]`. |
+
+## Related
+
+- `` — renders a translated string by key
+- Register a custom locale
+
+
diff --git a/site/src/content/docs/reference/media-text.mdx b/site/src/content/docs/reference/media-text.mdx
new file mode 100644
index 00000000..4aa357b9
--- /dev/null
+++ b/site/src/content/docs/reference/media-text.mdx
@@ -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";
+
+
+
+`` renders the translated value for a registry key inside a `` (or any ancestor with the i18n provider mixin). Use it for standalone labels, tooltips, or demos — skin controls resolve keys internally.
+
+```html
+
+
+
+```
+
+## Attributes
+
+| Attribute | Description |
+| --- | --- |
+| `key` | Opaque translation key (`play`, `pause`, `timeRemainingPhrase`, …). |
+
+Parametric keys are not yet supported on `` — use `createTranslator` in script for interpolated strings.
+
+
diff --git a/site/src/content/docs/reference/on-i18n-registry-change.mdx b/site/src/content/docs/reference/on-i18n-registry-change.mdx
new file mode 100644
index 00000000..a339d7d2
--- /dev/null
+++ b/site/src/content/docs/reference/on-i18n-registry-change.mdx
@@ -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.
+
+
diff --git a/site/src/content/docs/reference/register-i18n.mdx b/site/src/content/docs/reference/register-i18n.mdx
new file mode 100644
index 00000000..c8edf5ca
--- /dev/null
+++ b/site/src/content/docs/reference/register-i18n.mdx
@@ -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 Internationalization.
+
+```ts
+import { registerI18n } from '@videojs/react/i18n';
+
+registerI18n('es', {
+ play: 'Reproducir',
+ pause: 'Pausar',
+});
+```
+
+
diff --git a/site/src/content/docs/reference/use-locale.mdx b/site/src/content/docs/reference/use-locale.mdx
new file mode 100644
index 00000000..0ff72d05
--- /dev/null
+++ b/site/src/content/docs/reference/use-locale.mdx
@@ -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";
+
+
+
+`useLocale` returns the resolved BCP 47 tag from the nearest `I18nProvider`, 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 {locale};
+}
+```
+
+
+
+
diff --git a/site/src/content/docs/reference/use-translator.mdx b/site/src/content/docs/reference/use-translator.mdx
new file mode 100644
index 00000000..6fa9e402
--- /dev/null
+++ b/site/src/content/docs/reference/use-translator.mdx
@@ -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";
+
+
+
+`useTranslator` returns the translator from the nearest `I18nProvider`. 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 {t('play')};
+}
+```
+
+
+
+
diff --git a/site/src/docs.config.ts b/site/src/docs.config.ts
index 6793fb41..ef8f4e43 100644
--- a/site/src/docs.config.ts
+++ b/site/src/docs.config.ts
@@ -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' },