Skip to content

feat(react): add i18n provider and wire UI labels#1595

Open
sampotts wants to merge 14 commits into
feat/i18n-htmlfrom
feat/i18n-react
Open

feat(react): add i18n provider and wire UI labels#1595
sampotts wants to merge 14 commits into
feat/i18n-htmlfrom
feat/i18n-react

Conversation

@sampotts

@sampotts sampotts commented May 25, 2026

Copy link
Copy Markdown
Collaborator

Refs #222
Closes #1364

Summary

  • Adds React I18nProvider, hooks, and wires skin control labels through useTranslator
  • Composes i18n on Container with nested-provider passthrough for explicit locales
  • Resolves langRootRef under ambient ancestor providers

Test plan

  • pnpm -F @videojs/react test src/i18n
  • Package typecheck

Stacked on #1591.

Made with Cursor


Note

Medium Risk
Touches accessibility strings across many controls and async locale loading; behavior is well tested but regressions in aria-labels or locale edge cases are possible.

Overview
Adds a React i18n layer (createI18n, default I18nProvider, useTranslator, useLocale) that merges registry, lazy built-in locale packs, props, and optional browser fallbacks, with locale from props, DOM lang (via langRootRef / ambient subscription), and nested-provider passthrough.

Container wraps the player shell in I18nProvider with langRootRef so skins inherit locale without extra setup. Package exports add @videojs/react/i18n and per-locale build entries.

UI controls and dialogs now resolve labels through resolveControlLabel / resolveControlAttrs and core phrase helpers (ErrorDialog title/description/close, time remaining, input-indicator labels). Presets drop hard-coded error dialog copy in favor of those components. Tests cover provider behavior, play button aria-label, and translated error dialog.

Reviewed by Cursor Bugbot for commit f12eb04. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel

vercel Bot commented May 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
v10-sandbox Ready Ready Preview, Comment Jun 19, 2026 5:31am

Request Review

Comment thread packages/react/src/i18n/create-i18n.tsx Outdated
Comment thread packages/react/src/i18n/create-i18n.tsx
Comment thread packages/react/src/i18n/create-i18n.tsx

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4f43963. Configure here.

Comment thread packages/react/src/i18n/create-i18n.tsx Outdated
sampotts and others added 14 commits June 19, 2026 15:30
Add createI18n with ambient lang inheritance, browser translation fallback,
default loadLocale, and translated aria labels across React player UI.

Co-authored-by: Cursor <cursoragent@cursor.com>
Wrap the player Provider with I18nProvider so preset skins inherit locale
and lazy locale loading, and restore English ARIA expectations in tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
Let outer I18nProvider (sandbox locale) win over player chrome, and skip
nested default providers so locale props are not shadowed.

Co-authored-by: Cursor <cursoragent@cursor.com>
Pass langRootRef from Container for ambient lang resolution, fall back to
documentElement before mount, and inherit ancestor locale when a nested
provider only supplies translation overrides.

Co-authored-by: Cursor <cursoragent@cursor.com>
Only pass locale to I18nProviderRoot when a value is resolved so nested
providers do not assign undefined under exactOptionalPropertyTypes.

Co-authored-by: Cursor <cursoragent@cursor.com>
Re-check lazy-load sequence after getBrowserTranslations resolves so a
superseded locale cannot register browser translations globally.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use formatRemaining with the parametric timeRemainingPhrase template instead
of the removed remainingTimeSuffix key.

Co-authored-by: Cursor <cursoragent@cursor.com>
Container's langRootRef no longer forces a nested provider that shadows
an ancestor locale or translations prop.

Co-authored-by: Cursor <cursoragent@cursor.com>
Track whether a provider locale came from an explicit prop so Container's
langRootRef still resolves player shell lang when a test or app wrapper
supplies an ambient ancestor provider.

Co-authored-by: Cursor <cursoragent@cursor.com>
Adds regression tests for document.documentElement.lang updates on
I18nProvider with and without langRootRef.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use mergeLocaleOverlays loadedTags instead of merged overlay so
English-only lazy merges no longer block browser translation fallback.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown
Contributor

