77 lines
2.8 KiB
TypeScript
77 lines
2.8 KiB
TypeScript
import type { LyricLine } from '@applemusic-like-lyrics/lyric'
|
|
|
|
/** 日语假名:平假名 + 片假名 + 半角假名 + 促音/长音符号 */
|
|
const KANA_RE = /[\p{Script=Hiragana}\p{Script=Katakana}\u30FC\uFF66-\uFF9F]/u
|
|
|
|
/** 韩文:谚文音节 + 谚文字母 + 谚文兼容字母 */
|
|
const HANGUL_RE = /[\p{Script=Hangul}\u3130-\u318F]/u
|
|
|
|
/** 中日韩统一表意文字(含扩展区) */
|
|
const HAN_RE = /\p{Script=Han}/u
|
|
|
|
/** 拉丁字母;数字与标点不能作为英文判断依据 */
|
|
const LATIN_RE = /\p{Script=Latin}/u
|
|
|
|
type LanguageSourceLine = Pick<LyricLine, 'words'> & Partial<Pick<LyricLine, 'translatedLyric'>>
|
|
|
|
/** 判断是否有实质内容的翻译歌词 */
|
|
const hasTranslation = (line: LanguageSourceLine): boolean =>
|
|
(line.translatedLyric ?? '').trim().length > 0
|
|
|
|
/**
|
|
* 基于整首歌词上下文推断每行语言,返回与行数组对齐的 BCP 47 语言标签数组
|
|
*
|
|
* Han 脚本无法独立区分中日韩;
|
|
* - 同一首歌词出现假名时,通常将纯汉字行视为日语;
|
|
* - 同一首歌词出现谚文时,通常将纯汉字行视为韩语;
|
|
* - CJK 混合启发式规则:若所有包含假名/谚文的行均有翻译,
|
|
* 则认定全为汉字且无翻译的行为中文,以此区分双语混合歌词;
|
|
* - 拉丁文字使用 BCP 47 的 und-Latn,避免误标为英语;
|
|
* - 无法判断时返回 undefined(不设置 lang 属性)。
|
|
*
|
|
* @param lines 已解析的整首歌词
|
|
*/
|
|
export const detectLyricLanguages = (
|
|
lines: ReadonlyArray<LanguageSourceLine>
|
|
): Array<string | undefined> => {
|
|
const lineContents = lines.map((line) => line.words.map((word) => word.word).join(''))
|
|
|
|
let hasKana = false
|
|
let hasHangul = false
|
|
let kanaUntranslatedCount = 0
|
|
let hangulUntranslatedCount = 0
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i]
|
|
const content = lineContents[i] ?? ''
|
|
const isTranslated = line ? hasTranslation(line) : false
|
|
|
|
if (KANA_RE.test(content)) {
|
|
hasKana = true
|
|
if (!isTranslated) kanaUntranslatedCount++
|
|
}
|
|
if (HANGUL_RE.test(content)) {
|
|
hasHangul = true
|
|
if (!isTranslated) hangulUntranslatedCount++
|
|
}
|
|
}
|
|
|
|
const allKanaTranslated = hasKana && kanaUntranslatedCount === 0
|
|
const allHangulTranslated = hasHangul && hangulUntranslatedCount === 0
|
|
|
|
return lines.map((line, i) => {
|
|
const content = lineContents[i] ?? ''
|
|
const isTranslated = hasTranslation(line)
|
|
|
|
if (KANA_RE.test(content)) return 'ja'
|
|
if (HANGUL_RE.test(content)) return 'ko'
|
|
if (HAN_RE.test(content)) {
|
|
if (hasKana) return allKanaTranslated && !isTranslated ? 'zh-CN' : 'ja'
|
|
if (hasHangul) return allHangulTranslated && !isTranslated ? 'zh-CN' : 'ko'
|
|
return 'zh-CN'
|
|
}
|
|
if (LATIN_RE.test(content)) return 'und-Latn'
|
|
return undefined
|
|
})
|
|
}
|