From 028dadb385eb4f879f80932a4002676dc11d5300 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Fri, 10 Jul 2026 12:58:47 +1000 Subject: [PATCH] feat(packages): i18n (#1708) --- .github/workflows/e2e.yml | 4 + apps/e2e/package.json | 1 + apps/e2e/playwright.config.ts | 85 +- apps/e2e/tests/sandbox-cdn-i18n.spec.ts | 47 + apps/e2e/tests/sandbox-html-i18n.spec.ts | 60 + apps/sandbox/README.md | 4 +- apps/sandbox/app/shared/html/i18n.ts | 42 + .../shared/i18n/browser-sandbox-prefetch.ts | 37 + .../i18n/cdn-locale-loaders.generated.ts | 57 + .../app/shared/i18n/cdn-sandbox-locales.ts | 48 + apps/sandbox/app/shared/i18n/locale-meta.ts | 114 ++ .../app/shared/i18n/sandbox-locales.ts | 55 + .../sandbox/app/shared/react/sandbox-i18n.tsx | 26 + apps/sandbox/app/shared/react/use-locale.ts | 11 + apps/sandbox/app/shared/sandbox-listener.ts | 26 + apps/sandbox/app/shell/app.tsx | 26 +- apps/sandbox/app/shell/navbar.tsx | 51 +- apps/sandbox/app/shell/preview.tsx | 22 +- .../scripts/generate-cdn-locale-loaders.ts | 28 + apps/sandbox/scripts/setup.ts | 2 + apps/sandbox/scripts/shared.ts | 10 +- apps/sandbox/templates/cdn/index.html | 4 +- apps/sandbox/templates/cdn/main.ts | 171 ++- apps/sandbox/templates/html-audio/index.html | 4 +- apps/sandbox/templates/html-audio/main.ts | 9 +- .../html-background-video/index.html | 2 +- .../templates/html-background-video/main.ts | 23 +- .../templates/html-dash-video/index.html | 4 +- .../sandbox/templates/html-dash-video/main.ts | 9 +- .../templates/html-hlsjs-video/index.html | 4 +- .../templates/html-hlsjs-video/main.ts | 9 +- .../templates/html-mux-audio/index.html | 4 +- apps/sandbox/templates/html-mux-audio/main.ts | 9 +- .../templates/html-mux-video/index.html | 4 +- apps/sandbox/templates/html-mux-video/main.ts | 9 +- .../html-native-hls-video/index.html | 4 +- .../templates/html-native-hls-video/main.ts | 9 +- .../html-simple-hls-audio-only/index.html | 4 +- .../html-simple-hls-video/index.html | 4 +- .../templates/html-simple-hls-video/main.ts | 9 +- apps/sandbox/templates/html-video/index.html | 4 +- apps/sandbox/templates/html-video/main.ts | 11 +- .../templates/html-vimeo-video/index.html | 4 +- apps/sandbox/templates/react-audio/index.html | 4 +- apps/sandbox/templates/react-audio/main.tsx | 16 +- .../react-background-video/index.html | 2 +- .../templates/react-background-video/main.tsx | 13 +- .../templates/react-dash-video/index.html | 4 +- .../templates/react-dash-video/main.tsx | 27 +- .../templates/react-hlsjs-video/index.html | 4 +- .../templates/react-hlsjs-video/main.tsx | 43 +- .../templates/react-mux-audio/index.html | 4 +- .../templates/react-mux-audio/main.tsx | 27 +- .../templates/react-mux-video/index.html | 4 +- .../templates/react-mux-video/main.tsx | 45 +- .../react-native-hls-video/index.html | 4 +- .../templates/react-native-hls-video/main.tsx | 43 +- .../react-simple-hls-audio-only/index.html | 4 +- .../react-simple-hls-video/index.html | 4 +- .../templates/react-simple-hls-video/main.tsx | 43 +- apps/sandbox/templates/react-video/index.html | 4 +- apps/sandbox/templates/react-video/main.tsx | 36 +- .../templates/react-vimeo-video/index.html | 4 +- apps/sandbox/vite.config.ts | 115 +- biome.json | 5 + build/plugins/cdn-i18n-external-plugin.ts | 48 + build/scripts/check-workspace.mjs | 128 ++ cspell.json | 1 + internal/design/i18n.md | 391 +++--- package.json | 1 + packages/core/package.json | 17 +- .../core/scripts/generate-i18n-locales.ts | 147 +++ .../core/src/core/i18n/browser-translation.ts | 178 +++ packages/core/src/core/i18n/index.ts | 23 + packages/core/src/core/i18n/load-locale.ts | 68 ++ packages/core/src/core/i18n/locales.ts | 65 + packages/core/src/core/i18n/locales/all.ts | 115 ++ packages/core/src/core/i18n/locales/ar.ts | 67 + packages/core/src/core/i18n/locales/az.ts | 68 ++ packages/core/src/core/i18n/locales/bg.ts | 67 + packages/core/src/core/i18n/locales/bn.ts | 68 ++ packages/core/src/core/i18n/locales/bs.ts | 67 + packages/core/src/core/i18n/locales/ca.ts | 68 ++ packages/core/src/core/i18n/locales/cs.ts | 67 + packages/core/src/core/i18n/locales/cy.ts | 68 ++ packages/core/src/core/i18n/locales/da.ts | 68 ++ packages/core/src/core/i18n/locales/de.ts | 69 ++ packages/core/src/core/i18n/locales/el.ts | 69 ++ packages/core/src/core/i18n/locales/en.ts | 69 ++ packages/core/src/core/i18n/locales/es.ts | 68 ++ packages/core/src/core/i18n/locales/et.ts | 69 ++ packages/core/src/core/i18n/locales/eu.ts | 68 ++ packages/core/src/core/i18n/locales/fa.ts | 68 ++ packages/core/src/core/i18n/locales/fi.ts | 67 + packages/core/src/core/i18n/locales/fr.ts | 69 ++ packages/core/src/core/i18n/locales/gd.ts | 69 ++ packages/core/src/core/i18n/locales/gl.ts | 68 ++ packages/core/src/core/i18n/locales/he.ts | 67 + packages/core/src/core/i18n/locales/hi.ts | 68 ++ packages/core/src/core/i18n/locales/hr.ts | 67 + packages/core/src/core/i18n/locales/hu.ts | 68 ++ packages/core/src/core/i18n/locales/it.ts | 69 ++ packages/core/src/core/i18n/locales/ja.ts | 69 ++ packages/core/src/core/i18n/locales/ko.ts | 69 ++ packages/core/src/core/i18n/locales/lv.ts | 68 ++ packages/core/src/core/i18n/locales/mr.ts | 68 ++ packages/core/src/core/i18n/locales/nb.ts | 68 ++ packages/core/src/core/i18n/locales/ne.ts | 68 ++ packages/core/src/core/i18n/locales/nl.ts | 69 ++ packages/core/src/core/i18n/locales/nn.ts | 68 ++ packages/core/src/core/i18n/locales/oc.ts | 68 ++ packages/core/src/core/i18n/locales/pl.ts | 69 ++ packages/core/src/core/i18n/locales/pt-BR.ts | 68 ++ packages/core/src/core/i18n/locales/pt-PT.ts | 68 ++ packages/core/src/core/i18n/locales/pt.ts | 2 + packages/core/src/core/i18n/locales/ro.ts | 69 ++ packages/core/src/core/i18n/locales/ru.ts | 68 ++ packages/core/src/core/i18n/locales/sk.ts | 68 ++ packages/core/src/core/i18n/locales/sl.ts | 69 ++ packages/core/src/core/i18n/locales/sr.ts | 67 + packages/core/src/core/i18n/locales/sv.ts | 68 ++ packages/core/src/core/i18n/locales/te.ts | 67 + packages/core/src/core/i18n/locales/th.ts | 67 + packages/core/src/core/i18n/locales/tr.ts | 69 ++ packages/core/src/core/i18n/locales/uk.ts | 68 ++ packages/core/src/core/i18n/locales/vi.ts | 68 ++ packages/core/src/core/i18n/locales/zh-CN.ts | 67 + packages/core/src/core/i18n/locales/zh-TW.ts | 67 + packages/core/src/core/i18n/locales/zh.ts | 2 + packages/core/src/core/i18n/registry.ts | 147 +++ .../core/src/core/i18n/resolve-translation.ts | 18 + .../i18n/tests/browser-translation.test.ts | 205 ++++ .../src/core/i18n/tests/load-locale.test.ts | 48 + .../core/src/core/i18n/tests/registry.test.ts | 98 ++ .../i18n/tests/resolve-translation.test.ts | 38 + .../src/core/i18n/tests/translator.test.ts | 35 + packages/core/src/core/i18n/translator.ts | 31 + packages/core/src/core/i18n/types.ts | 109 ++ packages/core/src/core/index.ts | 2 + .../ui/airplay-button/airplay-button-core.ts | 12 +- .../audio-track-radio-group-core.ts | 12 +- .../captions-button/captions-button-core.ts | 12 +- .../captions-radio-group-core.ts | 12 +- .../tests/captions-radio-group-core.test.ts | 17 + .../core/ui/cast-button/cast-button-core.ts | 12 +- .../core/ui/error-dialog/error-dialog-i18n.ts | 74 ++ .../tests/error-dialog-i18n.test.ts | 58 + .../fullscreen-button-core.ts | 12 +- .../core/src/core/ui/input-feedback/labels.ts | 19 + .../core/src/core/ui/input-feedback/status.ts | 4 +- .../ui/input-feedback/tests/labels.test.ts | 32 + .../input-feedback/volume-indicator-core.ts | 12 +- .../core/ui/live-button/live-button-core.ts | 12 +- .../core/ui/mute-button/mute-button-core.ts | 12 +- .../tests/mute-button-core.test.ts | 4 +- .../src/core/ui/pip-button/pip-button-core.ts | 12 +- .../core/ui/play-button/play-button-core.ts | 12 +- .../tests/play-button-core.test.ts | 6 +- .../playback-rate-button-core.ts | 17 +- .../tests/playback-rate-button-core.test.ts | 20 +- .../playback-rate-radio-group-core.ts | 19 +- .../playback-rate-radio-group-core.test.ts | 16 +- .../quality-radio-group-core.ts | 12 +- .../core/ui/seek-button/seek-button-core.ts | 18 +- .../tests/seek-button-core.test.ts | 40 +- .../core/src/core/ui/slider/slider-core.ts | 14 +- .../tests/time-slider-core.test.ts | 35 +- .../core/ui/time-slider/time-slider-core.ts | 44 +- .../src/core/ui/time/tests/time-core.test.ts | 57 +- packages/core/src/core/ui/time/time-core.ts | 41 +- .../core/src/core/ui/utils/resolve-label.ts | 11 + .../core/ui/utils/tests/resolve-label.test.ts | 20 + .../tests/volume-slider-core.test.ts | 15 +- .../ui/volume-slider/volume-slider-core.ts | 20 +- packages/core/src/dom/media/dash/index.ts | 4 +- .../core/src/dom/media/native-hls/errors.ts | 4 +- .../dom/media/native-hls/tests/errors.test.ts | 18 +- .../tests/native-hls-custom-media.test.ts | 2 +- .../dom/media/simple-hls-audio-only/index.ts | 4 +- .../core/src/dom/media/simple-hls/index.ts | 4 +- packages/core/src/dom/media/vimeo/index.ts | 4 +- packages/core/tsdown.config.ts | 11 + packages/html/package.json | 29 +- packages/html/scripts/build-cdn-locales.ts | 21 + packages/html/src/cdn/i18n-registry.ts | 1 + packages/html/src/cdn/i18n.ts | 19 + packages/html/src/cdn/locales/.gitignore | 1 + packages/html/src/cdn/video.ts | 1 + .../src/define/audio/minimal-skin.tailwind.ts | 4 +- .../html/src/define/audio/minimal-skin.ts | 4 +- .../html/src/define/audio/skin.tailwind.ts | 4 +- packages/html/src/define/audio/skin.ts | 4 +- packages/html/src/define/audio/ui.ts | 4 + packages/html/src/define/global.css | 3 +- packages/html/src/define/i18n.ts | 13 + .../live-audio/minimal-skin.tailwind.ts | 4 +- .../src/define/live-audio/minimal-skin.ts | 4 +- .../src/define/live-audio/skin.tailwind.ts | 4 +- packages/html/src/define/live-audio/skin.ts | 4 +- packages/html/src/define/live-audio/ui.ts | 5 + .../live-video/minimal-skin.tailwind.ts | 4 +- .../src/define/live-video/minimal-skin.ts | 4 +- .../src/define/live-video/skin.tailwind.ts | 4 +- packages/html/src/define/live-video/skin.ts | 4 +- packages/html/src/define/live-video/ui.ts | 5 + packages/html/src/define/shared.css | 12 + packages/html/src/define/skin-element.ts | 1 - .../src/define/tests/registration.test.ts | 10 + .../src/define/video/minimal-skin.tailwind.ts | 23 +- .../html/src/define/video/minimal-skin.ts | 23 +- packages/html/src/define/video/minimal-ui.ts | 1 + .../html/src/define/video/skin.tailwind.ts | 23 +- packages/html/src/define/video/skin.ts | 23 +- packages/html/src/define/video/ui.ts | 5 + packages/html/src/i18n/context.ts | 23 + packages/html/src/i18n/controller.ts | 65 + packages/html/src/i18n/create-i18n.ts | 32 + packages/html/src/i18n/index.ts | 36 + packages/html/src/i18n/locale.ts | 15 + packages/html/src/i18n/locales/all.ts | 2 + .../html/src/i18n/locales/all/register.ts | 7 + packages/html/src/i18n/locales/ar.ts | 2 + packages/html/src/i18n/locales/ar/register.ts | 5 + packages/html/src/i18n/locales/az.ts | 2 + packages/html/src/i18n/locales/az/register.ts | 5 + packages/html/src/i18n/locales/bg.ts | 2 + packages/html/src/i18n/locales/bg/register.ts | 5 + packages/html/src/i18n/locales/bn.ts | 2 + packages/html/src/i18n/locales/bn/register.ts | 5 + packages/html/src/i18n/locales/bs.ts | 2 + packages/html/src/i18n/locales/bs/register.ts | 5 + packages/html/src/i18n/locales/ca.ts | 2 + packages/html/src/i18n/locales/ca/register.ts | 5 + packages/html/src/i18n/locales/cs.ts | 2 + packages/html/src/i18n/locales/cs/register.ts | 5 + packages/html/src/i18n/locales/cy.ts | 2 + packages/html/src/i18n/locales/cy/register.ts | 5 + packages/html/src/i18n/locales/da.ts | 2 + packages/html/src/i18n/locales/da/register.ts | 5 + packages/html/src/i18n/locales/de.ts | 2 + packages/html/src/i18n/locales/de/register.ts | 5 + packages/html/src/i18n/locales/el.ts | 2 + packages/html/src/i18n/locales/el/register.ts | 5 + packages/html/src/i18n/locales/en.ts | 2 + packages/html/src/i18n/locales/en/register.ts | 5 + packages/html/src/i18n/locales/es.ts | 2 + packages/html/src/i18n/locales/es/register.ts | 5 + packages/html/src/i18n/locales/et.ts | 2 + packages/html/src/i18n/locales/et/register.ts | 5 + packages/html/src/i18n/locales/eu.ts | 2 + packages/html/src/i18n/locales/eu/register.ts | 5 + packages/html/src/i18n/locales/fa.ts | 2 + packages/html/src/i18n/locales/fa/register.ts | 5 + packages/html/src/i18n/locales/fi.ts | 2 + packages/html/src/i18n/locales/fi/register.ts | 5 + packages/html/src/i18n/locales/fr.ts | 2 + packages/html/src/i18n/locales/fr/register.ts | 5 + packages/html/src/i18n/locales/gd.ts | 2 + packages/html/src/i18n/locales/gd/register.ts | 5 + packages/html/src/i18n/locales/gl.ts | 2 + packages/html/src/i18n/locales/gl/register.ts | 5 + packages/html/src/i18n/locales/he.ts | 2 + packages/html/src/i18n/locales/he/register.ts | 5 + packages/html/src/i18n/locales/hi.ts | 2 + packages/html/src/i18n/locales/hi/register.ts | 5 + packages/html/src/i18n/locales/hr.ts | 2 + packages/html/src/i18n/locales/hr/register.ts | 5 + packages/html/src/i18n/locales/hu.ts | 2 + packages/html/src/i18n/locales/hu/register.ts | 5 + packages/html/src/i18n/locales/it.ts | 2 + packages/html/src/i18n/locales/it/register.ts | 5 + packages/html/src/i18n/locales/ja.ts | 2 + packages/html/src/i18n/locales/ja/register.ts | 5 + packages/html/src/i18n/locales/ko.ts | 2 + packages/html/src/i18n/locales/ko/register.ts | 5 + packages/html/src/i18n/locales/lv.ts | 2 + packages/html/src/i18n/locales/lv/register.ts | 5 + packages/html/src/i18n/locales/mr.ts | 2 + packages/html/src/i18n/locales/mr/register.ts | 5 + packages/html/src/i18n/locales/nb.ts | 2 + packages/html/src/i18n/locales/nb/register.ts | 5 + packages/html/src/i18n/locales/ne.ts | 2 + packages/html/src/i18n/locales/ne/register.ts | 5 + packages/html/src/i18n/locales/nl.ts | 2 + packages/html/src/i18n/locales/nl/register.ts | 5 + packages/html/src/i18n/locales/nn.ts | 2 + packages/html/src/i18n/locales/nn/register.ts | 5 + packages/html/src/i18n/locales/oc.ts | 2 + packages/html/src/i18n/locales/oc/register.ts | 5 + packages/html/src/i18n/locales/pl.ts | 2 + packages/html/src/i18n/locales/pl/register.ts | 5 + packages/html/src/i18n/locales/pt-BR.ts | 2 + .../html/src/i18n/locales/pt-BR/register.ts | 5 + packages/html/src/i18n/locales/pt-PT.ts | 2 + .../html/src/i18n/locales/pt-PT/register.ts | 5 + packages/html/src/i18n/locales/pt.ts | 2 + packages/html/src/i18n/locales/pt/register.ts | 5 + packages/html/src/i18n/locales/ro.ts | 2 + packages/html/src/i18n/locales/ro/register.ts | 5 + packages/html/src/i18n/locales/ru.ts | 2 + packages/html/src/i18n/locales/ru/register.ts | 5 + packages/html/src/i18n/locales/sk.ts | 2 + packages/html/src/i18n/locales/sk/register.ts | 5 + packages/html/src/i18n/locales/sl.ts | 2 + packages/html/src/i18n/locales/sl/register.ts | 5 + packages/html/src/i18n/locales/sr.ts | 2 + packages/html/src/i18n/locales/sr/register.ts | 5 + packages/html/src/i18n/locales/sv.ts | 2 + packages/html/src/i18n/locales/sv/register.ts | 5 + packages/html/src/i18n/locales/te.ts | 2 + packages/html/src/i18n/locales/te/register.ts | 5 + packages/html/src/i18n/locales/th.ts | 2 + packages/html/src/i18n/locales/th/register.ts | 5 + packages/html/src/i18n/locales/tr.ts | 2 + packages/html/src/i18n/locales/tr/register.ts | 5 + packages/html/src/i18n/locales/uk.ts | 2 + packages/html/src/i18n/locales/uk/register.ts | 5 + packages/html/src/i18n/locales/vi.ts | 2 + packages/html/src/i18n/locales/vi/register.ts | 5 + packages/html/src/i18n/locales/zh-CN.ts | 2 + .../html/src/i18n/locales/zh-CN/register.ts | 5 + packages/html/src/i18n/locales/zh-TW.ts | 2 + .../html/src/i18n/locales/zh-TW/register.ts | 5 + packages/html/src/i18n/locales/zh.ts | 2 + packages/html/src/i18n/locales/zh/register.ts | 5 + packages/html/src/i18n/provider-element.ts | 11 + packages/html/src/i18n/provider-mixin.ts | 161 +++ .../html/src/i18n/tests/create-i18n.test.ts | 415 +++++++ packages/html/src/i18n/tests/provider.test.ts | 187 +++ packages/html/src/i18n/text-mixin.ts | 40 + packages/html/src/i18n/types.ts | 14 + packages/html/src/index.ts | 28 + .../audio-track-radio-group-element.ts | 64 +- .../audio-track-radio-group-element.test.ts | 54 +- .../captions-radio-group-element.ts | 60 +- .../captions-radio-group-element.test.ts | 124 ++ .../ui/error-dialog/error-dialog-element.ts | 72 +- .../tests/error-dialog-element.test.ts | 64 + .../src/ui/live-button/live-button-element.ts | 18 +- packages/html/src/ui/media-button-element.ts | 32 +- packages/html/src/ui/media-ui-element.ts | 15 +- packages/html/src/ui/menu/context.ts | 12 +- .../ui/menu/get-menu-item-setting-state.ts | 12 +- .../html/src/ui/menu/menu-back-element.ts | 6 +- .../html/src/ui/menu/menu-item-element.ts | 11 +- .../src/ui/menu/menu-radio-group-element.ts | 69 +- .../tests/menu-item-value-element.test.ts | 55 +- .../playback-rate-radio-group-element.ts | 49 +- .../playback-rate-radio-group-element.test.ts | 4 +- .../quality-radio-group-element.ts | 64 +- .../tests/quality-radio-group-element.test.ts | 24 +- packages/html/src/ui/radio-group/context.ts | 10 + .../src/ui/radio-group/radio-group-element.ts | 27 + .../status-announcer-element.ts | 10 +- .../status-indicator-element.ts | 10 +- .../src/ui/tests/media-button-element.test.ts | 105 +- packages/html/src/ui/text/text-element.ts | 12 + .../tests/time-slider-element.test.ts | 2 +- .../src/ui/time-slider/time-slider-element.ts | 27 +- .../src/ui/time/tests/time-element.test.ts | 2 - packages/html/src/ui/time/time-element.ts | 26 +- .../ui/tooltip/tests/tooltip-element.test.ts | 180 ++- .../html/src/ui/tooltip/tooltip-element.ts | 21 +- .../src/ui/tooltip/tooltip-label-element.ts | 12 + .../volume-indicator-element.ts | 9 +- .../tests/volume-slider-element.test.ts | 2 +- .../ui/volume-slider/volume-slider-element.ts | 16 +- packages/html/tsconfig.json | 3 +- packages/html/tsdown.cdn.config.ts | 14 +- packages/html/tsdown.config.ts | 16 +- packages/react/package.json | 14 +- packages/react/src/i18n/context.tsx | 63 + packages/react/src/i18n/create-i18n.tsx | 132 ++ .../react/src/i18n/get-provider-root-props.ts | 42 + packages/react/src/i18n/index.ts | 26 + packages/react/src/i18n/locales/all.ts | 2 + .../react/src/i18n/locales/all/register.ts | 7 + packages/react/src/i18n/locales/ar.ts | 2 + .../react/src/i18n/locales/ar/register.ts | 5 + packages/react/src/i18n/locales/az.ts | 2 + .../react/src/i18n/locales/az/register.ts | 5 + packages/react/src/i18n/locales/bg.ts | 2 + .../react/src/i18n/locales/bg/register.ts | 5 + packages/react/src/i18n/locales/bn.ts | 2 + .../react/src/i18n/locales/bn/register.ts | 5 + packages/react/src/i18n/locales/bs.ts | 2 + .../react/src/i18n/locales/bs/register.ts | 5 + packages/react/src/i18n/locales/ca.ts | 2 + .../react/src/i18n/locales/ca/register.ts | 5 + packages/react/src/i18n/locales/cs.ts | 2 + .../react/src/i18n/locales/cs/register.ts | 5 + packages/react/src/i18n/locales/cy.ts | 2 + .../react/src/i18n/locales/cy/register.ts | 5 + packages/react/src/i18n/locales/da.ts | 2 + .../react/src/i18n/locales/da/register.ts | 5 + packages/react/src/i18n/locales/de.ts | 2 + .../react/src/i18n/locales/de/register.ts | 5 + packages/react/src/i18n/locales/el.ts | 2 + .../react/src/i18n/locales/el/register.ts | 5 + packages/react/src/i18n/locales/en.ts | 2 + .../react/src/i18n/locales/en/register.ts | 5 + packages/react/src/i18n/locales/es.ts | 2 + .../react/src/i18n/locales/es/register.ts | 5 + packages/react/src/i18n/locales/et.ts | 2 + .../react/src/i18n/locales/et/register.ts | 5 + packages/react/src/i18n/locales/eu.ts | 2 + .../react/src/i18n/locales/eu/register.ts | 5 + packages/react/src/i18n/locales/fa.ts | 2 + .../react/src/i18n/locales/fa/register.ts | 5 + packages/react/src/i18n/locales/fi.ts | 2 + .../react/src/i18n/locales/fi/register.ts | 5 + packages/react/src/i18n/locales/fr.ts | 2 + .../react/src/i18n/locales/fr/register.ts | 5 + packages/react/src/i18n/locales/gd.ts | 2 + .../react/src/i18n/locales/gd/register.ts | 5 + packages/react/src/i18n/locales/gl.ts | 2 + .../react/src/i18n/locales/gl/register.ts | 5 + packages/react/src/i18n/locales/he.ts | 2 + .../react/src/i18n/locales/he/register.ts | 5 + packages/react/src/i18n/locales/hi.ts | 2 + .../react/src/i18n/locales/hi/register.ts | 5 + packages/react/src/i18n/locales/hr.ts | 2 + .../react/src/i18n/locales/hr/register.ts | 5 + packages/react/src/i18n/locales/hu.ts | 2 + .../react/src/i18n/locales/hu/register.ts | 5 + packages/react/src/i18n/locales/it.ts | 2 + .../react/src/i18n/locales/it/register.ts | 5 + packages/react/src/i18n/locales/ja.ts | 2 + .../react/src/i18n/locales/ja/register.ts | 5 + packages/react/src/i18n/locales/ko.ts | 2 + .../react/src/i18n/locales/ko/register.ts | 5 + packages/react/src/i18n/locales/lv.ts | 2 + .../react/src/i18n/locales/lv/register.ts | 5 + packages/react/src/i18n/locales/mr.ts | 2 + .../react/src/i18n/locales/mr/register.ts | 5 + packages/react/src/i18n/locales/nb.ts | 2 + .../react/src/i18n/locales/nb/register.ts | 5 + packages/react/src/i18n/locales/ne.ts | 2 + .../react/src/i18n/locales/ne/register.ts | 5 + packages/react/src/i18n/locales/nl.ts | 2 + .../react/src/i18n/locales/nl/register.ts | 5 + packages/react/src/i18n/locales/nn.ts | 2 + .../react/src/i18n/locales/nn/register.ts | 5 + packages/react/src/i18n/locales/oc.ts | 2 + .../react/src/i18n/locales/oc/register.ts | 5 + packages/react/src/i18n/locales/pl.ts | 2 + .../react/src/i18n/locales/pl/register.ts | 5 + packages/react/src/i18n/locales/pt-BR.ts | 2 + .../react/src/i18n/locales/pt-BR/register.ts | 5 + packages/react/src/i18n/locales/pt-PT.ts | 2 + .../react/src/i18n/locales/pt-PT/register.ts | 5 + packages/react/src/i18n/locales/pt.ts | 2 + .../react/src/i18n/locales/pt/register.ts | 5 + packages/react/src/i18n/locales/ro.ts | 2 + .../react/src/i18n/locales/ro/register.ts | 5 + packages/react/src/i18n/locales/ru.ts | 2 + .../react/src/i18n/locales/ru/register.ts | 5 + packages/react/src/i18n/locales/sk.ts | 2 + .../react/src/i18n/locales/sk/register.ts | 5 + packages/react/src/i18n/locales/sl.ts | 2 + .../react/src/i18n/locales/sl/register.ts | 5 + packages/react/src/i18n/locales/sr.ts | 2 + .../react/src/i18n/locales/sr/register.ts | 5 + packages/react/src/i18n/locales/sv.ts | 2 + .../react/src/i18n/locales/sv/register.ts | 5 + packages/react/src/i18n/locales/te.ts | 2 + .../react/src/i18n/locales/te/register.ts | 5 + packages/react/src/i18n/locales/th.ts | 2 + .../react/src/i18n/locales/th/register.ts | 5 + packages/react/src/i18n/locales/tr.ts | 2 + .../react/src/i18n/locales/tr/register.ts | 5 + packages/react/src/i18n/locales/uk.ts | 2 + .../react/src/i18n/locales/uk/register.ts | 5 + packages/react/src/i18n/locales/vi.ts | 2 + .../react/src/i18n/locales/vi/register.ts | 5 + packages/react/src/i18n/locales/zh-CN.ts | 2 + .../react/src/i18n/locales/zh-CN/register.ts | 5 + packages/react/src/i18n/locales/zh-TW.ts | 2 + .../react/src/i18n/locales/zh-TW/register.ts | 5 + packages/react/src/i18n/locales/zh.ts | 2 + .../react/src/i18n/locales/zh/register.ts | 5 + .../react/src/i18n/tests/create-i18n.test.tsx | 1086 +++++++++++++++++ packages/react/src/i18n/use-ambient-lang.ts | 19 + .../react/src/i18n/use-lang-root-element.ts | 33 + .../react/src/i18n/use-lazy-translations.ts | 56 + .../src/i18n/use-locale-root-notifications.ts | 33 + .../react/src/i18n/use-merged-translations.ts | 22 + packages/react/src/i18n/use-registry-epoch.ts | 12 + packages/react/src/index.ts | 25 +- packages/react/src/player/container.tsx | 45 + packages/react/src/player/context.tsx | 44 +- packages/react/src/player/create-player.tsx | 3 +- .../react/src/player/tests/context.test.tsx | 67 +- .../src/player/tests/create-player.test.tsx | 58 +- .../presets/audio/minimal-skin.tailwind.tsx | 11 +- .../react/src/presets/audio/minimal-skin.tsx | 16 +- .../react/src/presets/audio/skin.tailwind.tsx | 11 +- packages/react/src/presets/audio/skin.tsx | 16 +- .../live-audio/minimal-skin.tailwind.tsx | 7 +- .../src/presets/live-audio/minimal-skin.tsx | 7 +- .../src/presets/live-audio/skin.tailwind.tsx | 7 +- .../react/src/presets/live-audio/skin.tsx | 7 +- .../live-video/minimal-skin.tailwind.tsx | 11 +- .../src/presets/live-video/minimal-skin.tsx | 11 +- .../src/presets/live-video/skin.tailwind.tsx | 11 +- .../react/src/presets/live-video/skin.tsx | 16 +- .../presets/video/minimal-skin.tailwind.tsx | 35 +- .../react/src/presets/video/minimal-skin.tsx | 35 +- .../react/src/presets/video/skin.tailwind.tsx | 35 +- packages/react/src/presets/video/skin.tsx | 35 +- packages/react/src/testing/mocks.tsx | 7 +- .../ui/audio-track/use-audio-track-options.ts | 5 +- .../tests/use-captions-options.test.tsx | 33 +- .../use-captions-options.ts | 7 +- packages/react/src/ui/create-media-button.tsx | 31 +- .../ui/error-dialog/error-dialog-close.tsx | 43 + .../error-dialog/error-dialog-description.tsx | 16 +- .../ui/error-dialog/error-dialog-title.tsx | 37 + .../react/src/ui/error-dialog/index.parts.ts | 10 +- .../error-dialog/tests/error-dialog.test.tsx | 50 + .../react/src/ui/live-button/live-button.tsx | 16 +- packages/react/src/ui/menu/menu-back.tsx | 5 +- .../ui/menu/menu-item-setting-provider.tsx | 10 +- .../ui/play-button/tests/play-button.test.tsx | 73 ++ .../tests/use-quality-options.test.tsx | 30 +- .../src/ui/quality/use-quality-options.ts | 21 +- .../ui/status-announcer/status-announcer.tsx | 10 +- .../status-indicator-root.tsx | 10 +- .../ui/time-slider/tests/time-slider.test.tsx | 20 + .../src/ui/time-slider/time-slider-root.tsx | 29 +- .../react/src/ui/time/tests/time.test.tsx | 33 + packages/react/src/ui/time/time-value.tsx | 29 +- .../volume-indicator-root.tsx | 11 +- .../ui/volume-slider/volume-slider-root.tsx | 18 +- packages/react/tsdown.config.ts | 14 +- .../tests/text-track-segment-loader.test.ts | 37 +- packages/utils/package.json | 4 + packages/utils/src/dom/index.ts | 6 + .../utils/src/dom/locale/effective-locale.ts | 16 + .../utils/src/dom/locale/find-nearest-lang.ts | 20 + .../src/dom/locale/merge-locale-overlays.ts | 27 + .../utils/src/dom/locale/resolve-lang-attr.ts | 12 + .../src/dom/locale/subscribe-ambient-lang.ts | 59 + .../dom/tests/locale-effective-locale.test.ts | 30 + .../tests/locale-find-nearest-lang.test.ts | 57 + .../locale-merge-locale-overlays.test.ts | 23 + .../tests/locale-resolve-lang-attr.test.ts | 23 + .../locale-subscribe-ambient-lang.test.ts | 64 + .../src/dom/tests/walk-ancestors.test.ts | 26 + packages/utils/src/dom/walk-ancestors.ts | 20 + packages/utils/src/percent/index.ts | 1 + packages/utils/src/percent/percent.ts | 43 + .../utils/src/percent/tests/percent.test.ts | 24 + packages/utils/src/time/format.ts | 151 ++- packages/utils/src/time/tests/format.test.ts | 53 +- packages/utils/src/types/types.ts | 5 + packages/utils/tsdown.config.ts | 1 + .../api-docs-builder/src/util-handler.ts | 29 +- .../docs/api-reference/UtilReference.astro | 4 +- .../src/utils/tests/utilReferenceSlug.test.ts | 13 + site/src/utils/utilReferenceSlug.ts | 15 + 561 files changed, 13059 insertions(+), 1320 deletions(-) create mode 100644 apps/e2e/tests/sandbox-cdn-i18n.spec.ts create mode 100644 apps/e2e/tests/sandbox-html-i18n.spec.ts create mode 100644 apps/sandbox/app/shared/html/i18n.ts create mode 100644 apps/sandbox/app/shared/i18n/browser-sandbox-prefetch.ts create mode 100644 apps/sandbox/app/shared/i18n/cdn-locale-loaders.generated.ts create mode 100644 apps/sandbox/app/shared/i18n/cdn-sandbox-locales.ts create mode 100644 apps/sandbox/app/shared/i18n/locale-meta.ts create mode 100644 apps/sandbox/app/shared/i18n/sandbox-locales.ts create mode 100644 apps/sandbox/app/shared/react/sandbox-i18n.tsx create mode 100644 apps/sandbox/app/shared/react/use-locale.ts create mode 100644 apps/sandbox/scripts/generate-cdn-locale-loaders.ts create mode 100644 build/plugins/cdn-i18n-external-plugin.ts create mode 100644 packages/core/scripts/generate-i18n-locales.ts create mode 100644 packages/core/src/core/i18n/browser-translation.ts create mode 100644 packages/core/src/core/i18n/index.ts create mode 100644 packages/core/src/core/i18n/load-locale.ts create mode 100644 packages/core/src/core/i18n/locales.ts create mode 100644 packages/core/src/core/i18n/locales/all.ts create mode 100644 packages/core/src/core/i18n/locales/ar.ts create mode 100644 packages/core/src/core/i18n/locales/az.ts create mode 100644 packages/core/src/core/i18n/locales/bg.ts create mode 100644 packages/core/src/core/i18n/locales/bn.ts create mode 100644 packages/core/src/core/i18n/locales/bs.ts create mode 100644 packages/core/src/core/i18n/locales/ca.ts create mode 100644 packages/core/src/core/i18n/locales/cs.ts create mode 100644 packages/core/src/core/i18n/locales/cy.ts create mode 100644 packages/core/src/core/i18n/locales/da.ts create mode 100644 packages/core/src/core/i18n/locales/de.ts create mode 100644 packages/core/src/core/i18n/locales/el.ts create mode 100644 packages/core/src/core/i18n/locales/en.ts create mode 100644 packages/core/src/core/i18n/locales/es.ts create mode 100644 packages/core/src/core/i18n/locales/et.ts create mode 100644 packages/core/src/core/i18n/locales/eu.ts create mode 100644 packages/core/src/core/i18n/locales/fa.ts create mode 100644 packages/core/src/core/i18n/locales/fi.ts create mode 100644 packages/core/src/core/i18n/locales/fr.ts create mode 100644 packages/core/src/core/i18n/locales/gd.ts create mode 100644 packages/core/src/core/i18n/locales/gl.ts create mode 100644 packages/core/src/core/i18n/locales/he.ts create mode 100644 packages/core/src/core/i18n/locales/hi.ts create mode 100644 packages/core/src/core/i18n/locales/hr.ts create mode 100644 packages/core/src/core/i18n/locales/hu.ts create mode 100644 packages/core/src/core/i18n/locales/it.ts create mode 100644 packages/core/src/core/i18n/locales/ja.ts create mode 100644 packages/core/src/core/i18n/locales/ko.ts create mode 100644 packages/core/src/core/i18n/locales/lv.ts create mode 100644 packages/core/src/core/i18n/locales/mr.ts create mode 100644 packages/core/src/core/i18n/locales/nb.ts create mode 100644 packages/core/src/core/i18n/locales/ne.ts create mode 100644 packages/core/src/core/i18n/locales/nl.ts create mode 100644 packages/core/src/core/i18n/locales/nn.ts create mode 100644 packages/core/src/core/i18n/locales/oc.ts create mode 100644 packages/core/src/core/i18n/locales/pl.ts create mode 100644 packages/core/src/core/i18n/locales/pt-BR.ts create mode 100644 packages/core/src/core/i18n/locales/pt-PT.ts create mode 100644 packages/core/src/core/i18n/locales/pt.ts create mode 100644 packages/core/src/core/i18n/locales/ro.ts create mode 100644 packages/core/src/core/i18n/locales/ru.ts create mode 100644 packages/core/src/core/i18n/locales/sk.ts create mode 100644 packages/core/src/core/i18n/locales/sl.ts create mode 100644 packages/core/src/core/i18n/locales/sr.ts create mode 100644 packages/core/src/core/i18n/locales/sv.ts create mode 100644 packages/core/src/core/i18n/locales/te.ts create mode 100644 packages/core/src/core/i18n/locales/th.ts create mode 100644 packages/core/src/core/i18n/locales/tr.ts create mode 100644 packages/core/src/core/i18n/locales/uk.ts create mode 100644 packages/core/src/core/i18n/locales/vi.ts create mode 100644 packages/core/src/core/i18n/locales/zh-CN.ts create mode 100644 packages/core/src/core/i18n/locales/zh-TW.ts create mode 100644 packages/core/src/core/i18n/locales/zh.ts create mode 100644 packages/core/src/core/i18n/registry.ts create mode 100644 packages/core/src/core/i18n/resolve-translation.ts create mode 100644 packages/core/src/core/i18n/tests/browser-translation.test.ts create mode 100644 packages/core/src/core/i18n/tests/load-locale.test.ts create mode 100644 packages/core/src/core/i18n/tests/registry.test.ts create mode 100644 packages/core/src/core/i18n/tests/resolve-translation.test.ts create mode 100644 packages/core/src/core/i18n/tests/translator.test.ts create mode 100644 packages/core/src/core/i18n/translator.ts create mode 100644 packages/core/src/core/i18n/types.ts create mode 100644 packages/core/src/core/ui/error-dialog/error-dialog-i18n.ts create mode 100644 packages/core/src/core/ui/error-dialog/tests/error-dialog-i18n.test.ts create mode 100644 packages/core/src/core/ui/input-feedback/labels.ts create mode 100644 packages/core/src/core/ui/input-feedback/tests/labels.test.ts create mode 100644 packages/core/src/core/ui/utils/resolve-label.ts create mode 100644 packages/core/src/core/ui/utils/tests/resolve-label.test.ts create mode 100644 packages/html/scripts/build-cdn-locales.ts create mode 100644 packages/html/src/cdn/i18n-registry.ts create mode 100644 packages/html/src/cdn/i18n.ts create mode 100644 packages/html/src/cdn/locales/.gitignore create mode 100644 packages/html/src/define/i18n.ts create mode 100644 packages/html/src/i18n/context.ts create mode 100644 packages/html/src/i18n/controller.ts create mode 100644 packages/html/src/i18n/create-i18n.ts create mode 100644 packages/html/src/i18n/index.ts create mode 100644 packages/html/src/i18n/locale.ts create mode 100644 packages/html/src/i18n/locales/all.ts create mode 100644 packages/html/src/i18n/locales/all/register.ts create mode 100644 packages/html/src/i18n/locales/ar.ts create mode 100644 packages/html/src/i18n/locales/ar/register.ts create mode 100644 packages/html/src/i18n/locales/az.ts create mode 100644 packages/html/src/i18n/locales/az/register.ts create mode 100644 packages/html/src/i18n/locales/bg.ts create mode 100644 packages/html/src/i18n/locales/bg/register.ts create mode 100644 packages/html/src/i18n/locales/bn.ts create mode 100644 packages/html/src/i18n/locales/bn/register.ts create mode 100644 packages/html/src/i18n/locales/bs.ts create mode 100644 packages/html/src/i18n/locales/bs/register.ts create mode 100644 packages/html/src/i18n/locales/ca.ts create mode 100644 packages/html/src/i18n/locales/ca/register.ts create mode 100644 packages/html/src/i18n/locales/cs.ts create mode 100644 packages/html/src/i18n/locales/cs/register.ts create mode 100644 packages/html/src/i18n/locales/cy.ts create mode 100644 packages/html/src/i18n/locales/cy/register.ts create mode 100644 packages/html/src/i18n/locales/da.ts create mode 100644 packages/html/src/i18n/locales/da/register.ts create mode 100644 packages/html/src/i18n/locales/de.ts create mode 100644 packages/html/src/i18n/locales/de/register.ts create mode 100644 packages/html/src/i18n/locales/el.ts create mode 100644 packages/html/src/i18n/locales/el/register.ts create mode 100644 packages/html/src/i18n/locales/en.ts create mode 100644 packages/html/src/i18n/locales/en/register.ts create mode 100644 packages/html/src/i18n/locales/es.ts create mode 100644 packages/html/src/i18n/locales/es/register.ts create mode 100644 packages/html/src/i18n/locales/et.ts create mode 100644 packages/html/src/i18n/locales/et/register.ts create mode 100644 packages/html/src/i18n/locales/eu.ts create mode 100644 packages/html/src/i18n/locales/eu/register.ts create mode 100644 packages/html/src/i18n/locales/fa.ts create mode 100644 packages/html/src/i18n/locales/fa/register.ts create mode 100644 packages/html/src/i18n/locales/fi.ts create mode 100644 packages/html/src/i18n/locales/fi/register.ts create mode 100644 packages/html/src/i18n/locales/fr.ts create mode 100644 packages/html/src/i18n/locales/fr/register.ts create mode 100644 packages/html/src/i18n/locales/gd.ts create mode 100644 packages/html/src/i18n/locales/gd/register.ts create mode 100644 packages/html/src/i18n/locales/gl.ts create mode 100644 packages/html/src/i18n/locales/gl/register.ts create mode 100644 packages/html/src/i18n/locales/he.ts create mode 100644 packages/html/src/i18n/locales/he/register.ts create mode 100644 packages/html/src/i18n/locales/hi.ts create mode 100644 packages/html/src/i18n/locales/hi/register.ts create mode 100644 packages/html/src/i18n/locales/hr.ts create mode 100644 packages/html/src/i18n/locales/hr/register.ts create mode 100644 packages/html/src/i18n/locales/hu.ts create mode 100644 packages/html/src/i18n/locales/hu/register.ts create mode 100644 packages/html/src/i18n/locales/it.ts create mode 100644 packages/html/src/i18n/locales/it/register.ts create mode 100644 packages/html/src/i18n/locales/ja.ts create mode 100644 packages/html/src/i18n/locales/ja/register.ts create mode 100644 packages/html/src/i18n/locales/ko.ts create mode 100644 packages/html/src/i18n/locales/ko/register.ts create mode 100644 packages/html/src/i18n/locales/lv.ts create mode 100644 packages/html/src/i18n/locales/lv/register.ts create mode 100644 packages/html/src/i18n/locales/mr.ts create mode 100644 packages/html/src/i18n/locales/mr/register.ts create mode 100644 packages/html/src/i18n/locales/nb.ts create mode 100644 packages/html/src/i18n/locales/nb/register.ts create mode 100644 packages/html/src/i18n/locales/ne.ts create mode 100644 packages/html/src/i18n/locales/ne/register.ts create mode 100644 packages/html/src/i18n/locales/nl.ts create mode 100644 packages/html/src/i18n/locales/nl/register.ts create mode 100644 packages/html/src/i18n/locales/nn.ts create mode 100644 packages/html/src/i18n/locales/nn/register.ts create mode 100644 packages/html/src/i18n/locales/oc.ts create mode 100644 packages/html/src/i18n/locales/oc/register.ts create mode 100644 packages/html/src/i18n/locales/pl.ts create mode 100644 packages/html/src/i18n/locales/pl/register.ts create mode 100644 packages/html/src/i18n/locales/pt-BR.ts create mode 100644 packages/html/src/i18n/locales/pt-BR/register.ts create mode 100644 packages/html/src/i18n/locales/pt-PT.ts create mode 100644 packages/html/src/i18n/locales/pt-PT/register.ts create mode 100644 packages/html/src/i18n/locales/pt.ts create mode 100644 packages/html/src/i18n/locales/pt/register.ts create mode 100644 packages/html/src/i18n/locales/ro.ts create mode 100644 packages/html/src/i18n/locales/ro/register.ts create mode 100644 packages/html/src/i18n/locales/ru.ts create mode 100644 packages/html/src/i18n/locales/ru/register.ts create mode 100644 packages/html/src/i18n/locales/sk.ts create mode 100644 packages/html/src/i18n/locales/sk/register.ts create mode 100644 packages/html/src/i18n/locales/sl.ts create mode 100644 packages/html/src/i18n/locales/sl/register.ts create mode 100644 packages/html/src/i18n/locales/sr.ts create mode 100644 packages/html/src/i18n/locales/sr/register.ts create mode 100644 packages/html/src/i18n/locales/sv.ts create mode 100644 packages/html/src/i18n/locales/sv/register.ts create mode 100644 packages/html/src/i18n/locales/te.ts create mode 100644 packages/html/src/i18n/locales/te/register.ts create mode 100644 packages/html/src/i18n/locales/th.ts create mode 100644 packages/html/src/i18n/locales/th/register.ts create mode 100644 packages/html/src/i18n/locales/tr.ts create mode 100644 packages/html/src/i18n/locales/tr/register.ts create mode 100644 packages/html/src/i18n/locales/uk.ts create mode 100644 packages/html/src/i18n/locales/uk/register.ts create mode 100644 packages/html/src/i18n/locales/vi.ts create mode 100644 packages/html/src/i18n/locales/vi/register.ts create mode 100644 packages/html/src/i18n/locales/zh-CN.ts create mode 100644 packages/html/src/i18n/locales/zh-CN/register.ts create mode 100644 packages/html/src/i18n/locales/zh-TW.ts create mode 100644 packages/html/src/i18n/locales/zh-TW/register.ts create mode 100644 packages/html/src/i18n/locales/zh.ts create mode 100644 packages/html/src/i18n/locales/zh/register.ts create mode 100644 packages/html/src/i18n/provider-element.ts create mode 100644 packages/html/src/i18n/provider-mixin.ts create mode 100644 packages/html/src/i18n/tests/create-i18n.test.ts create mode 100644 packages/html/src/i18n/tests/provider.test.ts create mode 100644 packages/html/src/i18n/text-mixin.ts create mode 100644 packages/html/src/i18n/types.ts create mode 100644 packages/html/src/ui/captions-radio-group/tests/captions-radio-group-element.test.ts create mode 100644 packages/html/src/ui/radio-group/context.ts create mode 100644 packages/html/src/ui/radio-group/radio-group-element.ts create mode 100644 packages/html/src/ui/text/text-element.ts create mode 100644 packages/react/src/i18n/context.tsx create mode 100644 packages/react/src/i18n/create-i18n.tsx create mode 100644 packages/react/src/i18n/get-provider-root-props.ts create mode 100644 packages/react/src/i18n/index.ts create mode 100644 packages/react/src/i18n/locales/all.ts create mode 100644 packages/react/src/i18n/locales/all/register.ts create mode 100644 packages/react/src/i18n/locales/ar.ts create mode 100644 packages/react/src/i18n/locales/ar/register.ts create mode 100644 packages/react/src/i18n/locales/az.ts create mode 100644 packages/react/src/i18n/locales/az/register.ts create mode 100644 packages/react/src/i18n/locales/bg.ts create mode 100644 packages/react/src/i18n/locales/bg/register.ts create mode 100644 packages/react/src/i18n/locales/bn.ts create mode 100644 packages/react/src/i18n/locales/bn/register.ts create mode 100644 packages/react/src/i18n/locales/bs.ts create mode 100644 packages/react/src/i18n/locales/bs/register.ts create mode 100644 packages/react/src/i18n/locales/ca.ts create mode 100644 packages/react/src/i18n/locales/ca/register.ts create mode 100644 packages/react/src/i18n/locales/cs.ts create mode 100644 packages/react/src/i18n/locales/cs/register.ts create mode 100644 packages/react/src/i18n/locales/cy.ts create mode 100644 packages/react/src/i18n/locales/cy/register.ts create mode 100644 packages/react/src/i18n/locales/da.ts create mode 100644 packages/react/src/i18n/locales/da/register.ts create mode 100644 packages/react/src/i18n/locales/de.ts create mode 100644 packages/react/src/i18n/locales/de/register.ts create mode 100644 packages/react/src/i18n/locales/el.ts create mode 100644 packages/react/src/i18n/locales/el/register.ts create mode 100644 packages/react/src/i18n/locales/en.ts create mode 100644 packages/react/src/i18n/locales/en/register.ts create mode 100644 packages/react/src/i18n/locales/es.ts create mode 100644 packages/react/src/i18n/locales/es/register.ts create mode 100644 packages/react/src/i18n/locales/et.ts create mode 100644 packages/react/src/i18n/locales/et/register.ts create mode 100644 packages/react/src/i18n/locales/eu.ts create mode 100644 packages/react/src/i18n/locales/eu/register.ts create mode 100644 packages/react/src/i18n/locales/fa.ts create mode 100644 packages/react/src/i18n/locales/fa/register.ts create mode 100644 packages/react/src/i18n/locales/fi.ts create mode 100644 packages/react/src/i18n/locales/fi/register.ts create mode 100644 packages/react/src/i18n/locales/fr.ts create mode 100644 packages/react/src/i18n/locales/fr/register.ts create mode 100644 packages/react/src/i18n/locales/gd.ts create mode 100644 packages/react/src/i18n/locales/gd/register.ts create mode 100644 packages/react/src/i18n/locales/gl.ts create mode 100644 packages/react/src/i18n/locales/gl/register.ts create mode 100644 packages/react/src/i18n/locales/he.ts create mode 100644 packages/react/src/i18n/locales/he/register.ts create mode 100644 packages/react/src/i18n/locales/hi.ts create mode 100644 packages/react/src/i18n/locales/hi/register.ts create mode 100644 packages/react/src/i18n/locales/hr.ts create mode 100644 packages/react/src/i18n/locales/hr/register.ts create mode 100644 packages/react/src/i18n/locales/hu.ts create mode 100644 packages/react/src/i18n/locales/hu/register.ts create mode 100644 packages/react/src/i18n/locales/it.ts create mode 100644 packages/react/src/i18n/locales/it/register.ts create mode 100644 packages/react/src/i18n/locales/ja.ts create mode 100644 packages/react/src/i18n/locales/ja/register.ts create mode 100644 packages/react/src/i18n/locales/ko.ts create mode 100644 packages/react/src/i18n/locales/ko/register.ts create mode 100644 packages/react/src/i18n/locales/lv.ts create mode 100644 packages/react/src/i18n/locales/lv/register.ts create mode 100644 packages/react/src/i18n/locales/mr.ts create mode 100644 packages/react/src/i18n/locales/mr/register.ts create mode 100644 packages/react/src/i18n/locales/nb.ts create mode 100644 packages/react/src/i18n/locales/nb/register.ts create mode 100644 packages/react/src/i18n/locales/ne.ts create mode 100644 packages/react/src/i18n/locales/ne/register.ts create mode 100644 packages/react/src/i18n/locales/nl.ts create mode 100644 packages/react/src/i18n/locales/nl/register.ts create mode 100644 packages/react/src/i18n/locales/nn.ts create mode 100644 packages/react/src/i18n/locales/nn/register.ts create mode 100644 packages/react/src/i18n/locales/oc.ts create mode 100644 packages/react/src/i18n/locales/oc/register.ts create mode 100644 packages/react/src/i18n/locales/pl.ts create mode 100644 packages/react/src/i18n/locales/pl/register.ts create mode 100644 packages/react/src/i18n/locales/pt-BR.ts create mode 100644 packages/react/src/i18n/locales/pt-BR/register.ts create mode 100644 packages/react/src/i18n/locales/pt-PT.ts create mode 100644 packages/react/src/i18n/locales/pt-PT/register.ts create mode 100644 packages/react/src/i18n/locales/pt.ts create mode 100644 packages/react/src/i18n/locales/pt/register.ts create mode 100644 packages/react/src/i18n/locales/ro.ts create mode 100644 packages/react/src/i18n/locales/ro/register.ts create mode 100644 packages/react/src/i18n/locales/ru.ts create mode 100644 packages/react/src/i18n/locales/ru/register.ts create mode 100644 packages/react/src/i18n/locales/sk.ts create mode 100644 packages/react/src/i18n/locales/sk/register.ts create mode 100644 packages/react/src/i18n/locales/sl.ts create mode 100644 packages/react/src/i18n/locales/sl/register.ts create mode 100644 packages/react/src/i18n/locales/sr.ts create mode 100644 packages/react/src/i18n/locales/sr/register.ts create mode 100644 packages/react/src/i18n/locales/sv.ts create mode 100644 packages/react/src/i18n/locales/sv/register.ts create mode 100644 packages/react/src/i18n/locales/te.ts create mode 100644 packages/react/src/i18n/locales/te/register.ts create mode 100644 packages/react/src/i18n/locales/th.ts create mode 100644 packages/react/src/i18n/locales/th/register.ts create mode 100644 packages/react/src/i18n/locales/tr.ts create mode 100644 packages/react/src/i18n/locales/tr/register.ts create mode 100644 packages/react/src/i18n/locales/uk.ts create mode 100644 packages/react/src/i18n/locales/uk/register.ts create mode 100644 packages/react/src/i18n/locales/vi.ts create mode 100644 packages/react/src/i18n/locales/vi/register.ts create mode 100644 packages/react/src/i18n/locales/zh-CN.ts create mode 100644 packages/react/src/i18n/locales/zh-CN/register.ts create mode 100644 packages/react/src/i18n/locales/zh-TW.ts create mode 100644 packages/react/src/i18n/locales/zh-TW/register.ts create mode 100644 packages/react/src/i18n/locales/zh.ts create mode 100644 packages/react/src/i18n/locales/zh/register.ts create mode 100644 packages/react/src/i18n/tests/create-i18n.test.tsx create mode 100644 packages/react/src/i18n/use-ambient-lang.ts create mode 100644 packages/react/src/i18n/use-lang-root-element.ts create mode 100644 packages/react/src/i18n/use-lazy-translations.ts create mode 100644 packages/react/src/i18n/use-locale-root-notifications.ts create mode 100644 packages/react/src/i18n/use-merged-translations.ts create mode 100644 packages/react/src/i18n/use-registry-epoch.ts create mode 100644 packages/react/src/player/container.tsx create mode 100644 packages/react/src/ui/error-dialog/error-dialog-close.tsx create mode 100644 packages/react/src/ui/error-dialog/error-dialog-title.tsx create mode 100644 packages/react/src/ui/error-dialog/tests/error-dialog.test.tsx create mode 100644 packages/react/src/ui/play-button/tests/play-button.test.tsx create mode 100644 packages/utils/src/dom/locale/effective-locale.ts create mode 100644 packages/utils/src/dom/locale/find-nearest-lang.ts create mode 100644 packages/utils/src/dom/locale/merge-locale-overlays.ts create mode 100644 packages/utils/src/dom/locale/resolve-lang-attr.ts create mode 100644 packages/utils/src/dom/locale/subscribe-ambient-lang.ts create mode 100644 packages/utils/src/dom/tests/locale-effective-locale.test.ts create mode 100644 packages/utils/src/dom/tests/locale-find-nearest-lang.test.ts create mode 100644 packages/utils/src/dom/tests/locale-merge-locale-overlays.test.ts create mode 100644 packages/utils/src/dom/tests/locale-resolve-lang-attr.test.ts create mode 100644 packages/utils/src/dom/tests/locale-subscribe-ambient-lang.test.ts create mode 100644 packages/utils/src/dom/tests/walk-ancestors.test.ts create mode 100644 packages/utils/src/dom/walk-ancestors.ts create mode 100644 packages/utils/src/percent/index.ts create mode 100644 packages/utils/src/percent/percent.ts create mode 100644 packages/utils/src/percent/tests/percent.test.ts create mode 100644 site/src/utils/tests/utilReferenceSlug.test.ts create mode 100644 site/src/utils/utilReferenceSlug.ts diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 10e46598..a8491bc6 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -64,6 +64,10 @@ jobs: - name: Run E2E tests run: pnpm --dir apps/e2e exec playwright test --project=vite-${{ matrix.browser }} + - name: Run sandbox E2E tests + if: matrix.browser == 'chromium' + run: pnpm --dir apps/e2e test:sandbox + - name: Upload test report if: always() uses: actions/upload-artifact@v4 diff --git a/apps/e2e/package.json b/apps/e2e/package.json index 191158e5..ae897160 100644 --- a/apps/e2e/package.json +++ b/apps/e2e/package.json @@ -17,6 +17,7 @@ "test:chromium": "playwright test --project=vite-chromium", "test:webkit": "playwright test --project=vite-webkit", "test:vite": "playwright test --project=vite-chromium", + "test:sandbox": "playwright test --project=sandbox-chromium", "test:all": "playwright test --project=vite-chromium --project=vite-webkit", "report": "playwright show-report" }, diff --git a/apps/e2e/playwright.config.ts b/apps/e2e/playwright.config.ts index 2342a9bb..a90aa81e 100644 --- a/apps/e2e/playwright.config.ts +++ b/apps/e2e/playwright.config.ts @@ -1,6 +1,52 @@ import { defineConfig, devices } from '@playwright/test'; const CI = !!process.env.CI; +const SANDBOX_SPEC = /sandbox-.*i18n\.spec\.ts/; +const VALUE_OPTIONS = new Set([ + '-c', + '-g', + '-j', + '-p', + '-u', + '--browser', + '--config', + '--global-timeout', + '--grep', + '--grep-invert', + '--max-failures', + '--output', + '--project', + '--repeat-each', + '--reporter', + '--retries', + '--shard', + '--test-list', + '--test-list-invert', + '--timeout', + '--trace', + '--tsconfig', + '--update-snapshots', + '--workers', +]); + +const args = process.argv.slice(2).filter((arg, index) => !(index === 0 && arg === 'test')); +const selectedProjects = args.flatMap((arg, index) => { + if (arg === '--project' || arg === '-p') { + return args[index + 1] ? [args[index + 1]] : []; + } + return arg.startsWith('--project=') ? [arg.slice('--project='.length)] : []; +}); +const testFilters = args.filter((arg, index) => !arg.startsWith('-') && !VALUE_OPTIONS.has(args[index - 1] ?? '')); +const selectedViteProject = selectedProjects.some((name) => name.startsWith('vite-')); +const selectedSandboxProject = selectedProjects.some((name) => name.startsWith('sandbox-')); +const selectedSandboxSpec = testFilters.some((arg) => SANDBOX_SPEC.test(arg)); +const unfilteredRun = selectedProjects.length === 0 && testFilters.length === 0; +const shouldStartViteServer = + selectedViteProject || + unfilteredRun || + (selectedProjects.length === 0 && testFilters.some((arg) => !SANDBOX_SPEC.test(arg))); +const shouldStartSandboxServer = + !process.env.SANDBOX_URL && (selectedSandboxProject || selectedSandboxSpec || unfilteredRun); export default defineConfig({ testDir: './tests', @@ -34,21 +80,30 @@ export default defineConfig({ // --- Chromium --- { name: 'vite-chromium', + testIgnore: SANDBOX_SPEC, use: { ...devices['Desktop Chrome'], baseURL: 'http://localhost:5180' }, }, // --- WebKit --- { name: 'vite-webkit', + testIgnore: SANDBOX_SPEC, use: { ...devices['Desktop Safari'], baseURL: 'http://localhost:5180' }, }, // --- Firefox --- { name: 'vite-firefox', + testIgnore: SANDBOX_SPEC, use: { ...devices['Desktop Firefox'], baseURL: 'http://localhost:5180' }, }, + { + name: 'sandbox-chromium', + testMatch: SANDBOX_SPEC, + use: { ...devices['Desktop Chrome'] }, + }, + // --- Future: Next.js --- // { // name: 'next-chromium', @@ -63,13 +118,29 @@ export default defineConfig({ ], webServer: [ - { - command: 'npx vite --port 5180', - cwd: './apps/vite', - port: 5180, - reuseExistingServer: !CI, - timeout: 120_000, - }, + ...(shouldStartViteServer + ? [ + { + command: 'npx vite --port 5180', + cwd: './apps/vite', + port: 5180, + reuseExistingServer: !CI, + timeout: 120_000, + }, + ] + : []), + ...(shouldStartSandboxServer + ? [ + { + command: + 'pnpm --dir ../.. build:cdn && node_modules/.bin/tsx scripts/setup.ts && node_modules/.bin/vite --host --port 5299', + cwd: '../sandbox', + port: 5299, + reuseExistingServer: !CI, + timeout: 300_000, + }, + ] + : []), // --- Future: Next.js --- // { diff --git a/apps/e2e/tests/sandbox-cdn-i18n.spec.ts b/apps/e2e/tests/sandbox-cdn-i18n.spec.ts new file mode 100644 index 00000000..feddc834 --- /dev/null +++ b/apps/e2e/tests/sandbox-cdn-i18n.spec.ts @@ -0,0 +1,47 @@ +import { expect, type Frame, type Page, test } from '@playwright/test'; +import { SELECTORS } from '../fixtures/selectors'; +import { PlayerPage } from '../page-objects/player'; + +const SANDBOX_BASE = process.env.SANDBOX_URL ?? 'http://localhost:5299'; + +test.use({ trace: 'off' }); + +async function getPreviewFrame(page: Page, path: string): Promise { + await expect(page.locator('iframe[title="player demo"]')).toHaveAttribute('src', new RegExp(`^${path}`)); + await expect + .poll(() => + page + .frames() + .find((frame) => frame.url().includes(path)) + ?.url() + ) + .toContain(path); + + const frame = page.frames().find((frame) => frame.url().includes(path)); + if (!frame) throw new Error(`Preview frame not found: ${path}`); + + return frame; +} + +test.describe('Sandbox CDN i18n', () => { + test('direct CDN page shows Spanish play label', async ({ page }) => { + await page.goto( + `${SANDBOX_BASE}/cdn/?preset=video&locale=es&styling=css&skin=default&source=hls-1&autoplay=0&muted=0&loop=0&preload=metadata`, + { waitUntil: 'domcontentloaded' } + ); + const player = new PlayerPage(page); + await expect(player.playButton).toHaveAttribute('aria-label', 'Reproducir', { timeout: 15_000 }); + }); + + test('shell iframe shows Spanish play label', async ({ page }) => { + await page.goto( + `${SANDBOX_BASE}/?platform=cdn&preset=video&locale=es&styling=css&skin=default&source=hls-1&autoplay=0&muted=0&loop=0&preload=metadata`, + { waitUntil: 'domcontentloaded' } + ); + const frame = await getPreviewFrame(page, '/cdn/'); + const playButton = frame.locator(SELECTORS.playButton).first(); + await expect(playButton).toHaveAttribute('aria-label', 'Reproducir', { + timeout: 15_000, + }); + }); +}); diff --git a/apps/e2e/tests/sandbox-html-i18n.spec.ts b/apps/e2e/tests/sandbox-html-i18n.spec.ts new file mode 100644 index 00000000..d76a7d8a --- /dev/null +++ b/apps/e2e/tests/sandbox-html-i18n.spec.ts @@ -0,0 +1,60 @@ +import { expect, type Frame, type Page, test } from '@playwright/test'; +import { SELECTORS } from '../fixtures/selectors'; + +const SANDBOX_BASE = process.env.SANDBOX_URL ?? 'http://localhost:5299'; + +const QUERY = 'locale=es&styling=css&skin=default&source=hls-1&autoplay=0&muted=0&loop=0&preload=metadata'; + +test.use({ trace: 'off' }); + +async function expectSpanishPlayLabel(scope: Page | Frame): Promise { + const playButton = scope.locator(SELECTORS.playButton).first(); + await expect(playButton).toHaveAttribute('aria-label', 'Reproducir', { timeout: 15_000 }); +} + +async function getPreviewFrame(page: Page, path: string): Promise { + await expect(page.locator('iframe[title="player demo"]')).toHaveAttribute('src', new RegExp(`^${path}`)); + await expect + .poll(() => + page + .frames() + .find((frame) => frame.url().includes(path)) + ?.url() + ) + .toContain(path); + + const frame = page.frames().find((frame) => frame.url().includes(path)); + if (!frame) throw new Error(`Preview frame not found: ${path}`); + + return frame; +} + +test.describe('Sandbox HTML i18n', () => { + test('direct HTML page shows Spanish play label', async ({ page }) => { + await page.goto(`${SANDBOX_BASE}/html-video/?${QUERY}`, { waitUntil: 'domcontentloaded' }); + await expectSpanishPlayLabel(page); + }); + + test('shell iframe shows Spanish play label', async ({ page }) => { + await page.goto(`${SANDBOX_BASE}/?platform=html&preset=video&${QUERY}`, { + waitUntil: 'domcontentloaded', + }); + const frame = await getPreviewFrame(page, '/html-video/'); + await expectSpanishPlayLabel(frame); + }); +}); + +test.describe('Sandbox React i18n', () => { + test('direct React page shows Spanish play label', async ({ page }) => { + await page.goto(`${SANDBOX_BASE}/react-video/?${QUERY}`, { waitUntil: 'domcontentloaded' }); + await expectSpanishPlayLabel(page); + }); + + test('shell iframe shows Spanish play label', async ({ page }) => { + await page.goto(`${SANDBOX_BASE}/?platform=react&preset=video&${QUERY}`, { + waitUntil: 'domcontentloaded', + }); + const frame = await getPreviewFrame(page, '/react-video/'); + await expectSpanishPlayLabel(frame); + }); +}); diff --git a/apps/sandbox/README.md b/apps/sandbox/README.md index cf755108..e7935551 100644 --- a/apps/sandbox/README.md +++ b/apps/sandbox/README.md @@ -12,6 +12,8 @@ pnpm dev # also runs the docs site Open the printed URL. The root route renders an interactive shell — a navbar with dropdowns for platform (HTML, React, CDN), preset (`video`, `hlsjs-video`, `audio`, etc.), skin, styling (CSS or Tailwind), and source — that previews the selected combination in an iframe. Use the **Open** button to pop the preview out into its own tab. +**Language** is in **Player settings** (gear icon) for every preset (HTML, React, and CDN). **CDN** registers copy through `@videojs/html/cdn/i18n` (the same registry as the CDN player bundle), not source `@videojs/html/i18n`. After pulling template changes, restart `pnpm dev:sandbox` so `scripts/setup.ts` refreshes `src/` from `templates/`. + The shell covers the main combinatorial matrix. One-off templates not in that matrix (e.g. `firefox-mse-repro`, `spf-segment-loading`, `simple-hls-html`) are reachable by navigating directly to `//`. See `apps/sandbox/templates/` for the full list. ## How it works @@ -22,7 +24,7 @@ Three directories participate: - **`templates/`** — The source of truth for each sandbox. One subdirectory per entry point, each containing its own `index.html` and `main.ts` / `main.tsx`. Checked into git. - **`src/`** — Your working copy where you freely edit, experiment, and break things. Fully gitignored (`src/*`). -On `pnpm dev:sandbox`, `scripts/setup.ts` mirrors every file from `templates/` into `src/` that doesn't already exist there. Existing files in `src/` are never overwritten, so your local changes persist across restarts. +On `pnpm dev:sandbox`, `scripts/setup.ts` mirrors new files from `templates/` into `src/` and overwrites `src/` files that differ from `templates/`. Edits made only in `src/` are replaced on the next dev start — use `sync` to copy them into `templates/` first. Vite discovers sandbox entries by scanning `src/*` for subdirectories that contain an `index.html` — no manual registration is needed. diff --git a/apps/sandbox/app/shared/html/i18n.ts b/apps/sandbox/app/shared/html/i18n.ts new file mode 100644 index 00000000..d46d1dfc --- /dev/null +++ b/apps/sandbox/app/shared/html/i18n.ts @@ -0,0 +1,42 @@ +import '@videojs/html/i18n'; + +import { ensureSandboxLocale, type SandboxLocaleTag } from '../i18n/sandbox-locales'; +import { getInitialLocale, onLocaleChange } from '../sandbox-listener'; + +let locale: SandboxLocaleTag = getInitialLocale(); +let localeApplySeq = 0; + +document.documentElement.lang = locale; + +export function wrapSandboxHtmlI18n(content: string): string { + return `${content}`; +} + +export async function prepareSandboxHtmlLocale(): Promise { + await ensureSandboxLocale(locale); +} + +export async function applySandboxHtmlLocale(next: SandboxLocaleTag): Promise { + const seq = ++localeApplySeq; + await ensureSandboxLocale(next); + if (seq !== localeApplySeq) return; + locale = next; + document.documentElement.lang = locale; +} + +export function bindSandboxHtmlLocaleChange(rerender: () => void): void { + onLocaleChange((next) => { + if (document.querySelector('media-i18n')) { + void applySandboxHtmlLocale(next); + return; + } + const seq = ++localeApplySeq; + void (async () => { + await ensureSandboxLocale(next); + if (seq !== localeApplySeq) return; + locale = next; + document.documentElement.lang = locale; + rerender(); + })(); + }); +} diff --git a/apps/sandbox/app/shared/i18n/browser-sandbox-prefetch.ts b/apps/sandbox/app/shared/i18n/browser-sandbox-prefetch.ts new file mode 100644 index 00000000..01b2e40f --- /dev/null +++ b/apps/sandbox/app/shared/i18n/browser-sandbox-prefetch.ts @@ -0,0 +1,37 @@ +import type { Locale, Translations } from '@videojs/core/i18n'; +import { getBrowserTranslations } from '@videojs/core/i18n'; + +import { type SandboxBrowserLocaleTag, sandboxLocaleLabel } from './locale-meta'; + +type RegisterI18n = (locale: Locale, translations: Partial) => void; + +/** Dedupes in-flight browser model download + translation per sandbox browser-only tag. */ +export function createBrowserSandboxPrefetch(register: RegisterI18n) { + const inflight = new Map>(); + + return async function prefetchBrowserSandboxLocale(tag: SandboxBrowserLocaleTag): Promise { + const pending = inflight.get(tag); + if (pending) return pending; + + const task = (async () => { + const label = sandboxLocaleLabel(tag); + const browser = await getBrowserTranslations(tag, { + downloadIfNeeded: true, + onModelDownload: { + start: () => { + console.info(`[videojs/sandbox] Downloading browser translation model for ${label} (${tag})…`); + }, + finish: () => { + console.info(`[videojs/sandbox] Browser translation model ready for ${label} (${tag})`); + }, + }, + }); + if (Object.keys(browser).length) register(tag, browser); + })().finally(() => { + inflight.delete(tag); + }); + + inflight.set(tag, task); + return task; + }; +} diff --git a/apps/sandbox/app/shared/i18n/cdn-locale-loaders.generated.ts b/apps/sandbox/app/shared/i18n/cdn-locale-loaders.generated.ts new file mode 100644 index 00000000..88919c53 --- /dev/null +++ b/apps/sandbox/app/shared/i18n/cdn-locale-loaders.generated.ts @@ -0,0 +1,57 @@ +/** Generated by scripts/generate-cdn-locale-loaders.ts — do not edit. */ +import type { SandboxLocalePack } from './locale-meta'; + +/** CDN locale chunks register via `../i18n.dev.js` — same module graph as `@videojs/html/cdn/video`. */ +export const cdnLocaleLoaders = { + ar: () => import('@videojs/html/cdn/locales/ar'), + az: () => import('@videojs/html/cdn/locales/az'), + bg: () => import('@videojs/html/cdn/locales/bg'), + bn: () => import('@videojs/html/cdn/locales/bn'), + bs: () => import('@videojs/html/cdn/locales/bs'), + ca: () => import('@videojs/html/cdn/locales/ca'), + cs: () => import('@videojs/html/cdn/locales/cs'), + cy: () => import('@videojs/html/cdn/locales/cy'), + da: () => import('@videojs/html/cdn/locales/da'), + de: () => import('@videojs/html/cdn/locales/de'), + el: () => import('@videojs/html/cdn/locales/el'), + es: () => import('@videojs/html/cdn/locales/es'), + et: () => import('@videojs/html/cdn/locales/et'), + eu: () => import('@videojs/html/cdn/locales/eu'), + fa: () => import('@videojs/html/cdn/locales/fa'), + fi: () => import('@videojs/html/cdn/locales/fi'), + fr: () => import('@videojs/html/cdn/locales/fr'), + gd: () => import('@videojs/html/cdn/locales/gd'), + gl: () => import('@videojs/html/cdn/locales/gl'), + he: () => import('@videojs/html/cdn/locales/he'), + hi: () => import('@videojs/html/cdn/locales/hi'), + hr: () => import('@videojs/html/cdn/locales/hr'), + hu: () => import('@videojs/html/cdn/locales/hu'), + it: () => import('@videojs/html/cdn/locales/it'), + ja: () => import('@videojs/html/cdn/locales/ja'), + ko: () => import('@videojs/html/cdn/locales/ko'), + lv: () => import('@videojs/html/cdn/locales/lv'), + mr: () => import('@videojs/html/cdn/locales/mr'), + nb: () => import('@videojs/html/cdn/locales/nb'), + ne: () => import('@videojs/html/cdn/locales/ne'), + nl: () => import('@videojs/html/cdn/locales/nl'), + nn: () => import('@videojs/html/cdn/locales/nn'), + oc: () => import('@videojs/html/cdn/locales/oc'), + pl: () => import('@videojs/html/cdn/locales/pl'), + pt: () => import('@videojs/html/cdn/locales/pt'), + 'pt-BR': () => import('@videojs/html/cdn/locales/pt-BR'), + 'pt-PT': () => import('@videojs/html/cdn/locales/pt-PT'), + ro: () => import('@videojs/html/cdn/locales/ro'), + ru: () => import('@videojs/html/cdn/locales/ru'), + sk: () => import('@videojs/html/cdn/locales/sk'), + sl: () => import('@videojs/html/cdn/locales/sl'), + sr: () => import('@videojs/html/cdn/locales/sr'), + sv: () => import('@videojs/html/cdn/locales/sv'), + te: () => import('@videojs/html/cdn/locales/te'), + th: () => import('@videojs/html/cdn/locales/th'), + tr: () => import('@videojs/html/cdn/locales/tr'), + uk: () => import('@videojs/html/cdn/locales/uk'), + vi: () => import('@videojs/html/cdn/locales/vi'), + zh: () => import('@videojs/html/cdn/locales/zh'), + 'zh-CN': () => import('@videojs/html/cdn/locales/zh-CN'), + 'zh-TW': () => import('@videojs/html/cdn/locales/zh-TW'), +} as const satisfies Record, () => Promise>; diff --git a/apps/sandbox/app/shared/i18n/cdn-sandbox-locales.ts b/apps/sandbox/app/shared/i18n/cdn-sandbox-locales.ts new file mode 100644 index 00000000..02049109 --- /dev/null +++ b/apps/sandbox/app/shared/i18n/cdn-sandbox-locales.ts @@ -0,0 +1,48 @@ +import { getI18nTranslations, registerI18n } from '@videojs/html/cdn/i18n'; + +import { createBrowserSandboxPrefetch } from './browser-sandbox-prefetch'; +import { cdnLocaleLoaders } from './cdn-locale-loaders.generated'; +import { isSandboxBrowserLocale, type SandboxLocaleTag } from './locale-meta'; + +const prefetchBrowserSandboxLocale = createBrowserSandboxPrefetch(registerI18n); + +let active: SandboxLocaleTag | null = null; + +/** Loads a CDN locale chunk or prefetches browser translations before the player renders. */ +export async function ensureCdnSandboxLocale(tag: SandboxLocaleTag): Promise { + if (tag === 'en') { + active = tag; + return; + } + + if (isSandboxBrowserLocale(tag)) { + if (active !== tag) { + await prefetchBrowserSandboxLocale(tag); + } + active = tag; + return; + } + + if (active === tag) { + return; + } + + const load = cdnLocaleLoaders[tag as keyof typeof cdnLocaleLoaders]; + if (!load) { + throw new Error(`Unknown sandbox locale: ${tag}`); + } + + await load(); + + if (import.meta.env.DEV) { + const registered = getI18nTranslations(tag).Play; + const enPlay = getI18nTranslations('en').Play; + if (registered === enPlay) { + throw new Error( + `[videojs/sandbox] Locale "${tag}" did not register on the CDN i18n registry (still "${enPlay}").` + ); + } + } + + active = tag; +} diff --git a/apps/sandbox/app/shared/i18n/locale-meta.ts b/apps/sandbox/app/shared/i18n/locale-meta.ts new file mode 100644 index 00000000..cd3a51b8 --- /dev/null +++ b/apps/sandbox/app/shared/i18n/locale-meta.ts @@ -0,0 +1,114 @@ +import { LOCALES, localeAliases } from '@videojs/core/i18n'; + +/** Locale packs available through Video.js (picker + CDN loaders). */ +export const SANDBOX_LOCALE_PACKS = ['en', ...LOCALES, ...localeAliases(LOCALES)] as const; + +export type SandboxLocalePack = (typeof SANDBOX_LOCALE_PACKS)[number]; + +/** + * Chrome Translator API tags (desktop). + * @see https://developer.chrome.com/docs/ai/translator-api#supported_languages + */ +const CHROME_TRANSLATOR_LOCALE_TAGS = [ + 'ar', + 'bg', + 'bn', + 'cs', + 'da', + 'de', + 'el', + 'en', + 'es', + 'fi', + 'fr', + 'hi', + 'hr', + 'hu', + 'id', + 'it', + 'iw', + 'ja', + 'kn', + 'ko', + 'lt', + 'mr', + 'nl', + 'no', + 'pl', + 'pt', + 'ro', + 'ru', + 'sk', + 'sl', + 'sv', + 'ta', + 'te', + 'th', + 'tr', + 'uk', + 'vi', + 'zh', + 'zh-Hant', +] as const; + +const sandboxLocalePackSet = new Set(SANDBOX_LOCALE_PACKS); + +export type SandboxBrowserLocaleTag = Extract< + (typeof CHROME_TRANSLATOR_LOCALE_TAGS)[number], + Exclude<(typeof CHROME_TRANSLATOR_LOCALE_TAGS)[number], SandboxLocalePack | 'en'> +>; + +/** + * Locales with no Video.js pack — Chrome Translator only, excluding tags we already ship. + * Derived from {@link CHROME_TRANSLATOR_LOCALE_TAGS} so unsupported tags never appear in the picker. + */ +export const SANDBOX_BROWSER_LOCALE_TAGS: readonly SandboxBrowserLocaleTag[] = CHROME_TRANSLATOR_LOCALE_TAGS.filter( + (tag): tag is SandboxBrowserLocaleTag => tag !== 'en' && !sandboxLocalePackSet.has(tag) +); + +/** All tags exposed in the sandbox language picker. */ +export type SandboxLocaleTag = SandboxLocalePack | SandboxBrowserLocaleTag; + +export const SANDBOX_LOCALE_TAGS: readonly SandboxLocaleTag[] = [ + ...SANDBOX_LOCALE_PACKS, + ...SANDBOX_BROWSER_LOCALE_TAGS, +]; + +export const DEFAULT_SANDBOX_LOCALE: SandboxLocaleTag = 'en'; + +const LANGUAGE_NAMES = new Intl.DisplayNames('en', { type: 'language' }); + +export function sandboxLocaleLabel(tag: string): string { + return LANGUAGE_NAMES.of(tag) ?? tag; +} + +export function isSandboxBrowserLocale(tag: string): tag is SandboxBrowserLocaleTag { + return (SANDBOX_BROWSER_LOCALE_TAGS as readonly string[]).includes(tag); +} + +export type SandboxLocaleOption = { + value: SandboxLocaleTag; + label: string; +}; + +export type SandboxLocaleOptionGroup = { + label: string; + options: SandboxLocaleOption[]; +}; + +function localeOptions(tags: readonly SandboxLocaleTag[]): SandboxLocaleOption[] { + return [...tags] + .map((tag) => ({ + value: tag, + label: sandboxLocaleLabel(tag), + })) + .sort((a, b) => a.label.localeCompare(b.label, 'en')); +} + +/** Flat list (legacy); prefer {@link SANDBOX_LOCALE_OPTION_GROUPS} in the picker. */ +export const SANDBOX_LOCALE_OPTIONS = localeOptions(SANDBOX_LOCALE_TAGS); + +export const SANDBOX_LOCALE_OPTION_GROUPS: SandboxLocaleOptionGroup[] = [ + { label: 'Built-in packs', options: localeOptions(SANDBOX_LOCALE_PACKS) }, + { label: 'Browser API only', options: localeOptions(SANDBOX_BROWSER_LOCALE_TAGS) }, +]; diff --git a/apps/sandbox/app/shared/i18n/sandbox-locales.ts b/apps/sandbox/app/shared/i18n/sandbox-locales.ts new file mode 100644 index 00000000..679f293b --- /dev/null +++ b/apps/sandbox/app/shared/i18n/sandbox-locales.ts @@ -0,0 +1,55 @@ +import { registerI18n } from '@videojs/html/i18n'; +import { all } from '@videojs/html/i18n/locales/all'; + +import { createBrowserSandboxPrefetch } from './browser-sandbox-prefetch'; +import { + DEFAULT_SANDBOX_LOCALE, + isSandboxBrowserLocale, + SANDBOX_LOCALE_OPTION_GROUPS, + SANDBOX_LOCALE_OPTIONS, + SANDBOX_LOCALE_TAGS, + type SandboxLocaleTag, + sandboxLocaleLabel, +} from './locale-meta'; + +export { + DEFAULT_SANDBOX_LOCALE, + isSandboxBrowserLocale, + SANDBOX_LOCALE_OPTION_GROUPS, + SANDBOX_LOCALE_OPTIONS, + SANDBOX_LOCALE_TAGS, + sandboxLocaleLabel, +}; +export type { SandboxLocaleTag }; + +const prefetchBrowserSandboxLocale = createBrowserSandboxPrefetch(registerI18n); + +let activeLocale: SandboxLocaleTag | null = null; + +/** Registers built-in packs or prefetches browser translations before the player renders. */ +export async function ensureSandboxLocale(tag: SandboxLocaleTag): Promise { + if (tag === 'en') { + activeLocale = 'en'; + return; + } + + if (isSandboxBrowserLocale(tag)) { + if (activeLocale !== tag) { + await prefetchBrowserSandboxLocale(tag); + } + activeLocale = tag; + return; + } + + if (activeLocale === tag) { + return; + } + + const translations = all[tag as keyof typeof all]; + if (!translations) { + throw new Error(`Unknown sandbox locale: ${tag}`); + } + + registerI18n(tag, translations); + activeLocale = tag; +} diff --git a/apps/sandbox/app/shared/react/sandbox-i18n.tsx b/apps/sandbox/app/shared/react/sandbox-i18n.tsx new file mode 100644 index 00000000..5a0c0240 --- /dev/null +++ b/apps/sandbox/app/shared/react/sandbox-i18n.tsx @@ -0,0 +1,26 @@ +import { ensureSandboxLocale } from '@app/shared/i18n/sandbox-locales'; +import { I18nProvider } from '@videojs/react/i18n'; +import { type ReactNode, useEffect } from 'react'; + +import { useLocale } from './use-locale'; + +function syncHtmlLang(locale: string): void { + if (typeof document === 'undefined' || document.documentElement.lang === locale) return; + document.documentElement.lang = locale; +} + +/** Composes React i18n and syncs `` for sandbox locale demos. */ +export function SandboxI18nProvider({ children }: { children: ReactNode }) { + const locale = useLocale(); + + syncHtmlLang(locale); + + useEffect(() => { + syncHtmlLang(locale); + void ensureSandboxLocale(locale).catch(() => { + // I18nProvider still lazy-loads registry packs when prefetch fails. + }); + }, [locale]); + + return {children}; +} diff --git a/apps/sandbox/app/shared/react/use-locale.ts b/apps/sandbox/app/shared/react/use-locale.ts new file mode 100644 index 00000000..aa042cb9 --- /dev/null +++ b/apps/sandbox/app/shared/react/use-locale.ts @@ -0,0 +1,11 @@ +import type { SandboxLocaleTag } from '@app/shared/i18n/locale-meta'; +import { getInitialLocale, onLocaleChange } from '@app/shared/sandbox-listener'; +import { useEffect, useState } from 'react'; + +export function useLocale(): SandboxLocaleTag { + const [locale, setLocale] = useState(getInitialLocale); + + useEffect(() => onLocaleChange(setLocale), []); + + return locale; +} diff --git a/apps/sandbox/app/shared/sandbox-listener.ts b/apps/sandbox/app/shared/sandbox-listener.ts index 90a242bc..39cde642 100644 --- a/apps/sandbox/app/shared/sandbox-listener.ts +++ b/apps/sandbox/app/shared/sandbox-listener.ts @@ -1,4 +1,5 @@ import { SKINS } from '@app/constants'; +import { DEFAULT_SANDBOX_LOCALE, SANDBOX_LOCALE_TAGS, type SandboxLocaleTag } from '@app/shared/i18n/locale-meta'; import type { Skin } from '@app/types'; import { DEFAULT_AUDIO_SOURCE, SOURCES, type SourceId } from './sources'; @@ -35,6 +36,12 @@ let currentAutoplay = readBoolean('autoplay'); let currentMuted = readBoolean('muted'); let currentLoop = readBoolean('loop'); let currentPreload = readPreload(); +let currentLocale = readLocale(); + +function readLocale(): SandboxLocaleTag { + const value = params.get('locale'); + return SANDBOX_LOCALE_TAGS.includes(value as SandboxLocaleTag) ? (value as SandboxLocaleTag) : DEFAULT_SANDBOX_LOCALE; +} export function getInitialSkin(): Skin { return currentSkin; @@ -155,3 +162,22 @@ export function onPreloadChange(callback: (preload: PreloadValue) => void): () = window.removeEventListener('message', handler); }; } + +export function getInitialLocale(): SandboxLocaleTag { + return currentLocale; +} + +export function onLocaleChange(callback: (locale: SandboxLocaleTag) => void): () => void { + const handler = (event: MessageEvent) => { + if (event.data?.type !== 'locale-change' || !SANDBOX_LOCALE_TAGS.includes(event.data.locale)) return; + + currentLocale = event.data.locale; + callback(currentLocale); + }; + + window.addEventListener('message', handler); + + return () => { + window.removeEventListener('message', handler); + }; +} diff --git a/apps/sandbox/app/shell/app.tsx b/apps/sandbox/app/shell/app.tsx index 223424ba..8448ed9a 100644 --- a/apps/sandbox/app/shell/app.tsx +++ b/apps/sandbox/app/shell/app.tsx @@ -1,4 +1,5 @@ import { PLATFORMS, PRESETS, STYLINGS } from '@app/constants'; +import { DEFAULT_SANDBOX_LOCALE, SANDBOX_LOCALE_TAGS, type SandboxLocaleTag } from '@app/shared/i18n/locale-meta'; import { DEFAULT_PRELOAD, PRELOAD_VALUES, type PreloadValue } from '@app/shared/sandbox-listener'; import type { SourceId } from '@app/shared/sources'; import { @@ -35,6 +36,12 @@ function readParams() { muted: params.get('muted') === '1', loop: params.get('loop') === '1', preload: PRELOAD_VALUES.includes(preload as PreloadValue) ? (preload as PreloadValue) : DEFAULT_PRELOAD, + locale: (() => { + const value = params.get('locale'); + return SANDBOX_LOCALE_TAGS.includes(value as SandboxLocaleTag) + ? (value as SandboxLocaleTag) + : DEFAULT_SANDBOX_LOCALE; + })(), }; } @@ -49,6 +56,7 @@ export function App() { const [muted, setMuted] = useState(initial.muted); const [loop, setLoop] = useState(initial.loop); const [preload, setPreload] = useState(initial.preload); + const [locale, setLocale] = useState(initial.locale); const iframeRef = useRef(null); const pagePath = getPagePath(platform, preset); @@ -65,9 +73,10 @@ export function App() { muted: muted ? '1' : '0', loop: loop ? '1' : '0', preload, + locale, }); history.replaceState(null, '', `/?${params}`); - }, [platform, styling, preset, skin, source, autoplay, muted, loop, preload]); + }, [platform, styling, preset, skin, source, autoplay, muted, loop, preload, locale]); useEffect(() => { iframeRef.current?.contentWindow?.postMessage({ type: 'skin-change', skin }, '*'); @@ -93,26 +102,30 @@ export function App() { iframeRef.current?.contentWindow?.postMessage({ type: 'preload-change', preload }, '*'); }, [preload]); + useEffect(() => { + iframeRef.current?.contentWindow?.postMessage({ type: 'locale-change', locale }, '*'); + }, [locale]); + // Constrain source to MP4 when switching to audio useEffect(() => { if (preset === 'audio' && SOURCES[source].type !== 'mp4') { setSource(DEFAULT_AUDIO_SOURCE); } - }, [preset, source, setSource]); + }, [preset, source]); // Constrain source to DASH when switching to dash-video useEffect(() => { if (preset === 'dash-video' && SOURCES[source].type !== 'dash') { setSource(DEFAULT_DASH_SOURCE); } - }, [preset, source, setSource]); + }, [preset, source]); // Constrain source away from DASH for non-DASH presets useEffect(() => { if (preset !== 'dash-video' && SOURCES[source].type === 'dash') { setSource(DEFAULT_SOURCE); } - }, [preset, source, setSource]); + }, [preset, source]); // CDN, background video, and vimeo video do not have a Tailwind skin variant. useEffect(() => { @@ -124,7 +137,7 @@ export function App() { const availableSources = preset === 'audio' ? MP4_SOURCE_IDS : preset === 'dash-video' ? DASH_SOURCE_IDS : NON_DASH_SOURCE_IDS; - const handleSourceChange = useCallback((value: string) => setSource(value as SourceId), [setSource]); + const handleSourceChange = useCallback((value: string) => setSource(value as SourceId), []); return (
@@ -147,6 +160,8 @@ export function App() { onLoopChange={setLoop} preload={preload} onPreloadChange={setPreload} + locale={locale} + onLocaleChange={setLocale} availableSources={availableSources} isBackgroundVideo={preset === 'background-video'} isSimpleHls={preset.startsWith('simple-hls-')} @@ -170,6 +185,7 @@ export function App() { muted={muted} loop={loop} preload={preload} + locale={locale} />
); diff --git a/apps/sandbox/app/shell/navbar.tsx b/apps/sandbox/app/shell/navbar.tsx index 3e267fe7..424cd5a1 100644 --- a/apps/sandbox/app/shell/navbar.tsx +++ b/apps/sandbox/app/shell/navbar.tsx @@ -1,4 +1,5 @@ import type { SKINS } from '@app/constants'; +import { SANDBOX_LOCALE_OPTION_GROUPS, type SandboxLocaleTag } from '@app/shared/i18n/locale-meta'; import { PRELOAD_VALUES, type PreloadValue } from '@app/shared/sandbox-listener'; import type { SourceId } from '@app/shared/sources'; import type { Platform, Preset, Skin, Styling } from '@app/types'; @@ -23,6 +24,8 @@ type NavbarProps = { onLoopChange: (value: boolean) => void; preload: PreloadValue; onPreloadChange: (value: PreloadValue) => void; + locale: SandboxLocaleTag; + onLocaleChange: (value: SandboxLocaleTag) => void; availableSources: readonly SourceId[]; isBackgroundVideo: boolean; isSimpleHls: boolean; @@ -76,6 +79,8 @@ export function Navbar({ onLoopChange, preload, onPreloadChange, + locale, + onLocaleChange, availableSources, isBackgroundVideo, isSimpleHls, @@ -154,6 +159,8 @@ export function Navbar({ onLoopChange={onLoopChange} preload={preload} onPreloadChange={onPreloadChange} + locale={locale} + onLocaleChange={onLocaleChange} /> void; preload: PreloadValue; onPreloadChange: (value: PreloadValue) => void; + locale: SandboxLocaleTag; + onLocaleChange: (value: SandboxLocaleTag) => void; }; function SettingsMenu({ @@ -201,6 +210,8 @@ function SettingsMenu({ onLoopChange, preload, onPreloadChange, + locale, + onLocaleChange, }: SettingsMenuProps) { const [open, setOpen] = useState(false); const containerRef = useRef(null); @@ -209,6 +220,7 @@ function SettingsMenu({ const mutedId = useId(); const loopId = useId(); const preloadId = useId(); + const localeId = useId(); useEffect(() => { if (!open) return; @@ -265,8 +277,15 @@ function SettingsMenu({