📦 Bundle Size Report

🎨 @videojs/html — no changes
Presets (7)
Entry Initial Lazy
/video (default) 48.58 kB 44.53 kB
/video (default + hls) 187.78 kB 44.53 kB
/video (minimal) 48.23 kB 44.53 kB
/video (minimal + hls) 187.39 kB 44.53 kB
/audio (default) 41.95 kB 44.53 kB
/audio (minimal) 40.70 kB 44.53 kB
/background 7.14 kB 44.53 kB
Media (10)
Entry Initial
/media/background-video 1.14 kB
/media/container 1.71 kB
/media/dash-video 242.62 kB
/media/hls-video 141.27 kB
/media/mux-audio 163.78 kB
/media/mux-video 163.64 kB
/media/native-hls-video 9.07 kB
/media/simple-hls-audio-only 16.99 kB
/media/simple-hls-video 18.78 kB
/media/vimeo-video 12.32 kB
Players (5)
Entry Initial Lazy
/video/player 10.94 kB 44.53 kB
/audio/player 8.31 kB 44.53 kB
/background/player 6.88 kB 44.53 kB
/live-video/player 10.52 kB 44.53 kB
/live-audio/player 8.31 kB 44.53 kB
Skins (30)
Entry Type Initial Lazy
/video/minimal-skin.css css 5.47 kB
/video/skin.css css 5.45 kB
/video/minimal-skin js 48.22 kB 44.53 kB
/video/minimal-skin.tailwind js 48.75 kB 44.53 kB
/video/skin js 48.55 kB 44.53 kB
/video/skin.tailwind js 49.20 kB 44.53 kB
/audio/minimal-skin.css css 3.61 kB
/audio/skin.css css 3.53 kB
/audio/minimal-skin js 40.70 kB 44.53 kB
/audio/minimal-skin.tailwind js 41.14 kB 44.53 kB
/audio/skin js 41.86 kB 44.53 kB
/audio/skin.tailwind js 42.33 kB 44.53 kB
/background/skin.css css 133 B
/background/skin js 1.14 kB
/live-video/minimal-skin.css css 5.47 kB
/live-video/skin.css css 5.45 kB
/live-video/minimal-skin js 47.37 kB 44.53 kB
/live-video/minimal-skin.tailwind js 47.78 kB 44.53 kB
/live-video/skin js 47.30 kB 44.53 kB
/live-video/skin.tailwind js 47.86 kB 44.53 kB
/live-audio/minimal-skin.css css 3.61 kB
/live-audio/skin.css css 3.53 kB
/live-audio/minimal-skin js 33.69 kB 44.53 kB
/live-audio/minimal-skin.tailwind js 33.19 kB 44.53 kB
/live-audio/skin js 35.06 kB 44.53 kB
/live-audio/skin.tailwind js 34.66 kB 44.53 kB
/global.css css 183 B
/shared.css css 88 B
/tailwind.css css 228 B
/skin-element js 5.22 kB 44.53 kB
UI Components (38)
Entry Initial
/ui/airplay-button 2.41 kB
/ui/alert-dialog 2.79 kB
/ui/alert-dialog-close 2.28 kB
/ui/alert-dialog-description 2.25 kB
/ui/alert-dialog-title 2.25 kB
/ui/buffering-indicator 2.54 kB
/ui/captions-button 2.49 kB
/ui/captions-radio-group 3.04 kB
/ui/cast-button 2.44 kB
/ui/compounds 3.03 kB
/ui/controls 2.81 kB
/ui/error-dialog 2.83 kB
/ui/fullscreen-button 2.41 kB
/ui/hotkey 2.40 kB
/ui/menu 2.85 kB
/ui/mute-button 2.42 kB
/ui/pip-button 2.45 kB
/ui/play-button 2.44 kB
/ui/playback-rate-button 2.57 kB
/ui/playback-rate-radio-group 2.99 kB
/ui/popover 2.94 kB
/ui/poster 2.34 kB
/ui/quality-radio-group 3.00 kB
/ui/seek-button 2.45 kB
/ui/seek-indicator 2.62 kB
/ui/seek-indicator-value 386 B
/ui/slider 2.83 kB
/ui/status-announcer 2.35 kB
/ui/status-indicator 2.49 kB
/ui/status-indicator-value 410 B
/ui/thumbnail 2.42 kB
/ui/time 2.80 kB
/ui/time-slider 2.86 kB
/ui/tooltip 2.81 kB
/ui/volume-indicator 2.48 kB
/ui/volume-indicator-fill 460 B
/ui/volume-indicator-value 474 B
/ui/volume-slider 2.83 kB

