Skip to content

Commit 63898ce

Browse files
sampottscursoragent
andcommitted
feat(packages): add built-in locale packs with v8 parity
Ship 50+ non-English locale modules in core (default export per pack), with framework entry points at @videojs/html/i18n and @videojs/react/i18n (locales/* and all). Includes pt-BR/pt-PT and zh-CN/zh-TW splits, pt/zh v8 aliases, locale completeness tests, dynamic tsdown entries, and sandbox locale picker wiring. Closes #1368 Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 56466e2 commit 63898ce

188 files changed

Lines changed: 3510 additions & 106 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ dist/
2222
lib/
2323
out/
2424
packages/*/types/
25+
# tsgo/tsc accidental emit beside i18n sources (declarations belong in dist/)
26+
packages/core/src/core/i18n/**/*.d.ts
27+
packages/core/src/core/i18n/**/*.d.ts.map
28+
packages/html/src/i18n/**/*.d.ts
29+
packages/html/src/i18n/**/*.d.ts.map
30+
packages/react/src/i18n/**/*.d.ts
31+
packages/react/src/i18n/**/*.d.ts.map
2532
*.tsbuildinfo
2633
.turbo/
2734
coverage/

apps/sandbox/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Three directories participate:
2222
- **`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.
2323
- **`src/`** — Your working copy where you freely edit, experiment, and break things. Fully gitignored (`src/*`).
2424

25-
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.
25+
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.
2626

2727
Vite discovers sandbox entries by scanning `src/*` for subdirectories that contain an `index.html` — no manual registration is needed.
2828

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { BUILT_IN_LOCALES, registerI18n } from '@videojs/html/i18n';
2+
import { all } from '@videojs/html/i18n/all';
3+
4+
/** Locales available in the sandbox language picker (includes v8 aliases). */
5+
export const SANDBOX_LOCALE_TAGS = ['en', ...BUILT_IN_LOCALES, 'pt', 'zh'] as const;
6+
7+
export type SandboxLocaleTag = (typeof SANDBOX_LOCALE_TAGS)[number];
8+
9+
export const DEFAULT_SANDBOX_LOCALE: SandboxLocaleTag = 'en';
10+
11+
type LoadedLocalePackTag = keyof typeof all;
12+
13+
let activeLocale: SandboxLocaleTag | null = null;
14+
15+
/** Registers built-in copy for non-English tags (English uses the default registry layer). */
16+
export function ensureSandboxLocale(tag: SandboxLocaleTag): void {
17+
if (tag === 'en') {
18+
activeLocale = 'en';
19+
return;
20+
}
21+
22+
if (activeLocale === tag) {
23+
return;
24+
}
25+
26+
const translations = all[tag as LoadedLocalePackTag];
27+
if (!translations) {
28+
throw new Error(`Unknown sandbox locale: ${tag}`);
29+
}
30+
31+
registerI18n(tag, translations);
32+
activeLocale = tag;
33+
}
34+
35+
const LANGUAGE_NAMES = new Intl.DisplayNames('en', { type: 'language' });
36+
37+
/** v8 `np` is not a valid BCP 47 tag (Nepali is `ne`). */
38+
const LOCALE_LABEL_OVERRIDES: Partial<Record<SandboxLocaleTag, string>> = {
39+
np: 'Nepali',
40+
};
41+
42+
export function sandboxLocaleLabel(tag: SandboxLocaleTag): string {
43+
return LOCALE_LABEL_OVERRIDES[tag] ?? LANGUAGE_NAMES.of(tag) ?? tag;
44+
}
45+
46+
export const SANDBOX_LOCALE_OPTIONS = [...SANDBOX_LOCALE_TAGS]
47+
.map((tag) => ({
48+
value: tag,
49+
label: sandboxLocaleLabel(tag),
50+
}))
51+
.sort((a, b) => a.label.localeCompare(b.label, 'en'));

apps/sandbox/app/shared/i18n/sandbox-translations.ts

Lines changed: 0 additions & 59 deletions
This file was deleted.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { SandboxLocaleTag } from '@app/shared/i18n/sandbox-locales';
2+
import { getInitialLocale, onLocaleChange } from '@app/shared/sandbox-listener';
3+
import { useEffect, useState } from 'react';
4+
5+
export function useLocale(): SandboxLocaleTag {
6+
const [locale, setLocale] = useState(getInitialLocale);
7+
8+
useEffect(() => onLocaleChange(setLocale), []);
9+
10+
return locale;
11+
}

apps/sandbox/app/shared/sandbox-listener.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { SKINS } from '@app/constants';
2+
import { DEFAULT_SANDBOX_LOCALE, SANDBOX_LOCALE_TAGS, type SandboxLocaleTag } from '@app/shared/i18n/sandbox-locales';
23
import type { Skin } from '@app/types';
34
import { DEFAULT_AUDIO_SOURCE, SOURCES, type SourceId } from './sources';
45

@@ -35,6 +36,12 @@ let currentAutoplay = readBoolean('autoplay');
3536
let currentMuted = readBoolean('muted');
3637
let currentLoop = readBoolean('loop');
3738
let currentPreload = readPreload();
39+
let currentLocale = readLocale();
40+
41+
function readLocale(): SandboxLocaleTag {
42+
const value = params.get('locale');
43+
return SANDBOX_LOCALE_TAGS.includes(value as SandboxLocaleTag) ? (value as SandboxLocaleTag) : DEFAULT_SANDBOX_LOCALE;
44+
}
3845

3946
export function getInitialSkin(): Skin {
4047
return currentSkin;
@@ -155,3 +162,22 @@ export function onPreloadChange(callback: (preload: PreloadValue) => void): () =
155162
window.removeEventListener('message', handler);
156163
};
157164
}
165+
166+
export function getInitialLocale(): SandboxLocaleTag {
167+
return currentLocale;
168+
}
169+
170+
export function onLocaleChange(callback: (locale: SandboxLocaleTag) => void): () => void {
171+
const handler = (event: MessageEvent) => {
172+
if (event.data?.type !== 'locale-change' || !SANDBOX_LOCALE_TAGS.includes(event.data.locale)) return;
173+
174+
currentLocale = event.data.locale;
175+
callback(currentLocale);
176+
};
177+
178+
window.addEventListener('message', handler);
179+
180+
return () => {
181+
window.removeEventListener('message', handler);
182+
};
183+
}

