Dreamio Turn Document · May 25, 2026

# Preserve external subtitle language names

Dreamio now keeps known OpenSubtitles language metadata attached to generic VLC subtitle tracks, so the native captions menu can show useful names instead of a stack of indistinguishable `Track N` entries.

Issue dreamio-ejh
Validation Tests and simulator build passed
Scope Native VLC subtitles

## Summary

Fixed the subtitle menu naming path for externally attached VLC subtitle tracks. Generic VLC names like `Track 2` now receive Dreamio's preserved OpenSubtitles display name when available.

## Changes Made

- Changed VLC subtitle display-name reconciliation so saved external subtitle names are only consumed by generic VLC track labels.
- Expanded subtitle language-code aliases for the ISO-639-2 codes observed in Stremio/OpenSubtitles payloads, including Dutch, Danish, Romanian, Serbian, Persian, Portuguese, Finnish, Hebrew, Hungarian, Turkish, Czech, Arabic, Greek, French, German, Spanish, and English.
- Added regression coverage for Dutch and Danish three-letter subtitle codes.

## Context

The debug log showed OpenSubtitles candidates arriving with usable language metadata, then VLC eventually surfacing external subtitle tracks as `Track 2`, `Track 3`, and similar generic names. Dreamio already had a preservation queue, but it could spend a queued display name on a VLC track that already had a meaningful title, which left later generic tracks unnamed.

## Important Implementation Details

The key behavioral change is deliberately narrow: reconciliation now skips non-generic VLC subtitle names. If VLC gives a real name such as `English (SDH) - [English]`, Dreamio keeps it. If VLC gives `Track 4`, Dreamio can replace it with the next preserved external subtitle display name.

The alias table turns common three-letter subtitle codes into two-letter language codes before asking `Locale` for localized names. This makes labels such as `nld` and `dan` become `Dutch` and `Danish` instead of raw codes.

## Relevant Diff Snippets

### Dreamio/VLCNativePlaybackBackend.swift

Dreamio/VLCNativePlaybackBackend.swift
+1

    328 unmodified lines329330331332333334328 unmodified lines            .filter { $0.id >= 0 }            .filter { !externalSubtitleBaselineTrackIDs.contains($0.id) }            .filter { externalSubtitleDisplayNamesByTrackID[$0.id] == nil }            .sorted { $0.id < $1.id }            .forEach { track in                guard !pendingExternalSubtitleDisplayNames.isEmpty else {328 unmodified lines329330331332333334335328 unmodified lines            .filter { $0.id >= 0 }            .filter { !externalSubtitleBaselineTrackIDs.contains($0.id) }            .filter { externalSubtitleDisplayNamesByTrackID[$0.id] == nil }            .filter { SubtitleDisplayName.isGenericLabel($0.name) }            .sorted { $0.id < $1.id }            .forEach { track in                guard !pendingExternalSubtitleDisplayNames.isEmpty else {

### Dreamio/StreamCandidate.swift

Dreamio/StreamCandidate.swift
-1+36

    47 unmodified lines4849505152535455565758596047 unmodified lines    ]
        private static let languageCodeAliases = [        "eng": "en",        "en": "en",        "spa": "es",        "es": "es",        "fre": "fr",        "fra": "fr",        "fr": "fr"    ]
        static func displayName(for candidate: SubtitleCandidate) -> String {47 unmodified lines48495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949547 unmodified lines    ]
        private static let languageCodeAliases = [        "ara": "ar",        "ar": "ar",        "cze": "cs",        "ces": "cs",        "cs": "cs",        "dan": "da",        "da": "da",        "de": "de",        "deu": "de",        "ell": "el",        "el": "el",        "eng": "en",        "en": "en",        "fin": "fi",        "fi": "fi",        "spa": "es",        "es": "es",        "ger": "de",        "gre": "el",        "heb": "he",        "he": "he",        "hun": "hu",        "hu": "hu",        "fre": "fr",        "fra": "fr",        "fr": "fr",        "dut": "nl",        "nld": "nl",        "nl": "nl",        "per": "fa",        "fas": "fa",        "fa": "fa",        "pob": "pt",        "por": "pt",        "pt": "pt",        "ron": "ro",        "rum": "ro",        "ro": "ro",        "srp": "sr",        "sr": "sr",        "tur": "tr",        "tr": "tr"    ]
        static func displayName(for candidate: SubtitleCandidate) -> String {

### Tests/StreamResolverTests.swift

Tests/StreamResolverTests.swift
+16

    471 unmodified lines472473474475476477471 unmodified lines            )),            "movie.es"        )    }
        private static func testSubtitleDisplayNameUsesPreservedNamesForGenericVLCTracks() {471 unmodified lines472473474475476477478479480481482483484485486487488489490491492493471 unmodified lines            )),            "movie.es"        )        assertEqual(            SubtitleDisplayName.displayName(for: SubtitleCandidate(                url: URL(string: "https://opensubtitles.example.test/download/subtitle.srt")!,                label: "Track 3",                language: "nld"            )),            "Dutch"        )        assertEqual(            SubtitleDisplayName.displayName(for: SubtitleCandidate(                url: URL(string: "https://opensubtitles.example.test/download/subtitle.srt")!,                label: "Track 4",                language: "dan"            )),            "Danish"        )    }
        private static func testSubtitleDisplayNameUsesPreservedNamesForGenericVLCTracks() {

## Expected Impact for End-Users

When subtitles load through the native VLC player, the captions menu should be easier to scan. Users should see language names for generic external subtitle tracks instead of guessing which `Track N` corresponds to the language they want.

## Validation

- Ran `swiftc -parse-as-library Dreamio/StreamCandidate.swift Dreamio/StreamResolver.swift Tests/StreamResolverTests.swift -o /tmp/StreamResolverTests && /tmp/StreamResolverTests`: passed.
- Ran `DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer xcodebuild -workspace Dreamio.xcworkspace -scheme Dreamio -configuration Debug -sdk iphonesimulator build`: passed.

## Issues, Limitations, and Mitigations

This still depends on VLC eventually surfacing external subtitle tracks after `addPlaybackSlave`. The change improves the names once tracks appear; it does not change VLC's asynchronous attachment behavior or guarantee that every remote subtitle file becomes visible instantly.

## Follow-up Work

No required follow-up remains for this fix. A useful later improvement would be deeper URL-to-track correlation if MobileVLCKit exposes enough metadata to map each external subtitle slave to an exact surfaced track.