Skip to content

feat(packages): i18n foundation#1708

Open
sampotts wants to merge 16 commits into
mainfrom
feat/i18n
Open

feat(packages): i18n foundation#1708
sampotts wants to merge 16 commits into
mainfrom
feat/i18n

Conversation

@sampotts

@sampotts sampotts commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Refs #1589
Refs #1590

Summary

Restores the combined i18n foundation and locale-pack work from the two accidentally merged PRs after #1707 reverted them out of main.

feat/i18n will be the feature branch the other stacked PRs merge into and this PR will be used to merge the whole feature into main:

main
#1708 feat/i18n
#1591 feat/i18n-html
#1595 feat/i18n-react
#1593 feat/i18n-sandbox
#1600 docs/i18n


Note

Medium Risk
Large, user-facing change to labels and accessibility across controls and many locales; risk is mitigated by tests, codegen checks, and restoring previously reviewed design, but regressions in string keys or locale loading would affect all skins.

Overview
Restores the Video.js 10 i18n stack that was reverted from main: a new @videojs/core/i18n entry with a global registry, typed createTranslator, loadLocale lazy imports, 50+ shipped locale packs (v8 parity), and optional Chrome Translator API fallback (getBrowserTranslations / shouldAttemptBrowserTranslation).

Codegen & CI: generate-i18n-locales.ts runs on @videojs/core prebuild, regenerating locales/all.ts, load-locale.ts, and html/react locale re-export stubs; check-workspace gains an i18n locales consistency check. package.json adds ./i18n and ./i18n/locales/* exports and marks the registry as a side effect.

Player copy: English defaults live in locales/en.ts with an expanded key set (live/cast, sliders, indicators, error dialog, timeRemainingPhrase, etc.). UI cores (e.g. captions/cast and others in the diff) now expose opaque label keys via resolveOptionalControlLabel instead of hardcoded English; related tests and internal/design/i18n.md are updated to implemented status and document provider merge order, tooltips, and loadLocale.

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

@netlify

netlify Bot commented Jun 18, 2026

Copy link
Copy Markdown

Deploy Preview for vjs10-site ready!

Name Link
🔨 Latest commit 127e44f
🔍 Latest deploy log https://app.netlify.com/projects/vjs10-site/deploys/6a473767e5ff5d0008d82d61
😎 Deploy Preview https://deploy-preview-1708--vjs10-site.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@vercel

vercel Bot commented Jun 18, 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 Jul 3, 2026 4:15am

Request Review

@sampotts sampotts marked this pull request as draft June 18, 2026 23:01
@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle Size Report

🎨 @videojs/html

Path Base initial PR initial Diff % Lazy
/video/minimal-skin 46.21 kB 47.59 kB +1.38 kB +3.0% +51.99 kB 🔺
/video/minimal-skin.tailwind 46.93 kB 48.30 kB +1.37 kB +2.9% +51.99 kB 🔺
/video/skin 46.24 kB 47.57 kB +1.33 kB +2.9% +51.99 kB 🔺
/video/skin.tailwind 47.02 kB 48.42 kB +1.40 kB +3.0% +51.99 kB 🔺
/audio/minimal-skin 38.94 kB 40.33 kB +1.38 kB +3.6% +51.99 kB 🔺
/audio/minimal-skin.tailwind 39.43 kB 40.84 kB +1.41 kB +3.6% +51.99 kB 🔺
/audio/skin 39.79 kB 41.16 kB +1.37 kB +3.4% +51.99 kB 🔺
/audio/skin.tailwind 40.30 kB 41.72 kB +1.41 kB +3.5% +51.99 kB 🔺
/live-video/minimal-skin 45.25 kB 46.54 kB +1.29 kB +2.9% +51.99 kB 🔺
/live-video/minimal-skin.tailwind 45.87 kB 47.14 kB +1.27 kB +2.8% +51.99 kB 🔺
/live-video/skin 44.86 kB 46.25 kB +1.39 kB +3.1% +51.99 kB 🔺
/live-video/skin.tailwind 45.52 kB 46.87 kB +1.35 kB +3.0% +51.99 kB 🔺
/live-audio/minimal-skin 31.54 kB 32.74 kB +1.20 kB +3.8% +51.99 kB 🔺
/live-audio/minimal-skin.tailwind 30.98 kB 32.16 kB +1.18 kB +3.8% +51.99 kB 🔺
/live-audio/skin 32.54 kB 33.68 kB +1.14 kB +3.5% +51.99 kB 🔺
/live-audio/skin.tailwind 32.01 kB 33.19 kB +1.18 kB +3.7% +51.99 kB 🔺
/ui/compounds 29.13 kB 30.54 kB +1.41 kB +4.8% +51.99 kB 🔺
/ui/controls 7.72 kB 7.72 kB +2 B +0.0% +51.99 kB 🔴
/ui/error-dialog 9.36 kB 9.35 kB -1 B -0.0% +51.99 kB 🔴
/ui/menu 17.93 kB 18.16 kB +236 B +1.3% +51.99 kB 🔴
/ui/slider 5.13 kB 5.14 kB +4 B +0.1% +51.99 kB 🔴
/ui/time 8.66 kB 9.67 kB +1.01 kB +11.7% +51.99 kB 🔴
/ui/time-slider 12.67 kB 12.85 kB +181 B +1.4% +51.99 kB 🔴
/ui/volume-slider 10.79 kB 11.00 kB +218 B +2.0% +51.99 kB 🔴
/video (default) 46.24 kB 47.64 kB +1.39 kB +3.0% +51.99 kB 🔺
/video (default + hls) 185.87 kB 187.04 kB +1.17 kB +0.6% +51.99 kB 🔺
/video (minimal) 46.25 kB 47.65 kB +1.41 kB +3.0% +51.99 kB 🔺
/video (minimal + hls) 185.90 kB 187.15 kB +1.26 kB +0.7% +51.99 kB 🔺
/audio (default) 39.78 kB 41.17 kB +1.39 kB +3.5% +51.99 kB 🔺
/audio (minimal) 38.93 kB 40.36 kB +1.43 kB +3.7% +51.99 kB 🔺
Presets (7)
Entry Initial Lazy
/video (default) 47.64 kB 51.99 kB
/video (default + hls) 187.04 kB 51.99 kB
/video (minimal) 47.65 kB 51.99 kB
/video (minimal + hls) 187.15 kB 51.99 kB
/audio (default) 41.17 kB 51.99 kB
/audio (minimal) 40.36 kB 51.99 kB
/background 4.20 kB
Media (10)
Entry Initial
/media/background-video 1.14 kB
/media/container 1.72 kB
/media/dash-video 214.70 kB
/media/hlsjs-video 141.51 kB
/media/mux-audio 164.02 kB
/media/mux-video 163.99 kB
/media/native-hls-video 9.09 kB
/media/simple-hls-audio-only 17.15 kB
/media/simple-hls-video 18.70 kB
/media/vimeo-video 12.32 kB
Players (5)
Entry Initial
/video/player 8.22 kB
/audio/player 5.38 kB
/background/player 3.93 kB
/live-video/player 7.64 kB
/live-audio/player 5.39 kB
Skins (30)
Entry Type Initial Lazy
/video/minimal-skin.css css 5.65 kB
/video/skin.css css 5.61 kB
/video/minimal-skin js 47.59 kB 51.99 kB
/video/minimal-skin.tailwind js 48.30 kB 51.99 kB
/video/skin js 47.57 kB 51.99 kB
/video/skin.tailwind js 48.42 kB 51.99 kB
/audio/minimal-skin.css css 3.93 kB
/audio/skin.css css 3.82 kB
/audio/minimal-skin js 40.33 kB 51.99 kB
/audio/minimal-skin.tailwind js 40.84 kB 51.99 kB
/audio/skin js 41.16 kB 51.99 kB
/audio/skin.tailwind js 41.72 kB 51.99 kB
/background/skin.css css 133 B
/background/skin js 1.14 kB
/live-video/minimal-skin.css css 5.65 kB
/live-video/skin.css css 5.61 kB
/live-video/minimal-skin js 46.54 kB 51.99 kB
/live-video/minimal-skin.tailwind js 47.14 kB 51.99 kB
/live-video/skin js 46.25 kB 51.99 kB
/live-video/skin.tailwind js 46.87 kB 51.99 kB
/live-audio/minimal-skin.css css 3.93 kB
/live-audio/skin.css css 3.82 kB
/live-audio/minimal-skin js 32.74 kB 51.99 kB
/live-audio/minimal-skin.tailwind js 32.16 kB 51.99 kB
/live-audio/skin js 33.68 kB 51.99 kB
/live-audio/skin.tailwind js 33.19 kB 51.99 kB
/global.css css 176 B
/shared.css css 104 B
/tailwind.css css 161 B
/skin-element js 1.45 kB
UI Components (39)
Entry Initial
/ui/airplay-button 2.47 kB
/ui/alert-dialog 2.64 kB
/ui/alert-dialog-close 2.26 kB
/ui/alert-dialog-description 2.20 kB
/ui/alert-dialog-title 2.23 kB
/ui/audio-track-radio-group 2.94 kB
/ui/buffering-indicator 2.38 kB
/ui/captions-button 2.55 kB
/ui/captions-radio-group 3.00 kB
/ui/cast-button 2.46 kB
/ui/compounds 2.89 kB
/ui/controls 2.64 kB
/ui/error-dialog 2.66 kB
/ui/fullscreen-button 2.43 kB
/ui/hotkey 2.34 kB
/ui/menu 2.68 kB
/ui/mute-button 2.46 kB
/ui/pip-button 2.49 kB
/ui/play-button 2.48 kB
/ui/playback-rate-button 2.52 kB
/ui/playback-rate-radio-group 2.87 kB
/ui/popover 2.86 kB
/ui/poster 2.34 kB
/ui/quality-radio-group 2.93 kB
/ui/seek-button 2.45 kB
/ui/seek-indicator 2.51 kB
/ui/seek-indicator-value 432 B
/ui/slider 2.68 kB
/ui/status-announcer 2.43 kB
/ui/status-indicator 2.54 kB
/ui/status-indicator-value 407 B
/ui/thumbnail 2.32 kB
/ui/time 2.65 kB
/ui/time-slider 2.69 kB
/ui/tooltip 2.84 kB
/ui/volume-indicator 2.50 kB
/ui/volume-indicator-fill 369 B
/ui/volume-indicator-value 390 B
/ui/volume-slider 2.68 kB

Sizes are marginal over the root entry point.

⚛️ @videojs/react

Path Base initial PR initial Diff % Lazy
/video/minimal-skin 38.65 kB 39.99 kB +1.34 kB +3.5% +51.99 kB 🔺
/video/minimal-skin.tailwind 44.67 kB 46.05 kB +1.38 kB +3.1% +51.99 kB 🔺
/video/skin 38.25 kB 39.63 kB +1.38 kB +3.6% +51.99 kB 🔺
/video/skin.tailwind 44.31 kB 45.71 kB +1.40 kB +3.1% +51.99 kB 🔺
/audio/minimal-skin 31.56 kB 32.91 kB +1.35 kB +4.3% +51.99 kB 🔺
/audio/minimal-skin.tailwind 33.75 kB 35.15 kB +1.40 kB +4.2% +51.99 kB 🔺
/audio/skin 31.48 kB 32.88 kB +1.40 kB +4.4% +51.99 kB 🔺
/audio/skin.tailwind 35.73 kB 37.03 kB +1.30 kB +3.6% +51.99 kB 🔺
/live-video/minimal-skin 33.42 kB 33.86 kB +447 B +1.3% 🔺
/live-video/minimal-skin.tailwind 39.37 kB 39.76 kB +400 B +1.0% 🔺
/live-video/skin 33.43 kB 33.86 kB +444 B +1.3% 🔺
/live-video/skin.tailwind 39.43 kB 39.86 kB +440 B +1.1% 🔺
/ui/time 7.81 kB 8.82 kB +1.01 kB +12.9% +51.99 kB 🔴
/video (default) 38.33 kB 39.73 kB +1.39 kB +3.6% +51.99 kB 🔺
/video (default + hls) 176.58 kB 177.89 kB +1.32 kB +0.7% +51.99 kB 🔺
/video (minimal) 38.69 kB 40.06 kB +1.37 kB +3.5% +51.99 kB 🔺
/video (minimal + hls) 177.08 kB 178.37 kB +1.29 kB +0.7% +51.99 kB 🔺
/audio (default) 31.54 kB 32.94 kB +1.40 kB +4.4% +51.99 kB 🔺
/audio (minimal) 31.61 kB 33.03 kB +1.42 kB +4.5% +51.99 kB 🔺
Presets (7)
Entry Initial Lazy
/video (default) 39.73 kB 51.99 kB
/video (default + hls) 177.89 kB 51.99 kB
/video (minimal) 40.06 kB 51.99 kB
/video (minimal + hls) 178.37 kB 51.99 kB
/audio (default) 32.94 kB 51.99 kB
/audio (minimal) 33.03 kB 51.99 kB
/background 754 B
Media (9)
Entry Initial
/media/background-video 575 B
/media/dash-video 213.16 kB
/media/hlsjs-video 139.94 kB
/media/mux-audio 162.56 kB
/media/mux-video 162.49 kB
/media/native-hls-video 7.42 kB
/media/simple-hls-audio-only 15.56 kB
/media/simple-hls-video 17.14 kB
/media/vimeo-video 10.58 kB
Skins (27)
Entry Type Initial Lazy
/tailwind.css css 161 B
/video/minimal-skin.css css 5.55 kB
/video/skin.css css 5.52 kB
/video/minimal-skin js 39.99 kB 51.99 kB
/video/minimal-skin.tailwind js 46.05 kB 51.99 kB
/video/skin js 39.63 kB 51.99 kB
/video/skin.tailwind js 45.71 kB 51.99 kB
/audio/minimal-skin.css css 3.79 kB
/audio/skin.css css 3.68 kB
/audio/minimal-skin js 32.91 kB 51.99 kB
/audio/minimal-skin.tailwind js 35.15 kB 51.99 kB
/audio/skin js 32.88 kB 51.99 kB
/audio/skin.tailwind js 37.03 kB 51.99 kB
/background/skin.css css 90 B
/background/skin js 272 B
/live-video/minimal-skin.css css 5.55 kB
/live-video/skin.css css 5.52 kB
/live-video/minimal-skin js 33.86 kB
/live-video/minimal-skin.tailwind js 39.76 kB
/live-video/skin js 33.86 kB
/live-video/skin.tailwind js 39.86 kB
/live-audio/minimal-skin.css css 3.79 kB
/live-audio/skin.css css 3.68 kB
/live-audio/minimal-skin js 22.69 kB
/live-audio/minimal-skin.tailwind js 25.81 kB
/live-audio/skin js 22.74 kB
/live-audio/skin.tailwind js 25.85 kB
UI Components (33)
Entry Initial
/ui/airplay-button 2.39 kB
/ui/alert-dialog 2.31 kB
/ui/audio-track 2.26 kB
/ui/buffering-indicator 2.25 kB
/ui/captions-button 2.35 kB
/ui/captions-radio-group 2.24 kB
/ui/cast-button 2.38 kB
/ui/controls 2.27 kB
/ui/error-dialog 2.40 kB
/ui/fullscreen-button 2.34 kB
/ui/gesture 2.33 kB
/ui/hotkey 2.33 kB
/ui/live-button 2.28 kB
/ui/menu 2.58 kB
/ui/mute-button 2.37 kB
/ui/pip-button 2.38 kB
/ui/play-button 2.38 kB
/ui/playback-rate 2.21 kB
/ui/playback-rate-button 2.45 kB
/ui/popover 2.75 kB
/ui/poster 2.22 kB
/ui/quality 2.24 kB
/ui/seek-button 2.34 kB
/ui/seek-indicator 2.26 kB
/ui/slider 2.42 kB
/ui/status-announcer 2.26 kB
/ui/status-indicator 2.29 kB
/ui/thumbnail 2.21 kB
/ui/time 1.99 kB
/ui/time-slider 2.39 kB
/ui/tooltip 2.77 kB
/ui/volume-indicator 2.26 kB
/ui/volume-slider 2.41 kB

Sizes are marginal over the root entry point.

🧩 @videojs/core

Path Base initial PR initial Diff % Lazy
. 9.40 kB 10.81 kB +1.41 kB +15.0% 🔴
/i18n 2.68 kB +51.99 kB 🆕
/i18n/runtime 1.32 kB 🆕
/i18n/locales/all 30.62 kB 🆕
/i18n/locales/ar 1.15 kB 🆕
/i18n/locales/az 1.04 kB 🆕
/i18n/locales/bg 1.21 kB 🆕
/i18n/locales/bn 1.23 kB 🆕
/i18n/locales/bs 981 B 🆕
/i18n/locales/ca 1.01 kB 🆕
/i18n/locales/cs 1019 B 🆕
/i18n/locales/cy 987 B 🆕
/i18n/locales/da 965 B 🆕
/i18n/locales/de 1.04 kB 🆕
/i18n/locales/el 1.39 kB 🆕
/i18n/locales/en 763 B 🆕
/i18n/locales/es 970 B 🆕
/i18n/locales/et 1.00 kB 🆕
/i18n/locales/eu 979 B 🆕
/i18n/locales/fa 1.15 kB 🆕
/i18n/locales/fi 1007 B 🆕
/i18n/locales/fr 1.01 kB 🆕
/i18n/locales/gd 1.05 kB 🆕
/i18n/locales/gl 965 B 🆕
/i18n/locales/he 1.07 kB 🆕
/i18n/locales/hi 1.24 kB 🆕
/i18n/locales/hr 1000 B 🆕
/i18n/locales/hu 1.04 kB 🆕
/i18n/locales/it 1004 B 🆕
/i18n/locales/ja 1.13 kB 🆕
/i18n/locales/ko 1.08 kB 🆕
/i18n/locales/lv 1.02 kB 🆕
/i18n/locales/mr 1.23 kB 🆕
/i18n/locales/nb 954 B 🆕
/i18n/locales/ne 1.23 kB 🆕
/i18n/locales/nl 976 B 🆕
/i18n/locales/nn 951 B 🆕
/i18n/locales/oc 1.02 kB 🆕
/i18n/locales/pl 1.07 kB 🆕
/i18n/locales/pt 981 B 🆕
/i18n/locales/pt-BR 981 B 🆕
/i18n/locales/pt-PT 954 B 🆕
/i18n/locales/ro 1.00 kB 🆕
/i18n/locales/ru 1.28 kB 🆕
/i18n/locales/sk 1.05 kB 🆕
/i18n/locales/sl 1008 B 🆕
/i18n/locales/sr 976 B 🆕
/i18n/locales/sv 978 B 🆕
/i18n/locales/te 1.26 kB 🆕
/i18n/locales/th 1.23 kB 🆕
/i18n/locales/tr 1.03 kB 🆕
/i18n/locales/uk 1.30 kB 🆕
/i18n/locales/vi 1.04 kB 🆕
/i18n/locales/zh 973 B 🆕
/i18n/locales/zh-CN 973 B 🆕
/i18n/locales/zh-TW 980 B 🆕
Entries (69)
Entry Initial Lazy
. 10.81 kB
/dom 17.56 kB
/dom/media/custom-media-element 2.09 kB
/dom/media/dash 208.96 kB
/dom/media/google-cast 4.03 kB
/dom/media/hls-js 135.88 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.09 kB
/dom/media/simple-hls 16.51 kB
/dom/media/simple-hls-audio-only 14.92 kB
/dom/media/vimeo 9.86 kB
/media/predicate 563 B
/i18n 2.68 kB 51.99 kB
/i18n/runtime 1.32 kB
/i18n/locales/all 30.62 kB
/i18n/locales/ar 1.15 kB
/i18n/locales/az 1.04 kB
/i18n/locales/bg 1.21 kB
/i18n/locales/bn 1.23 kB
/i18n/locales/bs 981 B
/i18n/locales/ca 1.01 kB
/i18n/locales/cs 1019 B
/i18n/locales/cy 987 B
/i18n/locales/da 965 B
/i18n/locales/de 1.04 kB
/i18n/locales/el 1.39 kB
/i18n/locales/en 763 B
/i18n/locales/es 970 B
/i18n/locales/et 1.00 kB
/i18n/locales/eu 979 B
/i18n/locales/fa 1.15 kB
/i18n/locales/fi 1007 B
/i18n/locales/fr 1.01 kB
/i18n/locales/gd 1.05 kB
/i18n/locales/gl 965 B
/i18n/locales/he 1.07 kB
/i18n/locales/hi 1.24 kB
/i18n/locales/hr 1000 B
/i18n/locales/hu 1.04 kB
/i18n/locales/it 1004 B
/i18n/locales/ja 1.13 kB
/i18n/locales/ko 1.08 kB
/i18n/locales/lv 1.02 kB
/i18n/locales/mr 1.23 kB
/i18n/locales/nb 954 B
/i18n/locales/ne 1.23 kB
/i18n/locales/nl 976 B
/i18n/locales/nn 951 B
/i18n/locales/oc 1.02 kB
/i18n/locales/pl 1.07 kB
/i18n/locales/pt 981 B
/i18n/locales/pt-BR 981 B
/i18n/locales/pt-PT 954 B
/i18n/locales/ro 1.00 kB
/i18n/locales/ru 1.28 kB
/i18n/locales/sk 1.05 kB
/i18n/locales/sl 1008 B
/i18n/locales/sr 976 B
/i18n/locales/sv 978 B
/i18n/locales/te 1.26 kB
/i18n/locales/th 1.23 kB
/i18n/locales/tr 1.03 kB
/i18n/locales/uk 1.30 kB
/i18n/locales/vi 1.04 kB
/i18n/locales/zh 973 B
/i18n/locales/zh-CN 973 B
/i18n/locales/zh-TW 980 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

Path Base initial PR initial Diff % Lazy
/dom 2.26 kB 2.71 kB +465 B +20.1% 🔴
/time 478 B 792 B +314 B +65.7% 🔴
Entries (10)
Entry Initial
/array 104 B
/dom 2.71 kB
/events 319 B
/function 327 B
/object 275 B
/predicate 265 B
/string 231 B
/style 190 B
/time 792 B
/number 158 B
📦 @videojs/spf — no changes
Entries (4)
Entry Initial
. 4.45 kB
/dom 6.33 kB
/hls 15.37 kB
/background-video 12.89 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.

@sampotts sampotts changed the title feat(core): restore i18n stack base feat(packages): add i18n Jun 18, 2026

@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 2 potential issues.

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 3d1a240. Configure here.

"sideEffects": false,
"sideEffects": [
"**/i18n/registry.js"
],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

