fix(portals): treat an undetermined audio language as unknown (#1689)

* fix(portals): treat an undetermined audio language as unknown

ICU 78, which Electron 43 and Node 26 ship, canonicalizes the ffprobe
marker `und` to the subtag `und` instead of an empty one. The source
metadata then recorded it as a stated language, so a switch between a
copy tagged `und` and one tagged English raised the "dub may differ"
warning. CI's older ICU hid this: the existing spec only fails on the
newer runtime.

Decline `und` explicitly, and cover `und-US` plus the dub comparison.

* test(portals): cover undetermined language across runtimes

---------

Co-authored-by: 4gray <serega05@gmail.com>
This commit is contained in:
Mahmut YUMand4gray authored and GitHub committed 2026-10-01 06:24:20 +02:00
1 parent 444ec06e94
commit 1dba959852
4 files changed
+67 -4

No files matched your search

@@ -0,0 +1,7 @@
---
type: fix
area: portals
---
Switching to another playlist's copy of a movie no longer warns that the dub
may differ when one copy's audio language is simply unknown (`und`).
+5
View File
@@ -776,6 +776,11 @@ tag a language at all, so in practice it is usually silent — which is the
honest state, and the same one the rest of this feature takes when it does not
know.
Undetermined language tags (`und`, including regional forms such as `und-US`)
are explicitly treated as unknown after normalization. This must hold whether
the runtime's `Intl.Locale` returns no language or preserves `und`: neither
case establishes a factual dub difference against a known language.
Switching sources through `startResolvedPlayback` closes the external session
it LAUNCHED first — tracked separately so refreshes and overlapping handoffs
cannot disown it within the same route session. The retained identity is ignored
@@ -180,8 +180,10 @@ describe('applyApiMetadata', () => {
});
it.each([
// ffprobe's marker for "we do not know".
// ffprobe's marker for "we do not know", regardless of whether
// this runtime's Intl.Locale preserves the language subtag.
['und'],
['und-US'],
['Russian'],
['en_US'],
[''],
@@ -360,6 +362,45 @@ describe('audioDiffersFactually', () => {
expect(audioDiffersFactually(from, to)).toBe(false);
});
it('stays silent when one side reports an undetermined language', () => {
// `und` is ffprobe admitting it does not know; measured against a
// stated English it is not a dub change.
const from = candidate(
applyApiMetadata(candidate(), { audioLanguage: 'und' })
);
const to = candidate(
applyApiMetadata(candidate(), { audioLanguage: 'eng' })
);
expect(audioDiffersFactually(from, to)).toBe(false);
});
it.each(['und', 'und-US', 'UND'])(
'stays silent for %s when Intl.Locale preserves und',
(raw) => {
const known = applyApiMetadata(candidate(), {
audioLanguage: 'eng',
});
// Exercise the newer runtime behavior even on Node 22. The
// captured Locale constructor still reads this prototype getter.
const language = jest
.spyOn(Intl.Locale.prototype, 'language', 'get')
.mockReturnValue('und');
try {
const unknown = applyApiMetadata(candidate(), {
audioLanguage: raw,
});
expect(unknown.audioLanguage).toBeUndefined();
expect(audioDiffersFactually(unknown, known)).toBe(false);
expect(audioDiffersFactually(known, unknown)).toBe(false);
} finally {
language.mockRestore();
}
}
);
it('stays silent when both sides state the same track', () => {
const from = candidate({
audioLanguage: { value: 'rus', provenance: 'api' },
@@ -217,8 +217,11 @@ function keepFactual(field?: VodSourceField): VodSourceField | undefined {
*
* `Intl.Locale` does the canonicalizing: 639-2 collapses to 639-1 where one
* exists, both German forms land on `de`, and regions drop away. `und` —
* ffprobe's marker for undetermined — canonicalizes to nothing, which is
* exactly right: it means the provider does not know either.
* ffprobe's marker for undetermined — is declined explicitly: it means the
* provider does not know either. Some runtimes return no language for this
* tag; others (including Electron 43 and Node 26) preserve `und`. Declining
* it in either case prevents "unknown vs. English" from becoming a dub
* change, independently of the runtime's Intl implementation.
*
* Anything that survives with more than three characters is not a language
* code (`Russian` parses as the subtag `russian`), so the comparison is
@@ -232,7 +235,11 @@ function canonicalLanguage(raw: string | null | undefined): string | null {
try {
const language = new LocaleCtor(value).language;
return language && language.length <= 3 ? language : null;
return language &&
language.length <= 3 &&
language !== UNDETERMINED_LANGUAGE
? language
: null;
} catch {
// Not a well-formed tag at all; saying nothing beats comparing junk.
return null;
@@ -253,6 +260,9 @@ const LocaleCtor = (
}
).Locale;
/** ISO 639-2 "undetermined"; see `canonicalLanguage`. */
const UNDETERMINED_LANGUAGE = 'und';
function cleanString(raw: string | null | undefined): string | null {
const trimmed = typeof raw === 'string' ? raw.trim() : '';
return trimmed === '' ? null : trimmed;