Sizes are marginal over the root entry point.

⚛️ @videojs/react

Path Base initial PR initial Diff % Lazy
/media/background-video 575 B 3.77 kB +3.20 kB +570.6% +44.53 kB 🔴
/media/dash-video 241.23 kB 244.04 kB +2.81 kB +1.2% +44.53 kB 🔺
/media/hls-video 139.89 kB 142.65 kB +2.76 kB +2.0% +44.53 kB 🔺
/media/mux-audio 162.20 kB 165.10 kB +2.90 kB +1.8% +44.53 kB 🔺
/media/mux-video 162.30 kB 165.20 kB +2.91 kB +1.8% +44.53 kB 🔺
/media/native-hls-video 7.41 kB 10.41 kB +3.00 kB +40.5% +44.53 kB 🔴
/media/simple-hls-audio-only 15.38 kB 18.43 kB +3.05 kB +19.8% +44.53 kB 🔴
/media/simple-hls-video 17.18 kB 20.24 kB +3.06 kB +17.8% +44.53 kB 🔴
/media/vimeo-video 10.58 kB 13.59 kB +3.01 kB +28.4% +44.53 kB 🔴
/video/minimal-skin 37.42 kB 41.05 kB +3.64 kB +9.7% +44.53 kB 🔺
/video/minimal-skin.tailwind 43.10 kB 46.72 kB +3.62 kB +8.4% +44.53 kB 🔺
/video/skin 37.39 kB 40.91 kB +3.52 kB +9.4% +44.53 kB 🔺
/video/skin.tailwind 43.09 kB 46.75 kB +3.66 kB +8.5% +44.53 kB 🔺
/audio/minimal-skin 30.52 kB 34.14 kB +3.61 kB +11.8% +44.53 kB 🔴
/audio/minimal-skin.tailwind 32.32 kB 35.87 kB +3.56 kB +11.0% +44.53 kB 🔴
/audio/skin 30.48 kB 34.09 kB +3.61 kB +11.9% +44.53 kB 🔴
/audio/skin.tailwind 34.33 kB 37.95 kB +3.62 kB +10.5% +44.53 kB 🔴
/live-video/minimal-skin 32.91 kB 36.52 kB +3.61 kB +11.0% +44.53 kB 🔴
/live-video/minimal-skin.tailwind 38.53 kB 42.08 kB +3.55 kB +9.2% +44.53 kB 🔺
/live-video/skin 32.94 kB 36.50 kB +3.56 kB +10.8% +44.53 kB 🔴
/live-video/skin.tailwind 38.58 kB 42.19 kB +3.60 kB +9.3% +44.53 kB 🔺
/live-audio/minimal-skin 21.98 kB 25.54 kB +3.55 kB +16.2% +44.53 kB 🔴
/live-audio/minimal-skin.tailwind 24.84 kB 28.41 kB +3.57 kB +14.4% +44.53 kB 🔴
/live-audio/skin 22.02 kB 25.61 kB +3.59 kB +16.3% +44.53 kB 🔴
/live-audio/skin.tailwind 25.01 kB 28.54 kB +3.53 kB +14.1% +44.53 kB 🔴
/ui/airplay-button 8.77 kB 11.91 kB +3.14 kB +35.8% +44.53 kB 🔴
/ui/buffering-indicator 6.77 kB 9.78 kB +3.02 kB +44.6% +44.53 kB 🔴
/ui/captions-button 8.78 kB 11.89 kB +3.11 kB +35.5% +44.53 kB 🔴
/ui/captions-radio-group 6.46 kB 9.52 kB +3.06 kB +47.4% +44.53 kB 🔴
/ui/cast-button 8.78 kB 11.93 kB +3.16 kB +36.0% +44.53 kB 🔴
/ui/controls 6.50 kB 9.57 kB +3.07 kB +47.2% +44.53 kB 🔴
/ui/error-dialog 8.28 kB 11.68 kB +3.39 kB +41.0% +44.53 kB 🔴
/ui/fullscreen-button 8.75 kB 11.88 kB +3.14 kB +35.9% +44.53 kB 🔴
/ui/gesture 6.96 kB 10.01 kB +3.05 kB +43.8% +44.53 kB 🔴
/ui/hotkey 7.45 kB 10.47 kB +3.03 kB +40.7% +44.53 kB 🔴
/ui/live-button 7.30 kB 10.45 kB +3.15 kB +43.2% +44.53 kB 🔴
/ui/menu 17.48 kB 20.47 kB +3.00 kB +17.1% +44.53 kB 🔴
/ui/mute-button 8.76 kB 11.89 kB +3.13 kB +35.7% +44.53 kB 🔴
/ui/pip-button 8.76 kB 11.88 kB +3.12 kB +35.6% +44.53 kB 🔴
/ui/play-button 8.75 kB 11.89 kB +3.15 kB +36.0% +44.53 kB 🔴
/ui/playback-rate 6.44 kB 9.47 kB +3.04 kB +47.2% +44.53 kB 🔴
/ui/playback-rate-button 8.85 kB 11.99 kB +3.13 kB +35.4% +44.53 kB 🔴
/ui/popover 6.35 kB 9.36 kB +3.01 kB +47.4% +44.53 kB 🔴
/ui/poster 6.40 kB 9.45 kB +3.05 kB +47.6% +44.53 kB 🔴
/ui/quality 6.85 kB 9.90 kB +3.04 kB +44.4% +44.53 kB 🔴
/ui/seek-button 8.89 kB 12.01 kB +3.12 kB +35.1% +44.53 kB 🔴
/ui/seek-indicator 10.22 kB 13.29 kB +3.07 kB +30.0% +44.53 kB 🔴
/ui/slider 10.83 kB 13.83 kB +3.00 kB +27.7% +44.53 kB 🔴
/ui/status-announcer 9.39 kB 12.49 kB +3.10 kB +33.0% +44.53 kB 🔴
/ui/status-indicator 10.30 kB 13.35 kB +3.06 kB +29.7% +44.53 kB 🔴
/ui/thumbnail 7.61 kB 10.62 kB +3.01 kB +39.6% +44.53 kB 🔴
/ui/time 7.49 kB 10.66 kB +3.17 kB +42.4% +44.53 kB 🔴
/ui/time-slider 10.43 kB 13.56 kB +3.13 kB +30.0% +44.53 kB 🔴
/ui/tooltip 7.01 kB 10.01 kB +3.00 kB +42.8% +44.53 kB 🔴
/ui/volume-indicator 10.37 kB 13.49 kB +3.12 kB +30.1% +44.53 kB 🔴
/ui/volume-slider 10.11 kB 13.24 kB +3.13 kB +31.0% +44.53 kB 🔴
/video (default) 37.38 kB 41.11 kB +3.74 kB +10.0% +44.53 kB 🔺
/video (default + hls) 175.59 kB 178.87 kB +3.28 kB +1.9% +44.53 kB 🔺
/video (minimal) 37.50 kB 41.17 kB +3.67 kB +9.8% +44.53 kB 🔺
/video (minimal + hls) 175.83 kB 179.06 kB +3.22 kB +1.8% +44.53 kB 🔺
/audio (default) 30.57 kB 34.17 kB +3.60 kB +11.8% +44.53 kB 🔴
/audio (minimal) 30.62 kB 34.22 kB +3.60 kB +11.8% +44.53 kB 🔴
/background 754 B 3.92 kB +3.18 kB +431.7% +44.53 kB 🔴
Presets (7)
Entry Initial Lazy
/video (default) 41.11 kB 44.53 kB
/video (default + hls) 178.87 kB 44.53 kB
/video (minimal) 41.17 kB 44.53 kB
/video (minimal + hls) 179.06 kB 44.53 kB
/audio (default) 34.17 kB 44.53 kB
/audio (minimal) 34.22 kB 44.53 kB
/background 3.92 kB 44.53 kB
Media (9)
Entry Initial Lazy
/media/background-video 3.77 kB 44.53 kB
/media/dash-video 244.04 kB 44.53 kB
/media/hls-video 142.65 kB 44.53 kB
/media/mux-audio 165.10 kB 44.53 kB
/media/mux-video 165.20 kB 44.53 kB
/media/native-hls-video 10.41 kB 44.53 kB
/media/simple-hls-audio-only 18.43 kB 44.53 kB
/media/simple-hls-video 20.24 kB 44.53 kB
/media/vimeo-video 13.59 kB 44.53 kB
Skins (27)
Entry Type Initial Lazy
/tailwind.css css 228 B
/video/minimal-skin.css css 5.37 kB
/video/skin.css css 5.34 kB
/video/minimal-skin js 41.05 kB 44.53 kB
/video/minimal-skin.tailwind js 46.72 kB 44.53 kB
/video/skin js 40.91 kB 44.53 kB
/video/skin.tailwind js 46.75 kB 44.53 kB
/audio/minimal-skin.css css 3.47 kB
/audio/skin.css css 3.39 kB
/audio/minimal-skin js 34.14 kB 44.53 kB
/audio/minimal-skin.tailwind js 35.87 kB 44.53 kB
/audio/skin js 34.09 kB 44.53 kB
/audio/skin.tailwind js 37.95 kB 44.53 kB
/background/skin.css css 90 B
/background/skin js 272 B
/live-video/minimal-skin.css css 5.37 kB
/live-video/skin.css css 5.34 kB
/live-video/minimal-skin js 36.52 kB 44.53 kB
/live-video/minimal-skin.tailwind js 42.08 kB 44.53 kB
/live-video/skin js 36.50 kB 44.53 kB
/live-video/skin.tailwind js 42.19 kB 44.53 kB
/live-audio/minimal-skin.css css 3.47 kB
/live-audio/skin.css css 3.39 kB
/live-audio/minimal-skin js 25.54 kB 44.53 kB
/live-audio/minimal-skin.tailwind js 28.41 kB 44.53 kB
/live-audio/skin js 25.61 kB 44.53 kB
/live-audio/skin.tailwind js 28.54 kB 44.53 kB
UI Components (32)
Entry Initial
/ui/airplay-button 2.26 kB
/ui/alert-dialog 2.47 kB
/ui/buffering-indicator 2.11 kB
/ui/captions-button 2.30 kB
/ui/captions-radio-group 2.08 kB
/ui/cast-button 2.27 kB
/ui/controls 2.06 kB
/ui/error-dialog 2.30 kB
/ui/fullscreen-button 2.26 kB
/ui/gesture 2.63 kB
/ui/hotkey 2.54 kB
/ui/live-button 2.18 kB
/ui/menu 2.41 kB
/ui/mute-button 2.34 kB
/ui/pip-button 2.33 kB
/ui/play-button 2.32 kB
/ui/playback-rate 2.10 kB
/ui/playback-rate-button 2.28 kB
/ui/popover 2.56 kB
/ui/poster 2.15 kB
/ui/quality 2.10 kB
/ui/seek-button 2.29 kB
/ui/seek-indicator 2.18 kB
/ui/slider 2.18 kB
/ui/status-announcer 2.14 kB
/ui/status-indicator 2.19 kB
/ui/thumbnail 2.10 kB
/ui/time 2.09 kB
/ui/time-slider 2.27 kB
/ui/tooltip 2.66 kB
/ui/volume-indicator 2.17 kB
/ui/volume-slider 2.30 kB