sideEffects omits bundled i18n entry

Medium Severity

sideEffects only lists **/i18n/registry.js, but the published @videojs/core/i18n entry is a single i18n bundle that inlines registry.ts (including pre-registering en). Bundlers that honor sideEffects may tree-shake away registry initialization when consumers import named exports from @videojs/core/i18n, leaving translations without the English base layer.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3d1a240. Configure here.

if (!expectedPlatform.has(tag)) {
warnings.push(`Unexpected locale re-export packages/${pkg}/src/i18n/locales/${file}`);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Workspace check crashes missing locale dirs

Low Severity

Check 7 calls readdirSync on packages/html/src/i18n/locales and packages/react/src/i18n/locales without verifying those directories exist. If locale stubs were not generated yet, pnpm check:workspace throws instead of reporting a clear warning like the per-file existsSync checks above.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3d1a240. Configure here.

@mihar-22 mihar-22 changed the title feat(packages): add i18n feat(packages): i18n foundation Jul 7, 2026

@mihar-22 mihar-22 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Great work and thanks for doing this 😎

Overall this is feeling pretty good, but the thing giving me the biggest pause is how much i18n seems to be leaking down into component props, keys, and types. I’m also feeling some inconsistency in how the translation keys are set/formatted/named.

We’ve been talking a bit in the background about having a better text primitive, which feels related to one part of this: how do we simplify where text lives?

The other part is that I would have ideally liked to see something more like an optional translation function passed down through context. When available, components could use it. When not available, they could just fall back to the provided word or phrase. That feels like a cleaner separation of concerns to me, and it would also help with bundle size since loading the English locale by default feels a bit unfortunate. I think keys are creating some of that complexity right now.

I’m not opposed to exploring backing out of translation keys. The naming of them also feels a little unclear to me right now. It’s a mix of UI-specific keys like indicator*, menu*, errorDialog*, and then more generic words like play or mute. My gut says I’d prefer mostly plain words/phrases, unless there’s a strong reason the UI location matters for translation context.

Whatever direction we go with, I think the main things I’d like us to preserve are:

  • it should be easy to understand where words live
  • translating them should be straightforward when needed
  • translation should stay completely optional (i should still be able to easily override any text on a component)
  • i18n shouldn’t leak into component APIs/types
  • we should be mindful of bundle size - ideally no need for a english translation map

I'm approving to unblock but it'd be nice if we can have some follow-up discussions/work on this front. Love to hear what you think about it all more too.


Maybe one follow-up for next time is whether we can split/chunk the CDN piece a bit more intentionally and add clear notes in PR description on how it would be consumed. I know it's somewhere in the design doc but that's harder to surface or have a clear/scoped conversation around.

*
* @example `es-419-u-nu-latn` → `['es-419', 'es', 'en']`
*/
export function localeLookupChain(locale: string): string[] {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(suggestion): findLocaleKeys

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(suggestion): maybe we should move browser translation to /dom? we can then use lib types if they exist or extend globals.d.ts to clean up globalThis stuff here

}

/** Registry map key: normalized tag with unicode extensions removed (same base as {@link localeLookupChain}). */
export function canonicalLocaleRegistryKey(locale: string): string {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(suggestion): getCanonicalLocaleKey

return out;
}

function mergeLookupChain(chain: string[]): Translations {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(suggestion): mergeI18nTranslations

@@ -0,0 +1,132 @@
import en from './locales/en';
import type { Translations } from './types';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Great if we can import and use TranslationKey type for this file.

@@ -0,0 +1,29 @@
function readLang(node: Element): string | undefined {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(suggestion): getElementLang

}

/** First non-empty `lang` on `start` or an ancestor (HTML language inheritance). */
export function nearestLang(start: Element | null): string | undefined {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(suggestion): findNearestLang

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We could have a generic walk up utility that allows for a callback which this builds on.

Comment on lines +180 to +193
export function formatVolumePercent(fraction: number, locale?: string | string[]): string {
const value = !isNumber(fraction) || !Number.isFinite(fraction) ? 0 : Math.min(1, Math.max(0, fraction));

try {
const formatter = getPercentFormatter(locale) ?? getPercentFormatter(undefined);
if (formatter) {
return formatter.format(value);
}
} catch {
// fall through to simple percent string
}

return formatVolumePercentFallback(value);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

as per earlier comment, this doesn't belong in time/format

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

maybe this function can be generalised, doesn't feel like a general/reusable utility at the moment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

i kind of get why this exists but it might be too advanced for our needs? love to see real-world examples

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is to subscribe to the document lang - e.g. for sites with language selectors, this would ensure the player updates to match.

@mihar-22 mihar-22 marked this pull request as ready for review July 7, 2026 20:07
@mihar-22 mihar-22 mentioned this pull request Jul 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants