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
| Layer | What it covers | Mechanism |
|---|---|---|
| Content locale | Manifest content: titles, speech-bubble text, localized assets | LocalizedString resolution + pickLocalizedAsset |
| UI language | Player 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
localeinput onpw-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
localeChangeoutput fires with the newLocaleCode.
<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:
- Exact match —
de-DEentry for requestedde-DE. - Base-language match — any entry whose base language matches (
de-ATsatisfiesde-DE). - Fallback locale — exact match of the fallback (typically the work's
meta.default_locale). - Base language of the fallback.
- 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:
- exact locale match,
- base-language match,
- fallback locale match,
- fallback base-language match,
- a variant without
locale(universal), - 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:
| Function | Purpose |
|---|---|
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.
Related pages
- Localization concepts — the format-level model shared by schema, player, and CMS.
- CMS Localization — how creators produce translations.
- Reader Interface — where the language switch lives in the UI.