import { createMockTmdb, type MockTmdb, TMDB_FIXTURES } from "@umt/testkit"; import { afterEach, describe, expect, it } from "vitest"; import { allowedLanguage, allowedLanguageChain, altTitleLanguage, chineseScriptOf, createTmdbClient, fetchRichDetail, textMatchesLanguage, untaggedTextAllowed, } from "./index.ts"; const JA = { originalLanguage: "ja", originCountry: ["JP"] }; const KO = { originalLanguage: "ko", originCountry: ["KR"] }; const TH = { originalLanguage: "th", originCountry: ["TH"] }; const ZH = { originalLanguage: "zh", originCountry: ["CN"] }; const EN = { originalLanguage: "en", originCountry: ["US"] }; describe("多语言保留规则:语言标签", () => { it("简中 / 繁中 / 英文总是保留,规范化大小写", () => { for (const tag of ["zh-CN", "zh-SG", "zh-Hans", "zh-TW", "zh-HK", "zh-MO", "zh-Hant", "zh", "en-US", "en-GB", "en"]) expect(allowedLanguage(tag, EN)).toBe(tag); expect(allowedLanguage("zh-cn", EN)).toBe("zh-CN"); expect(allowedLanguage("zh_hant", EN)).toBe("zh-Hant"); expect(allowedLanguage("EN-au", EN)).toBe("en-AU"); }); it("其它语言只在是作品原语言时保留", () => { for (const tag of ["ru-RU", "pt-BR", "vi-VN", "es-ES", "th-TH", "bg-BG", "he-IL", "fr-FR", "ja-JP", "ko-KR"]) expect(allowedLanguage(tag, EN)).toBeNull(); expect(allowedLanguage("ja-JP", JA)).toBe("ja-JP"); expect(allowedLanguage("ja", JA)).toBe("ja"); expect(allowedLanguage("ko-KR", JA)).toBeNull(); expect(allowedLanguage("ko-KR", KO)).toBe("ko-KR"); expect(allowedLanguage("th-TH", TH)).toBe("th-TH"); // 原语言是中文:中文的其它地区也算原语言;不额外增加别的语言 expect(allowedLanguage("zh-MY", ZH)).toBe("zh-MY"); expect(allowedLanguage("zh-MY", EN)).toBeNull(); expect(allowedLanguage("ja-JP", ZH)).toBeNull(); // 没有原语言信息:只有中英文 expect(allowedLanguage("ja-JP", {})).toBeNull(); expect(allowedLanguage(null, JA)).toBeNull(); expect(allowedLanguage("", JA)).toBeNull(); }); it("语言链按作品收敛:去掉不保留的语言,保持顺序", () => { const chain = ["ja-JP", "zh-CN", "fr-FR", "zh-TW", "en-US"]; expect(allowedLanguageChain(chain, JA)).toEqual(chain.filter((x) => x !== "fr-FR")); expect(allowedLanguageChain(chain, EN)).toEqual(["zh-CN", "zh-TW", "en-US"]); expect(allowedLanguageChain(["fr-FR"], EN)).toEqual([]); }); }); describe("多语言保留规则:文字校验", () => { it("简繁特有字", () => { expect(chineseScriptOf("人生切割术")).toBe("zh-Hans"); expect(chineseScriptOf("人生切割術")).toBe("zh-Hant"); expect(chineseScriptOf("沙丘")).toBeNull(); }); it("各语言的文字", () => { expect(textMatchesLanguage("zh-CN", "奥本海默传")).toBe(true); expect(textMatchesLanguage("zh-CN", "奧本海默傳")).toBe(false); // 明显繁体 expect(textMatchesLanguage("zh-TW", "最後生還者")).toBe(true); expect(textMatchesLanguage("zh-TW", "最后的生还者")).toBe(false); expect(textMatchesLanguage("zh", "最後生還者")).toBe(true); // 不带地区不看简繁 expect(textMatchesLanguage("zh-CN", "Severance CN")).toBe(false); expect(textMatchesLanguage("zh-CN", "進撃の巨人")).toBe(false); expect(textMatchesLanguage("ja-JP", "進撃の巨人")).toBe(true); expect(textMatchesLanguage("ja-JP", "Shingeki no Kyojin")).toBe(false); expect(textMatchesLanguage("ko-KR", "오징어 게임")).toBe(true); expect(textMatchesLanguage("ko-KR", "Squid Game")).toBe(false); expect(textMatchesLanguage("th-TH", "ฉลาดเกมส์โกง")).toBe(true); expect(textMatchesLanguage("th-TH", "Bad Genius")).toBe(false); expect(textMatchesLanguage("en-US", "Bad Genius")).toBe(true); expect(textMatchesLanguage("en-US", "Очень странные дела")).toBe(false); expect(textMatchesLanguage("ru-RU", "Очень странные дела")).toBe(true); expect(textMatchesLanguage("ja-Latn", "Shingeki no Kyojin")).toBe(true); }); }); describe("多语言保留规则:alternative_titles 按地区推断语言", () => { it("CN / SG → 简中,TW / HK / MO → 繁中;文字不符丢弃", () => { expect(altTitleLanguage({ title: "人生切割术", region: "CN" }, EN)).toBe("zh-CN"); expect(altTitleLanguage({ title: "生活割離術", region: "HK" }, EN)).toBe("zh-HK"); expect(altTitleLanguage({ title: "人生切割術", region: "CN" }, EN)).toBeNull(); expect(altTitleLanguage({ title: "Severance CN", region: "CN" }, EN)).toBeNull(); expect(altTitleLanguage({ title: "奧本海默", region: "TW" }, EN)).toBe("zh-TW"); }); it("英语区 → 英文;其它地区只在是原产地区时 → 原语言", () => { expect(altTitleLanguage({ title: "Dune 2", region: "US" }, EN)).toBe("en-US"); expect(altTitleLanguage({ title: "Attack on Titan", region: "GB" }, JA)).toBe("en-GB"); expect(altTitleLanguage({ title: "Дюна 2", region: "RU" }, EN)).toBeNull(); expect(altTitleLanguage({ title: "Duna: Parte Dois", region: "BR" }, EN)).toBeNull(); expect(altTitleLanguage({ title: "進撃の巨人", region: "JP" }, JA)).toBe("ja-JP"); expect(altTitleLanguage({ title: "進撃の巨人", region: "JP" }, EN)).toBeNull(); expect(altTitleLanguage({ title: "오징어 게임", region: "KR" }, KO)).toBe("ko-KR"); expect(altTitleLanguage({ title: "ฉลาดเกมส์โกง", region: "TH" }, TH)).toBe("th-TH"); expect(altTitleLanguage({ title: "Bad Genius", region: "TH" }, TH)).toBeNull(); expect(altTitleLanguage({ title: "流浪地球 2", region: "CN" }, ZH)).toBe("zh-CN"); expect(altTitleLanguage({ title: "The Wandering Earth 2", region: "US" }, ZH)).toBe("en-US"); expect(altTitleLanguage({ title: "无地区", region: null }, ZH)).toBeNull(); }); it("原产地区的罗马字别名记原语言拉丁转写", () => { expect(altTitleLanguage({ title: "Shingeki no Kyojin", region: "JP", type: "Romaji" }, JA)).toBe("ja-Latn"); expect(altTitleLanguage({ title: "Shingeki no Kyojin", region: "JP", type: "" }, JA)).toBeNull(); expect(altTitleLanguage({ title: "Shingeki no Kyojin", region: "FR", type: "Romaji" }, JA)).toBeNull(); }); it("没有语言标记的旧别名按文字判断", () => { expect(untaggedTextAllowed("人生切割术", EN)).toBe(true); expect(untaggedTextAllowed("Severance", EN)).toBe(true); expect(untaggedTextAllowed("Разделение", EN)).toBe(false); expect(untaggedTextAllowed("세브란스", EN)).toBe(false); expect(untaggedTextAllowed("세브란스", KO)).toBe(true); expect(untaggedTextAllowed("ฉลาดเกมส์โกง", TH)).toBe(true); expect(untaggedTextAllowed("2049", EN)).toBe(true); }); }); describe("多语言保留规则:TMDB 详情", () => { let mock: MockTmdb | null = null; afterEach(async () => { await mock?.close(); mock = null; }); it("details() 与富详情只留保留语言的译名与别名(奥本海默的 ja-JP 译名不要)", async () => { mock = await createMockTmdb({ fixtures: TMDB_FIXTURES }); const tmdb = createTmdbClient({ baseUrl: `${mock.url}/3`, apiKey: "test-tmdb-key", retries: 0, timeoutMs: 1000 }); const d = await tmdb.details("movie", 872585); expect(d.aliases).not.toContain("オッペンハイマー"); expect(d.aliases).toEqual(expect.arrayContaining(["奥本海默传", "奧本海默"])); expect(d.titles.find((t) => t.text === "奥本海默传")).toMatchObject({ kind: "alias", lang: "zh-CN" }); const rich = await fetchRichDetail(tmdb, "movie", 872585, ["ja-JP", "zh-CN", "en-US"]); expect(rich.translations.map((t) => t.lang).sort()).toEqual(["en-US", "zh-CN"]); expect(rich.alternativeTitles.map((a) => a.language).sort()).toEqual(["zh-CN", "zh-TW"]); // 请求语言 ja-JP 不在保留范围:语言链收敛,不用 ja-JP 的值兜底 expect(rich.languages).toEqual(["zh-CN", "en-US"]); expect(rich.title).toBe("奥本海默"); expect(rich.titleLanguage).toBe("zh-CN"); }); });