Der Kern der Sprachwahl: Kürzel, die nicht lügen, und Abwägen statt Filtern
**`LanguageCode`** bringt Kürzel auf eine vergleichbare Form. Der Anlass ist ein stiller Fehler: verglichen wurden die ersten zwei Zeichen, also `ge` gegen `de` — und `ger` ist die ISO-639-2/B-Form von Deutsch, die ffmpeg in viele Dateien schreibt. Bei solchen Titeln wirkte die Spracheinstellung überhaupt nicht. `gsw` bleibt `gsw`. Schweizerdeutsch ist keine Schreibweise von Deutsch, und es gleichzusetzen hiesse, jemandem Schweizerdeutsch unterzuschieben, der Hochdeutsch wollte. Es gehört in die Rangliste, nicht in eine Gleichsetzung. **`AudioTrackChooser`** wägt ab, statt zu filtern: jede Spur bekommt Punkte, die höchste gewinnt. Damit gibt es immer eine Antwort, und Audiodeskription wird gemieden, ohne unmöglich zu werden — gibt es eine Sprache nur mit Audiodeskription, ist sie besser als Stille. Gemerkt wird nicht eine Nummer, sondern eine Beschreibung der Spur: Folgen derselben Serie haben unterschiedlich sortierte Spuren, und „Spur 2" bedeutet in der nächsten Folge etwas anderes. Siebzehn Tests, darunter der ZDF-Fall wörtlich — deutsche Spur mit Audiodeskription als Standard, englisch betitelte deutsche Spur daneben — und zwar in beide Richtungen: mit Meiden gewinnt die richtige, ohne Meiden gewinnt das Kürzel. Sonst stünde der Schalter nur dabei. Dazu trägt `MediaStream` jetzt `title`, `isDefault`, `isHearingImpaired` und `isOriginal`. Der Name aus der Datei ist das zweite Signal neben dem Kürzel und manchmal das einzige richtige. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
946de29893
commit
afb4a63655
@@ -0,0 +1,202 @@
|
||||
import Testing
|
||||
import Foundation
|
||||
import MediaKit
|
||||
@testable import FlixyPlayback
|
||||
|
||||
@Suite("Sprachkürzel")
|
||||
struct LanguageCodeTests {
|
||||
/// Der Fall, an dem die alte Prüfung lautlos scheiterte: `ger` ist die
|
||||
/// ISO-639-2/B-Form von Deutsch, und ffmpeg schreibt sie in viele Dateien.
|
||||
@Test("ger, deu und de-DE meinen alle Deutsch")
|
||||
func germanHasThreeSpellings() {
|
||||
#expect(LanguageCode.matches("ger", "de"))
|
||||
#expect(LanguageCode.matches("deu", "de"))
|
||||
#expect(LanguageCode.matches("de-DE", "de"))
|
||||
#expect(LanguageCode.matches("GER", "de"))
|
||||
}
|
||||
|
||||
/// Schweizerdeutsch ist keine Schreibweise von Deutsch. Es gleichzusetzen
|
||||
/// wäre bequem und falsch — wer Hochdeutsch will, bekäme sonst
|
||||
/// Schweizerdeutsch untergeschoben.
|
||||
@Test("Schweizerdeutsch ist nicht Deutsch")
|
||||
func swissGermanStandsAlone() {
|
||||
#expect(!LanguageCode.matches("gsw", "de"))
|
||||
#expect(LanguageCode.matches("gsw", "gsw"))
|
||||
}
|
||||
|
||||
@Test("Auch die anderen Doppelformen")
|
||||
func otherDoubleForms() {
|
||||
#expect(LanguageCode.matches("fre", "fra"))
|
||||
#expect(LanguageCode.matches("dut", "nl"))
|
||||
#expect(LanguageCode.matches("cze", "cs"))
|
||||
#expect(LanguageCode.matches("chi", "zh"))
|
||||
}
|
||||
|
||||
@Test("Ohne Angabe kein Treffer")
|
||||
func missingIsNoMatch() {
|
||||
#expect(!LanguageCode.matches(nil, "de"))
|
||||
#expect(!LanguageCode.matches("de", nil))
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Wahl der Tonspur")
|
||||
struct AudioTrackChooserTests {
|
||||
private func spur(
|
||||
_ index: Int,
|
||||
_ sprache: String?,
|
||||
title: String? = nil,
|
||||
channels: Int = 2,
|
||||
isDefault: Bool = false
|
||||
) -> MediaStream {
|
||||
MediaStream(
|
||||
id: String(index),
|
||||
kind: .audio,
|
||||
codec: "aac",
|
||||
language: sprache,
|
||||
languageCode: sprache,
|
||||
displayTitle: title,
|
||||
channels: channels,
|
||||
title: title,
|
||||
isDefault: isDefault
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Der ZDF-Fall
|
||||
|
||||
/// Wörtlich der Ärger, wegen dem es den Typ gibt: die als deutsch
|
||||
/// ausgezeichnete Spur ist die mit Audiodeskription, und die als englisch
|
||||
/// ausgezeichnete ist in Wahrheit die deutsche ohne.
|
||||
private var zdf: [MediaStream] {
|
||||
[
|
||||
spur(0, "ger", title: "Deutsch (Audiodeskription)", isDefault: true),
|
||||
spur(1, "eng", title: "Deutsch"),
|
||||
]
|
||||
}
|
||||
|
||||
@Test("Audiodeskription wird gemieden, auch wenn sie Standard ist")
|
||||
func avoidsAudioDescription() {
|
||||
let gewählt = AudioTrackChooser.choose(
|
||||
from: zdf,
|
||||
wish: .init(language: "de")
|
||||
)
|
||||
#expect(gewählt?.id == "1")
|
||||
}
|
||||
|
||||
/// Ohne das Meiden gewinnt die ausgezeichnete Sprache — der Beweis, dass
|
||||
/// der Schalter tatsächlich das Ruder herumreißt und nicht nur dabeisteht.
|
||||
@Test("Ohne das Meiden gewinnt das Kürzel")
|
||||
func withoutAvoidanceTheCodeWins() {
|
||||
let gewählt = AudioTrackChooser.choose(
|
||||
from: zdf,
|
||||
wish: .init(language: "de", avoidsAudioDescription: false)
|
||||
)
|
||||
#expect(gewählt?.id == "0")
|
||||
}
|
||||
|
||||
/// Und wenn der Nutzer einmal gewählt hat, gilt das — auch gegen die
|
||||
/// Sprache.
|
||||
@Test("Die gemerkte Spur schlägt den Sprachwunsch")
|
||||
func rememberedBeatsLanguage() {
|
||||
let gemerkt = AudioTrackChooser.TrackSignature(zdf[1])
|
||||
let gewählt = AudioTrackChooser.choose(
|
||||
from: zdf,
|
||||
wish: .init(language: "de", remembered: gemerkt, avoidsAudioDescription: false)
|
||||
)
|
||||
#expect(gewählt?.id == "1")
|
||||
}
|
||||
|
||||
// MARK: - Die Rangliste
|
||||
|
||||
@Test("Fehlt die Wunschsprache, greift die Rangliste")
|
||||
func fallbackListApplies() {
|
||||
let spuren = [spur(0, "fre"), spur(1, "eng"), spur(2, "ita")]
|
||||
let gewählt = AudioTrackChooser.choose(
|
||||
from: spuren,
|
||||
wish: .init(language: "de", fallbacks: ["gsw", "en", "fr"])
|
||||
)
|
||||
#expect(gewählt?.id == "1")
|
||||
}
|
||||
|
||||
@Test("Die Reihenfolge der Rangliste zählt")
|
||||
func fallbackOrderMatters() {
|
||||
let spuren = [spur(0, "fre"), spur(1, "eng")]
|
||||
let zuerstFranzösisch = AudioTrackChooser.choose(
|
||||
from: spuren,
|
||||
wish: .init(language: "de", fallbacks: ["fr", "en"])
|
||||
)
|
||||
#expect(zuerstFranzösisch?.id == "0")
|
||||
}
|
||||
|
||||
@Test("Schweizerdeutsch lässt sich in die Rangliste stellen")
|
||||
func swissGermanInTheList() {
|
||||
let spuren = [spur(0, "eng"), spur(1, "gsw")]
|
||||
let gewählt = AudioTrackChooser.choose(
|
||||
from: spuren,
|
||||
wish: .init(language: "de", fallbacks: ["gsw", "en"])
|
||||
)
|
||||
#expect(gewählt?.id == "1")
|
||||
}
|
||||
|
||||
// MARK: - Der Boden darunter
|
||||
|
||||
@Test("Ohne jede Übereinstimmung kommt trotzdem eine Spur")
|
||||
func alwaysAnswers() {
|
||||
let spuren = [spur(0, "jpn"), spur(1, "kor", isDefault: true)]
|
||||
let gewählt = AudioTrackChooser.choose(
|
||||
from: spuren,
|
||||
wish: .init(language: "de", fallbacks: ["en"])
|
||||
)
|
||||
// Die Standardspur — nachvollziehbar statt beliebig.
|
||||
#expect(gewählt?.id == "1")
|
||||
}
|
||||
|
||||
@Test("Gibt es die Sprache nur mit Audiodeskription, ist sie besser als nichts")
|
||||
func audioDescriptionBeatsSilence() {
|
||||
let spuren = [spur(0, "ger", title: "Deutsch (Audiodeskription)")]
|
||||
let gewählt = AudioTrackChooser.choose(from: spuren, wish: .init(language: "de"))
|
||||
#expect(gewählt?.id == "0")
|
||||
}
|
||||
|
||||
@Test("Ohne Tonspuren gibt es nichts zu wählen")
|
||||
func noAudioNoChoice() {
|
||||
let nurBild = [MediaStream(id: "0", kind: .video)]
|
||||
#expect(AudioTrackChooser.choose(from: nurBild, wish: .init()) == nil)
|
||||
}
|
||||
|
||||
@Test("Kommentarspuren werden gemieden")
|
||||
func avoidsCommentary() {
|
||||
let spuren = [
|
||||
spur(0, "ger", title: "Audiokommentar des Regisseurs", isDefault: true),
|
||||
spur(1, "ger", title: "Deutsch"),
|
||||
]
|
||||
let gewählt = AudioTrackChooser.choose(from: spuren, wish: .init(language: "de"))
|
||||
#expect(gewählt?.id == "1")
|
||||
}
|
||||
|
||||
/// „AD" als eigenes Wort, nicht als Teil von „Broadcast".
|
||||
@Test("AD erkennt nur das freistehende Wort")
|
||||
func adOnlyAsWholeWord() {
|
||||
let spuren = [
|
||||
spur(0, "ger", title: "Deutsch Broadcast Mix"),
|
||||
spur(1, "ger", title: "Deutsch AD"),
|
||||
]
|
||||
let gewählt = AudioTrackChooser.choose(from: spuren, wish: .init(language: "de"))
|
||||
#expect(gewählt?.id == "0")
|
||||
}
|
||||
|
||||
@Test("Mehr Kanäle gewinnen bei sonst gleichem Stand")
|
||||
func moreChannelsWin() {
|
||||
let spuren = [spur(0, "ger", title: "Deutsch", channels: 2),
|
||||
spur(1, "ger", title: "Deutsch", channels: 6)]
|
||||
let gewählt = AudioTrackChooser.choose(from: spuren, wish: .init(language: "de"))
|
||||
#expect(gewählt?.id == "1")
|
||||
}
|
||||
|
||||
/// Zweimal dieselbe Datei muss zweimal dasselbe ergeben — sonst ist ein
|
||||
/// Fehler nicht wiederholbar.
|
||||
@Test("Gleichstand entscheidet die Nummer")
|
||||
func tiesAreStable() {
|
||||
let spuren = [spur(1, "ger", title: "Deutsch"), spur(0, "ger", title: "Deutsch")]
|
||||
#expect(AudioTrackChooser.choose(from: spuren, wish: .init(language: "de"))?.id == "0")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user