From 2ffd18c5aa0f4b427cd41d33ec62d0f7951c4535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A2=B3=E7=83=A4=E5=85=AB=E7=88=AA=E9=B1=BC?= <68000793+ranhengzhang@users.noreply.github.com> Date: Fri, 9 Jan 2026 02:30:14 +0800 Subject: [PATCH 1/3] Refactor TTML translation handling to clean instead of sort Replaced sorting of TTML translations with cleaning logic. Added a new method to clean unnecessary translations from TTML content. --- src/core/player/LyricManager.ts | 100 ++++++++++++++++++-------------- 1 file changed, 58 insertions(+), 42 deletions(-) diff --git a/src/core/player/LyricManager.ts b/src/core/player/LyricManager.ts index 2e2d20f86..645c45b60 100644 --- a/src/core/player/LyricManager.ts +++ b/src/core/player/LyricManager.ts @@ -416,7 +416,7 @@ class LyricManager { } if (isStale()) return; if (!ttmlContent || typeof ttmlContent !== "string") return; - const sorted = this.sortTTMLTranslations(ttmlContent); + const sorted = this.cleanTTMLTranslations(ttmlContent); const parsed = parseTTML(sorted); const lines = parsed?.lines || []; if (!lines.length) return; @@ -500,7 +500,7 @@ class LyricManager { if (!lyric) return { lrcData: [], yrcData: [] }; // TTML 直接返回 if (format === "ttml") { - const sorted = this.sortTTMLTranslations(lyric); + const sorted = this.cleanTTMLTranslations(lyric); const ttml = parseTTML(sorted); const lines = ttml?.lines || []; statusStore.usingTTMLLyric = true; @@ -534,52 +534,68 @@ class LyricManager { } /** - * 处理 TTML 内容并排序翻译 - * @param ttmlContent 原始 TTML 内容 - * @param translationOrder 翻译排序顺序 - * @returns 排序后的 TTML 内容 - */ - // 此函数应该在 AMLL 的 TTML 解析器支持多语言翻译后删除 - private sortTTMLTranslations( - ttmlContent: string, - translationOrder: string[] = ["zh-CN", "zh-Hans", "zh-TW", "zh-Hant"], + * 清洗 TTML 中不需要的翻译 + * @param ttmlContent 原始 TTML 内容 + * @returns 清洗后的 TTML 内容 + */ + // 当支持 i18n 之后,需要对其中的部分函数进行修改,使其优选逻辑能够根据用户界面语言变化 + private cleanTTMLTranslations( // 一般没有多种音译,故不对音译部分进行清洗,如果需要请另写处理函数 + ttmlContent: string ): string { - // 使用 DOMParser 解析 XML 内容 - const parser = new DOMParser(); - const xmlDoc = parser.parseFromString(ttmlContent, "text/xml"); - - // 查找所有歌词行元素 - const lyricsElements = xmlDoc.querySelectorAll("tt > body > div > p"); - - lyricsElements.forEach((element: Element) => { - // 获取当前歌词行的所有翻译元素 - const translationElements = Array.from(element.children).filter( - (child) => - child.hasAttribute("ttm:role") && child.getAttribute("ttm:role") === "x-translation", - ); + const lang_counter = (ttml_text: string) => { + // 使用正则匹配所有 xml:lang="xx-XX" 格式的字符串 + const langRegex = /(?<=<(span|translation)[^<>]+)xml:lang="([^"]+)"/g; + const matches = ttml_text.matchAll(langRegex); - // 按照指定顺序对翻译进行排序 - // 按照指定顺序对翻译进行排序 - translationElements.sort((a, b) => { - const aLang = (a.getAttribute("xml:lang") || a.getAttribute("lang") || "").toLowerCase(); - const bLang = (b.getAttribute("xml:lang") || b.getAttribute("lang") || "").toLowerCase(); + // 提取匹配结果并去重 + const langSet = new Set(); + for (const match of matches) { + if (match[2]) langSet.add(match[2]); + } - const aIndex = translationOrder.findIndex((lang) => aLang.startsWith(lang.toLowerCase())); - const bIndex = translationOrder.findIndex((lang) => bLang.startsWith(lang.toLowerCase())); + return Array.from(langSet); + } - // 如果找不到指定语言,则放在最后 - return (aIndex === -1 ? Infinity : aIndex) - (bIndex === -1 ? Infinity : bIndex); - }); + const lang_filter = (langs: string[]) : (string | null) => { + if (langs.length <= 1) return null; - // 重新排列翻译元素 - translationElements.forEach((translationElement) => { - element.appendChild(translationElement); // 移动到末尾以实现排序 - }); - }); + if (langs.includes('zh-Hans')) return 'zh-Hans'; + if (langs.includes('zh-CN')) return 'zh-CN'; + if (langs.includes('zh-Hant')) return 'zh-Hant'; + + const major = langs.find(key => key.startsWith('zh')); + if (major) return major; + + return langs[0]; + } + + const ttml_cleaner = (ttml_text: string, major_lang: string | null): string => { + // 如果没有指定主语言,直接返回原文本(或者根据需求返回空) + if (major_lang === null) return ttml_text; + + /** + * 替换逻辑回调函数 + * @param match 完整匹配到的标签字符串 (例如 ...<\/span>) + * @param lang 正则中第一个捕获组匹配到的语言代码 (例如 "ja-JP") + */ + const replacer = (match: string, lang: string) => (lang === major_lang ? match : ""); + + if (ttml_text.indexOf("iTunesMetadata") !== -1) { + const translationRegex = /]+xml:lang="([^"]+)"[^>]*>[\s\S]*?<\/translation>/g; + + return ttml_text.replace(translationRegex, replacer); + } else { + const spanRegex = /]+xml:lang="([^"]+)"[^>]*>[\s\S]*?<\/span>/g; + + return ttml_text.replace(spanRegex, replacer); + } + } - // 序列化回字符串 - const serializer = new XMLSerializer(); - return serializer.serializeToString(xmlDoc); + const context_lang = lang_counter(ttmlContent); + const major = lang_filter(context_lang); + const cleaned_ttml = ttml_cleaner(ttmlContent, major); + + return cleaned_ttml; } /** From 011d1f07e73d4c16db3201f3ccd1d596458df8ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A2=B3=E7=83=A4=E5=85=AB=E7=88=AA=E9=B1=BC?= <68000793+ranhengzhang@users.noreply.github.com> Date: Fri, 9 Jan 2026 03:01:20 +0800 Subject: [PATCH 2/3] Refactor lyric text replacement logic Consolidate regex replacements for translation and span tags. --- src/core/player/LyricManager.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/core/player/LyricManager.ts b/src/core/player/LyricManager.ts index 645c45b60..c68d1dff0 100644 --- a/src/core/player/LyricManager.ts +++ b/src/core/player/LyricManager.ts @@ -579,16 +579,9 @@ class LyricManager { * @param lang 正则中第一个捕获组匹配到的语言代码 (例如 "ja-JP") */ const replacer = (match: string, lang: string) => (lang === major_lang ? match : ""); - - if (ttml_text.indexOf("iTunesMetadata") !== -1) { - const translationRegex = /]+xml:lang="([^"]+)"[^>]*>[\s\S]*?<\/translation>/g; - - return ttml_text.replace(translationRegex, replacer); - } else { - const spanRegex = /]+xml:lang="([^"]+)"[^>]*>[\s\S]*?<\/span>/g; - - return ttml_text.replace(spanRegex, replacer); - } + const translationRegex = /]+xml:lang="([^"]+)"[^>]*>[\s\S]*?<\/translation>/g; + const spanRegex = /]+xml:lang="([^" ]+)"[^>]*>[\s\S]*?<\/span>/g; + return ttml_text.replace(translationRegex, replacer).replace(spanRegex, replacer); } const context_lang = lang_counter(ttmlContent); From e32663d20766f3b3e92bb991f53bbb5e3317e516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A2=B3=E7=83=A4=E5=85=AB=E7=88=AA=E9=B1=BC?= Date: Fri, 9 Jan 2026 18:21:00 +0800 Subject: [PATCH 3/3] refactor: improve TTML translation cleaning logic - [Refactor] Enhance the logic for cleaning unnecessary translations in TTML content. - [Fix] Adjust parameter handling and improve language matching functionality. --- src/core/player/LyricManager.ts | 51 +++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/src/core/player/LyricManager.ts b/src/core/player/LyricManager.ts index c68d1dff0..5de40fb24 100644 --- a/src/core/player/LyricManager.ts +++ b/src/core/player/LyricManager.ts @@ -534,13 +534,14 @@ class LyricManager { } /** - * 清洗 TTML 中不需要的翻译 - * @param ttmlContent 原始 TTML 内容 - * @returns 清洗后的 TTML 内容 - */ + * 清洗 TTML 中不需要的翻译 + * @param ttmlContent 原始 TTML 内容 + * @returns 清洗后的 TTML 内容 + */ // 当支持 i18n 之后,需要对其中的部分函数进行修改,使其优选逻辑能够根据用户界面语言变化 - private cleanTTMLTranslations( // 一般没有多种音译,故不对音译部分进行清洗,如果需要请另写处理函数 - ttmlContent: string + private cleanTTMLTranslations( + // 一般没有多种音译,故不对音译部分进行清洗,如果需要请另写处理函数 + ttmlContent: string, ): string { const lang_counter = (ttml_text: string) => { // 使用正则匹配所有 xml:lang="xx-XX" 格式的字符串 @@ -554,40 +555,52 @@ class LyricManager { } return Array.from(langSet); - } + }; - const lang_filter = (langs: string[]) : (string | null) => { + const lang_filter = (langs: string[]): string | null => { if (langs.length <= 1) return null; - if (langs.includes('zh-Hans')) return 'zh-Hans'; - if (langs.includes('zh-CN')) return 'zh-CN'; - if (langs.includes('zh-Hant')) return 'zh-Hant'; + const lang_matcher = (target: string) => { + return langs.find((lang) => { + try { + return new Intl.Locale(lang).maximize().script === target; + } catch { + return false; + } + }); + }; + + const hans_matched = lang_matcher("Hans"); + if (hans_matched) return hans_matched; - const major = langs.find(key => key.startsWith('zh')); + const hant_matched = lang_matcher("Hant"); + if (hant_matched) return hant_matched; + + const major = langs.find((key) => key.startsWith("zh")); if (major) return major; return langs[0]; - } + }; const ttml_cleaner = (ttml_text: string, major_lang: string | null): string => { // 如果没有指定主语言,直接返回原文本(或者根据需求返回空) if (major_lang === null) return ttml_text; /** - * 替换逻辑回调函数 - * @param match 完整匹配到的标签字符串 (例如 ...<\/span>) - * @param lang 正则中第一个捕获组匹配到的语言代码 (例如 "ja-JP") - */ + * 替换逻辑回调函数 + * @param match 完整匹配到的标签字符串 (例如 ...<\/span>) + * @param lang 正则中第一个捕获组匹配到的语言代码 (例如 "ja-JP") + */ const replacer = (match: string, lang: string) => (lang === major_lang ? match : ""); const translationRegex = /]+xml:lang="([^"]+)"[^>]*>[\s\S]*?<\/translation>/g; const spanRegex = /]+xml:lang="([^" ]+)"[^>]*>[\s\S]*?<\/span>/g; return ttml_text.replace(translationRegex, replacer).replace(spanRegex, replacer); - } + }; const context_lang = lang_counter(ttmlContent); const major = lang_filter(context_lang); const cleaned_ttml = ttml_cleaner(ttmlContent, major); - + return cleaned_ttml; }