apps/sandbox/app/shell/app.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { PLATFORMS, PRESETS, STYLINGS } from '@app/constants';
2+
import { DEFAULT_SANDBOX_LOCALE, SANDBOX_LOCALE_TAGS, type SandboxLocaleTag } from '@app/shared/i18n/sandbox-locales';
23
import { DEFAULT_PRELOAD, PRELOAD_VALUES, type PreloadValue } from '@app/shared/sandbox-listener';
34
import type { SourceId } from '@app/shared/sources';
45
import {
@@ -34,6 +35,12 @@ function readParams() {
3435
muted: params.get('muted') === '1',
3536
loop: params.get('loop') === '1',
3637
preload: PRELOAD_VALUES.includes(preload as PreloadValue) ? (preload as PreloadValue) : DEFAULT_PRELOAD,
38+
locale: (() => {
39+
const value = params.get('locale');
40+
return SANDBOX_LOCALE_TAGS.includes(value as SandboxLocaleTag)
41+
? (value as SandboxLocaleTag)
42+
: DEFAULT_SANDBOX_LOCALE;
43+
})(),
3744
};
3845
}
3946

@@ -48,7 +55,9 @@ export function App() {
4855
const [muted, setMuted] = useState(initial.muted);
4956
const [loop, setLoop] = useState(initial.loop);
5057
const [preload, setPreload] = useState<PreloadValue>(initial.preload);
58+
const [locale, setLocale] = useState<SandboxLocaleTag>(initial.locale);
5159
const iframeRef = useRef<HTMLIFrameElement>(null);
60+
const showLocalePicker = preset === 'video' && platform !== 'cdn';
5261

5362
const pagePath = getPagePath(platform, preset);
5463

@@ -64,9 +73,10 @@ export function App() {
6473
muted: muted ? '1' : '0',
6574
loop: loop ? '1' : '0',
6675
preload,
76+
locale,
6777
});
6878
history.replaceState(null, '', `/?${params}`);
69-
}, [platform, styling, preset, skin, source, autoplay, muted, loop, preload]);
79+
}, [platform, styling, preset, skin, source, autoplay, muted, loop, preload, locale]);
7080