Sizes are marginal over the root entry point.

🧩 @videojs/core — no changes
Entries (68)
Entry Initial Lazy
. 10.50 kB
/dom 17.01 kB
/dom/media/custom-media-element 2.09 kB
/dom/media/dash 236.88 kB
/dom/media/google-cast 4.03 kB
/dom/media/hls 135.73 kB
/dom/media/media-host 1.25 kB
/dom/media/media-played-ranges 576 B
/dom/media/mux 151.26 kB
/dom/media/native-hls 3.07 kB
/dom/media/simple-hls 16.54 kB
/dom/media/simple-hls-audio-only 14.75 kB
/dom/media/vimeo 9.86 kB
/media/predicate 573 B
/i18n 2.55 kB 44.53 kB
/i18n/locales/all 26.94 kB
/i18n/locales/ar 1017 B
/i18n/locales/az 914 B
/i18n/locales/bg 1.02 kB
/i18n/locales/bn 1.04 kB
/i18n/locales/bs 837 B
/i18n/locales/ca 878 B
/i18n/locales/cs 865 B
/i18n/locales/cy 829 B
/i18n/locales/da 821 B
/i18n/locales/de 916 B
/i18n/locales/el 1.20 kB
/i18n/locales/en 645 B
/i18n/locales/es 829 B
/i18n/locales/et 882 B
/i18n/locales/eu 828 B
/i18n/locales/fa 1008 B
/i18n/locales/fi 856 B
/i18n/locales/fr 896 B
/i18n/locales/gd 917 B
/i18n/locales/gl 816 B
/i18n/locales/he 940 B
/i18n/locales/hi 1.06 kB
/i18n/locales/hr 846 B
/i18n/locales/hu 913 B
/i18n/locales/it 856 B
/i18n/locales/ja 997 B
/i18n/locales/ko 960 B
/i18n/locales/lv 886 B
/i18n/locales/mr 1.07 kB
/i18n/locales/nb 814 B
/i18n/locales/ne 1.06 kB
/i18n/locales/nl 834 B
/i18n/locales/nn 801 B
/i18n/locales/oc 904 B
/i18n/locales/pl 951 B
/i18n/locales/pt 836 B
/i18n/locales/pt-BR 836 B
/i18n/locales/pt-PT 807 B
/i18n/locales/ro 882 B
/i18n/locales/ru 1.10 kB
/i18n/locales/sk 929 B
/i18n/locales/sl 861 B
/i18n/locales/sr 827 B
/i18n/locales/sv 825 B
/i18n/locales/te 1.10 kB
/i18n/locales/th 1.08 kB
/i18n/locales/tr 906 B
/i18n/locales/uk 1.13 kB
/i18n/locales/vi 903 B
/i18n/locales/zh 810 B
/i18n/locales/zh-CN 810 B
/i18n/locales/zh-TW 821 B
🏷️ @videojs/element — no changes
Entries (2)
Entry Initial
. 996 B
/context 943 B
📦 @videojs/store — no changes
Entries (3)
Entry Initial
. 1.39 kB
/html 696 B
/react 360 B
🔧 @videojs/utils — no changes
Entries (10)
Entry Initial
/array 104 B
/dom 2.67 kB
/events 319 B
/function 327 B
/object 275 B
/predicate 265 B
/string 231 B
/style 190 B
/time 930 B
/number 158 B
📦 @videojs/spf — no changes
Entries (4)
Entry Initial
. 4.45 kB
/dom 6.33 kB
/hls 15.44 kB
/background-looping-video 12.97 kB

ℹ️ How to interpret

JS sizes are initial static graph totals (minified + brotli). Lazy dynamic chunks are shown separately when present.

Icon Meaning
No change
🔺 Increased ≤ 10%
🔴 Increased > 10%
🔽 Decreased
🆕 New (no baseline)

Run pnpm size locally to check current initial sizes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant