Skip to content

feat(html): i18n#1591

Open
sampotts wants to merge 32 commits into
feat/i18nfrom
feat/i18n-html
Open

feat(html): i18n#1591
sampotts wants to merge 32 commits into
feat/i18nfrom
feat/i18n-html

Conversation

@sampotts

@sampotts sampotts commented May 25, 2026

Copy link
Copy Markdown
Collaborator

Refs #222
Closes #1365
Closes #1367

Summary

Adds the HTML i18n layer with ambient <html lang> inheritance, browser translation fallback, default loadLocale, and translated labels across web component controls.

Changes

  • createI18n()I18nController, ProviderMixin, TextMixin, <media-i18n-provider>, <media-text>
  • Controls auto-forward translated aria-labels and tooltips via I18nController
  • Error dialog uses core i18n helpers; CDN i18n entry point and build plugin
  • Package exports for @videojs/html/i18n and @videojs/html/cdn/i18n
Stack info

PR 3 of 5 — stacks on locale packs PR.

Testing

pnpm -F @videojs/html build
pnpm -F @videojs/html test src/i18n

Made with Cursor


Note

Medium Risk
Touches many player UI and accessibility strings across skins and controls; async locale/browser loading adds edge cases, though covered by new tests.

Overview
Adds an HTML i18n layer on top of @videojs/core/i18n: createI18n() exposes Lit context, I18nController, provider/text mixins, and registers <media-i18n-provider> / <media-text>. Locale resolution follows explicit lang, ambient <html lang>, lazy shipped locale packs, registry overlays, and optional browser translation (with sequence guards on async loads).

Players and SkinElement are wrapped with I18nProviderMixin so skins and controls share a translator. UI elements consume I18nController and use resolveControlAttrs / resolveControlLabel for translated aria-labels, tooltips, error-dialog copy, time remaining phrases, and input-indicator labels. Skin templates drop hardcoded error-dialog strings in favor of runtime-filled elements.

Packaging & CDN: new @videojs/html/i18n and locale export paths, build:cdn runs locale generation plus cdnI18nExternalPlugin (shared i18n.js on jsDelivr in prod, relative in dev), and the video CDN entry pulls in define/i18n.

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

@vercel

vercel Bot commented May 25, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
v10-sandbox Ready Ready Preview, Comment Jul 3, 2026 4:15am

Request Review

Comment thread packages/html/src/define/video/skin.ts
Comment thread packages/html/src/ui/playback-rate-menu/playback-rate-menu-trigger-element.ts Outdated
Comment thread build/plugins/cdn-i18n-external-plugin.ts
Comment thread packages/html/src/i18n/create-i18n.ts Outdated
Comment thread packages/html/src/ui/tooltip/tooltip-element.ts Outdated
sampotts and others added 29 commits July 3, 2026 13:53
Compose i18n provider onto SkinElement, resolve playback-rate tooltips via
getResolvedLabel, and normalize CDN i18n entry paths on Windows.

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

Co-authored-by: Cursor <cursoragent@cursor.com>
Keep tooltip copy visible before media state attaches by translating the
trigger label when getResolvedLabel returns undefined.

Co-authored-by: Cursor <cursoragent@cursor.com>
Adds integration coverage that play button aria-label updates when
document.documentElement.lang changes and the i18n provider has no
explicit lang attribute.

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

Co-authored-by: Cursor <cursoragent@cursor.com>
* via `adoptedStyleSheets` (or `<style>` fallback).
*/
export class SkinElement extends ReactiveElement {
export class SkinElement extends I18nProviderMixin(ReactiveElement) {

@mihar-22 mihar-22 Jul 7, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rough thought but this is a possible smell to me in that it needs to be baked in and not an optional higher-level concern. Largely related to my comments on #1708.

In other words, I think i18n should be composed in rather than provided out of the box but this circles back to conversation around keys because a default english translation is required since keys have no meaning on their own.

readonly #consumer: ContextConsumer<I18nContext, ReactiveControllerHost & HTMLElement>;
#unsubscribeRegistry: (() => void) | undefined;

constructor(host: ReactiveControllerHost & HTMLElement, context: I18nContext) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(suggestion): pull ReactiveControllerHost & HTMLElement up into a I18nControllerHost type

Comment on lines +59 to +61
export namespace I18nController {
export type Constructor = new (host: ReactiveControllerHost & HTMLElement, context: I18nContext) => I18nController;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this can be Constructor = typeof I18nController, also good to add Host = I18nControllerHost to the namespace

Comment on lines +19 to +32
export function createI18n(options?: CreateI18nOptions): CreateI18nResult {
const ProviderMixin = createI18nProviderMixin({
i18nContext,
loadLocale: options?.loadLocale,
});
const TextMixin = createTextMixin({ i18nContext });

return {
context: i18nContext,
I18nController,
ProviderMixin,
TextMixin,
};
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

beautiful 🖼️

Comment on lines +4 to +7
/** DOM `lang` values are untyped strings; align with core {@link Locale} at the boundary. */
export function localeFromDomLang(raw: string | undefined): Locale | undefined {
return (domLangFromString(raw) as Locale) ?? undefined;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we could make the return type generic on domLangFromString, or we could move it out of from utils for now if it's only required in the html package.

Comment on lines +10 to +12
export function resolvePlayerLocale(explicit: Locale | undefined, inherited: Locale | undefined): Locale {
return effectiveLocale(explicit, inherited) as Locale;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same comments as localeFromDomLang

Comment on lines +26 to +27
/** Override lazy loading of shipped locale packs (tests or custom loaders). */
loadLocale?: LocaleLoader | undefined;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

maybe loader or load

import type { LocaleLoader, ReactiveElementMixinBase } from './types';

export interface I18nProviderConfig {
i18nContext: I18nContext;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

probably just context

export type I18nTextMixin = <Base extends ReactiveElementMixinBase>(Base: Base) => Constructor<ReactiveElement> & Base;

export interface TextMixinConfig {
i18nContext: I18nContext;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

probably just context is fine

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe it'd help if we had a base radio group element that the rest can extend and share basic logic on

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Wire I18nController into UI Elements and Move Error Dialog Defaults to Core Feature: HTML i18n Layer (@videojs/html/i18n)

4 participants