7181
useEffect(() => {
7282
iframeRef.current?.contentWindow?.postMessage({ type: 'skin-change', skin }, '*');
@@ -92,6 +102,11 @@ export function App() {
92102
iframeRef.current?.contentWindow?.postMessage({ type: 'preload-change', preload }, '*');
93103
}, [preload]);
94104

105+
useEffect(() => {
106+
if (!showLocalePicker) return;
107+
iframeRef.current?.contentWindow?.postMessage({ type: 'locale-change', locale }, '*');
108+
}, [locale, showLocalePicker]);
109+
95110
// Constrain source to MP4 when switching to audio
96111
useEffect(() => {
97112
if (preset === 'audio' && SOURCES[source].type !== 'mp4') {
@@ -146,6 +161,9 @@ export function App() {
146161
onLoopChange={setLoop}
147162
preload={preload}
148163
onPreloadChange={setPreload}
164+
locale={locale}
165+
onLocaleChange={setLocale}
166+
showLocalePicker={showLocalePicker}
149167
availableSources={availableSources}
150168
isBackgroundVideo={preset === 'background-video'}
151169
isSimpleHlsVideo={preset === 'simple-hls-video'}
@@ -168,6 +186,7 @@ export function App() {
168186
muted={muted}
169187
loop={loop}
170188
preload={preload}
189+
locale={locale}
171190
/>
172191
</div>
173192
);

apps/sandbox/app/shell/navbar.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { SKINS } from '@app/constants';
2+
import { SANDBOX_LOCALE_OPTIONS, type SandboxLocaleTag } from '@app/shared/i18n/sandbox-locales';
23
import { PRELOAD_VALUES, type PreloadValue } from '@app/shared/sandbox-listener';
34
import type { SourceId } from '@app/shared/sources';
45
import type { Platform, Preset, Skin, Styling } from '@app/types';
@@ -23,6 +24,9 @@ type NavbarProps = {
2324
onLoopChange: (value: boolean) => void;
2425
preload: PreloadValue;
2526
onPreloadChange: (value: PreloadValue) => void;
27+
locale: SandboxLocaleTag;
28+
onLocaleChange: (value: SandboxLocaleTag) => void;
29+
showLocalePicker: boolean;
2630
availableSources: readonly SourceId[];
2731
isBackgroundVideo: boolean;
2832
isSimpleHlsVideo: boolean;
@@ -73,6 +77,9 @@ export function Navbar({
7377
onLoopChange,
7478
preload,
7579
onPreloadChange,
80+
locale,
81+
onLocaleChange,
82+
showLocalePicker,
7683
availableSources,
7784
isBackgroundVideo,
7885
isSimpleHlsVideo,
@@ -150,6 +157,9 @@ export function Navbar({
150157
onLoopChange={onLoopChange}
151158
preload={preload}
152159
onPreloadChange={onPreloadChange}
160+
locale={locale}
161+
onLocaleChange={onLocaleChange}
162+
showLocalePicker={showLocalePicker}
153163
/>
154164
<a
155165
href="https://github.com/videojs/v10"
@@ -186,6 +196,9 @@ type SettingsMenuProps = {
186196
onLoopChange: (value: boolean) => void;
187197
preload: PreloadValue;
188198
onPreloadChange: (value: PreloadValue) => void;
199+
locale: SandboxLocaleTag;
200+
onLocaleChange: (value: SandboxLocaleTag) => void;
201+
showLocalePicker: boolean;
189202
};
190203

191204
function SettingsMenu({
@@ -197,6 +210,9 @@ function SettingsMenu({
197210
onLoopChange,
198211
preload,
199212
onPreloadChange,
213+
locale,
214+
onLocaleChange,
215+
showLocalePicker,
200216
}: SettingsMenuProps) {
201217
const [open, setOpen] = useState(false);
202218
const containerRef = useRef<HTMLDivElement>(null);
@@ -205,6 +221,7 @@ function SettingsMenu({
205221
const mutedId = useId();
206222
const loopId = useId();
207223
const preloadId = useId();
224+
const localeId = useId();
208225

209226
useEffect(() => {
210227
if (!open) return;
@@ -261,8 +278,17 @@ function SettingsMenu({
261278
<div
262279
id={menuId}
263280
role="menu"
264-
className="absolute right-0 top-full mt-2 z-20 grid grid-cols-[1fr_auto] auto-rows-[1.75rem] items-center gap-x-6 gap-y-1 rounded-md border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 p-3 shadow-md shadow-black/5"
281+
className="absolute right-0 top-full mt-2 z-20 grid grid-cols-[1fr_auto] auto-rows-[1.75rem] items-center gap-x-6 gap-y-1 rounded-md border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 p-3 shadow-md shadow-black/5 max-h-[min(24rem,70vh)] overflow-y-auto"
265282
>
283+
{showLocalePicker && (
284+
<SelectItem
285+
id={localeId}
286+
label="Language"
287+
value={locale}
288+
onChange={(value) => onLocaleChange(value as SandboxLocaleTag)}
289+
options={SANDBOX_LOCALE_OPTIONS}
290+
/>
291+
)}
266292
<CheckboxItem id={autoplayId} label="Autoplay" checked={autoplay} onChange={onAutoplayChange} />
267293
<CheckboxItem id={mutedId} label="Muted" checked={muted} onChange={onMutedChange} />
268294
<CheckboxItem id={loopId} label="Loop" checked={loop} onChange={onLoopChange} />

apps/sandbox/app/shell/preview.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { SandboxLocaleTag } from '@app/shared/i18n/sandbox-locales';
12
import type { PreloadValue } from '@app/shared/sandbox-listener';
23
import type { SourceId } from '@app/shared/sources';
34
import type { Preset, Skin, Styling } from '@app/types';
@@ -13,10 +14,11 @@ type PreviewProps = {
1314
muted: boolean;
1415
loop: boolean;
1516
preload: PreloadValue;
17+
locale: SandboxLocaleTag;
1618
};
1719

1820
export const Preview = forwardRef<HTMLIFrameElement, PreviewProps>(function Preview(
19-
{ pagePath, preset, skin, styling, source, autoplay, muted, loop, preload },
21+
{ pagePath, preset, skin, styling, source, autoplay, muted, loop, preload, locale },
2022
ref
2123
) {
2224
const buildUrl = (base: string) => {
@@ -29,6 +31,7 @@ export const Preview = forwardRef<HTMLIFrameElement, PreviewProps>(function Prev
2931
muted: muted ? '1' : '0',
3032
loop: loop ? '1' : '0',
3133
preload,
34+
locale,
3235
});
3336
return `${base}?${params}`;
3437
};

apps/sandbox/scripts/setup.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
import { mirrorTemplatesToSrc, removeGeneratedSrcFiles } from './shared.js';
1+
import { mirrorTemplatesToSrc, removeGeneratedSrcFiles, syncTemplatesToSrc } from './shared.js';
22

3+
const synced = await syncTemplatesToSrc();
34
const created = await mirrorTemplatesToSrc();
45
const removed = await removeGeneratedSrcFiles();
56

7+
for (const file of synced) {
8+
console.log(`Synced ${file}`);
9+
}
10+
611
for (const file of created) {
712
console.log(`Created ${file}`);
813
}

0 commit comments

Comments
 (0)