Using the PlayerLocalization

Localization

How the PanelWave Player resolves locales — fallback chains for text and assets, runtime language switching, and the separate UI language.

PanelWave works are multilingual by design: every user-facing string in a manifest is a LocalizedString keyed by BCP-47 locale (en-US, de-DE, …), and assets can ship per-locale variants. The player resolves both at render time and can switch language without a reload.

Two localization layers

LayerWhat it coversMechanism
Content localeManifest content: titles, speech-bubble text, localized assetsLocalizedString resolution + pickLocalizedAsset
UI languagePlayer chrome: toolbar labels, modals, loading/error text@ngx-translate with JSON files the host app serves (assets/i18n/<lang>.json)

Both follow the same selection: setting the content locale also switches the UI language to the locale's base language (de-DE → de). English and German UI files ship inside the npm package (node_modules/@panelwave/player/src/assets/i18n/); the host app copies them into its build and serves them — see Installation. They cover the toolbar (labels and tooltips), dialogs, the comments drawer's timestamps, the paywall overlay including its reason sentence (paywall.reason_*), the age gate (age_gate.*), the branch chooser (branch_chooser.*, including the "Option N" label of an unlabelled path) and — from the player release after 1.2.0 — the lock placeholder (player.locked.*), the cover and chapter labels (navigation.*) the autoplay Author timing (toolbar.author_timing*) and the toolbar opener (toolbar.show). The age gate's month names come from the browser's Intl data for the current locale, so they need no keys.

To add a UI language, create <lang>.json with the same keys (start from en.json) and serve it next to the others.

If you ship your own translation files, add the keys introduced on 2026-09-27 — age_gate.*, branch_chooser.* and paywall.reason_subscription_required / reason_purchase_required / reason_age_verification_required / reason_entitlement_required — or those surfaces show raw keys. Copy them from the package's en.json.

Setting and switching the locale

  • From the host: the locale input on pw-player-shell (default 'en-US'). Changing it later switches content and UI language live.
  • At runtime by the reader: the globe button in the toolbar opens the language modal, listing the work's meta.locales. It only appears when the work has more than one locale.
  • At runtime programmatically: call changeLocale(locale) on the shell.
  • Observing changes: the localeChange output fires with the new LocaleCode.
<pw-player-shell
  [manifest]="manifest"
  [locale]="'de-DE'"
  (localeChange)="onLocale($event)">
</pw-player-shell>

Switching is instant — components re-resolve their strings and assets against the new locale; no manifest reload occurs.

The fallback chain

Text resolution (resolveLocalizedString(value, requestedLocale, fallbackLocale)) tries, in order:

  1. Exact match — de-DE entry for requested de-DE.
  2. Base-language match — any entry whose base language matches (de-AT satisfies de-DE).
  3. Fallback locale — exact match of the fallback (typically the work's meta.default_locale).
  4. Base language of the fallback.
  5. First available entry — last resort, so text never disappears entirely.

The helper createLocaleFallbackChain('en-GB', 'en-US') materializes this as ['en-GB', 'en', 'en-US'] (base languages deduplicated).

Localized assets

Asset catalog entries can carry multiple variants, each optionally tagged with a locale — e.g. a voice-over in several languages, or artwork containing burned-in text. pickLocalizedAsset(variants, requestedLocale, fallbackLocale) selects:

  1. exact locale match,
  2. base-language match,
  3. fallback locale match,
  4. fallback base-language match,
  5. a variant without locale (universal),
  6. the first variant.
import { pickLocalizedAsset } from '@panelwave/player';

const variants = [
  { src: 'vo-en.mp3', mime: 'audio/mpeg', locale: 'en-US' },
  { src: 'vo-de.mp3', mime: 'audio/mpeg', locale: 'de-DE' },
  { src: 'vo.mp3', mime: 'audio/mpeg' }, // universal fallback
];

const chosen = pickLocalizedAsset(variants, 'de-DE', 'en-US');
// → { src: 'vo-de.mp3', … }

See Assets for how variants are declared in the manifest.

Utility functions

All exported from the package for host-application use:

FunctionPurpose
resolveLocalizedString(ls, requested, fallback)Resolve one LocalizedString with the full fallback chain
pickLocalizedAsset(variants, requested, fallback)Pick the best asset variant
getBaseLanguage(locale)'zh-Hans-CN' → 'zh'
isLocaleSupported(locale, supported)Exact or base-language membership test
createLocaleFallbackChain(requested, fallback)Ordered candidate list
normalizeLocaleCode(locale)'en_us' → 'en-US'
getLocalizationCompleteness(strings, locale)% of strings translated into a locale

Choosing a sensible initial locale

Match the browser language against the work's locales and fall back to the work's default:

import { isLocaleSupported, normalizeLocaleCode } from '@panelwave/player';
import type { PanelWaveManifest, LocaleCode } from '@panelwave/player';

function pickInitialLocale(manifest: PanelWaveManifest): LocaleCode {
  const wanted = normalizeLocaleCode(navigator.language); // e.g. 'de-DE'
  return isLocaleSupported(wanted, manifest.meta.locales)
    ? wanted
    : manifest.meta.default_locale;
}

To remember a language the reader picks, store the value of the localeChange output and pass it as locale on the next visit. localeChange fires only when the language changes, never with the start locale, so storing it does not overwrite your choice with the player's default. The PanelWave reader uses this order: ?lang= in the link, the language picked earlier on the device, the browser languages, then the work's default.