fix: match ISO 639-2 bibliographic and terminological language codes - #2041
Open
chrisbenincasa wants to merge 5 commits into
Open
fix: match ISO 639-2 bibliographic and terminological language codes#2041chrisbenincasa wants to merge 5 commits into
chrisbenincasa wants to merge 5 commits into
Conversation
Owner
Author
|
Added A broader sweep for the same bug class turned up several more sites (troubleshoot subtitle matching, Meilisearch language filtering, and unnormalized writes at the Plex/Jellyfin/Emby boundary). Those are being handled separately to keep this PR focused. |
This was referenced Aug 28, 2026
ISO 639-2 assigns 20 languages two three-letter codes: a bibliographic (/B) code derived from the English name and a terminological (/T) code derived from the native name. German is both "ger" and "deu". Media servers and containers use them interchangeably. A German audio preference never matched a Jellyfin track, because: - LanguageService.getAlpha3TCode called alpha3TToAlpha2 in its /B branch, so it returned undefined for every /B code. This broke normalization in CelEvaluationService.matchLanguageInList, SubtitleStreamPicker and LocalSubtitlesService, and dropped the language of any sidecar or container track tagged with a /B code. - StreamSelectionEvaluator compared codes with raw string equality and never normalized either side. - SubtitleStreamPicker normalized the stream's code but compared it to a raw, unnormalized preference. - The FFmpeg settings language picker emitted /B codes while the channel subtitle picker emitted /T codes, so the two screens disagreed and the subtitle table could not resolve a display name for either. Adds LanguageService.codesMatch, which normalizes both sides to /T and falls back to a case-insensitive exact comparison for codes it cannot resolve, and routes all stream language matching through it. Stream language is now resolved by tag precedence (ISO 639-2 > ISO 639-1 > free-form), matching what buildCelContext already does. Both web pickers now emit /T. Preferences already stored as /B keep working through codesMatch and still resolve a display name, so no migration is needed. Fixes #1960 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The options list suppressed already-selected languages by comparing ISO 639-2 codes while isOptionEqualToValue compared ISO 639-1 codes. Now that the picker emits terminological codes, a preference saved earlier as "ger" no longer matched the "deu" option, so German was offered again in the dropdown and could be selected twice. Both checks now key on the ISO 639-1 code, which has no bibliographic and terminological split. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The troubleshoot simulator resolved audio through the stream selector but re-implemented subtitle language matching inline with raw string comparison. After the selector learned to treat the two ISO 639-2 code sets as equivalent, the two disagreed: the report would claim "No subtitle found for languages: ger" for a file that playback selects a German subtitle for. This matters more than a normal display bug, because the troubleshoot report is what users paste into issues — #1960 was diagnosed from one. Extracts the lookup into findSubtitleForLanguages and routes it through the selector's streamMatchesLanguage, which is now exported. Also adds the missing iso6392 to the FFmpeg settings language-preference fallback; LanguagePreferenceSchema requires it, so the fallback produced a value that would fail validation. Refs #1960, #2026 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chrisbenincasa
force-pushed
the
fix/iso-639-language-code-matching
branch
from
September 7, 2026 17:24
d1d9e14 to
84c337e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1960
Problem
ISO 639-2 assigns 20 languages two three-letter codes: a bibliographic (
/B) code derived from the English name, and a terminological (/T) code derived from the native name. German is bothgeranddeu; French bothfreandfra. Media servers and containers use them interchangeably.A German audio preference never matched a Jellyfin track. From the reporter's troubleshooting log — preference
[ger, eng], track taggeddeu, English selected:English is unaffected (it has only one code), which is why this went unnoticed.
Root cause
Four defects, one of which is the origin of the other three:
LanguageService.getAlpha3TCodewas broken for every/Bcode. Its/Bbranch calledalpha3TToAlpha2instead ofalpha3BToAlpha2.alpha3TToAlpha2('ger')returnsundefined, so the function returnedundefinedfor all 20 bibliographic codes. This silently brokeCelEvaluationService.matchLanguageInList,SubtitleStreamPicker, andLocalSubtitlesService— the last of which dropped the language of any sidecar subtitle named with a/Bcode (Movie.ger.srt), logging it as an unknown language.StreamSelectionEvaluatornever normalized at all. Both the audio and subtitleby_languagepaths compared codes with raw lowercased string equality, so'deu' !== 'ger'fell through to the next preference.SubtitleStreamPickernormalized only one side. It ran the stream's code throughgetAlpha3TCodebut compared the result to a raw, unnormalized preference.The two web language pickers disagreed.
useLanguagePreferences(Settings → FFmpeg → Audio Language Preferences) emitted/Bviaalpha2ToAlpha3B, whileLanguageAutocomplete(channel subtitle preferences) emitted/Tviaalpha2ToAlpha3T. The server normalizes all probed stream tags to/T, so the FFmpeg settings screen was writing codes that could never match.Also fixed (not in the original report)
languageBy3LetterCodewas keyed on/Bcodes butChannelSubtitlePreferencesTablelooks up values written byLanguageAutocomplete, which emits/T. The Language column rendered blank for all 20 affected languages.Changes
alpha3BToAlpha2call inLanguageService.getAlpha3TCode.LanguageService.codesMatch, which normalizes both sides to/Tand falls back to a case-insensitive exact comparison when either side can't be resolved — keeping private-use codes such asqaaworking.StreamSelectionEvaluatorandSubtitleStreamPickerthroughcodesMatch.ISO 639-2 > ISO 639-1 > free-form) rather than matching against any of the three fields. This is the precedencebuildCelContextalready uses in the same file, so the module is now self-consistent./T; the display map indexes both code sets.maybeLang3Blocal inLocalSubtitlesService— it holds a/Tcode.No migration needed. Preferences already stored as
/Bkeep matching throughcodesMatch, and still resolve a display name.Note for reviewers
Two pre-existing tests began failing once matching actually worked. Their fixtures build a "French" stream that also carries
languageCodeISO6391: 'en'andlanguage: 'English'— an artifact ofmakeAudioStream's English defaults. The old code only avoided matching them by accident ('english' !== 'eng'). I made the fixtures internally consistent and added a test pinning the tag-precedence rule so the choice is tested rather than incidental.Test plan
LanguageService.test.tscovers all 20/B↔/Tpairs in both directions, pluscodesMatch— 74 testsStreamSelectionEvaluator.test.tsfor audio and subtitles, including preference ordering,preferChannels, tag precedence, and the unresolvable-code fallbackSubtitleStreamPicker.test.tsCelEvaluationService.test.tsforhasAudioLangweb/src/helpers/language.test.tspins that both pickers emit/Tand that legacy/Bvalues still resolve a display nameLanguageServicebug produces 12 failures across the three server suitespnpm turbo typecheckcleanpnpm lint-changedcleanpnpm turbo test— 1319 server tests, 87 web tests, all passing🤖 Generated with Claude Code