From 6b38e36e4d8c05aff1839c0f473994c64f4a2e3f Mon Sep 17 00:00:00 2001 From: Hein Thant Maung Maung Date: Wed, 26 Nov 2025 09:30:16 +0700 Subject: [PATCH 1/4] feat(synced-lyrics): add simplified/traditional Chinese converter --- package.json | 1 + pnpm-lock.yaml | 9 ++++ src/i18n/resources/en.json | 18 +++++++ src/plugins/synced-lyrics/menu.ts | 52 +++++++++++++++++++ .../renderer/components/PlainLyrics.tsx | 23 ++++++-- .../renderer/components/SyncedLine.tsx | 16 +++++- src/plugins/synced-lyrics/renderer/utils.tsx | 17 ++++++ src/plugins/synced-lyrics/types.ts | 1 + 8 files changed, 130 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 7d2a014a69..a714776c49 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "bgutils-js": "3.2.0", "butterchurn": "3.0.0-beta.5", "butterchurn-presets": "3.0.0-beta.4", + "chinese-conv": "^4.0.0", "color": "5.0.0", "conf": "14.0.0", "custom-electron-prompt": "1.5.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 649e5b18bd..853363db67 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,6 +111,9 @@ importers: butterchurn-presets: specifier: 3.0.0-beta.4 version: 3.0.0-beta.4 + chinese-conv: + specifier: ^4.0.0 + version: 4.0.0 color: specifier: 5.0.0 version: 5.0.0 @@ -1902,6 +1905,10 @@ packages: resolution: {integrity: sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + chinese-conv@4.0.0: + resolution: {integrity: sha512-PVBMzvv6CtX1cubaDXfxYscIdbOAHPuY/E2vnfJIzOACX+xIW4NRKRlNsZVI2p5KxGsXyUp7tVHfvQlqZ4yx/w==} + engines: {node: ^20.19.0 || >=22.12.0} + chownr@2.0.0: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} engines: {node: '>=10'} @@ -6642,6 +6649,8 @@ snapshots: chalk@5.0.1: {} + chinese-conv@4.0.0: {} + chownr@2.0.0: {} chownr@3.0.0: {} diff --git a/src/i18n/resources/en.json b/src/i18n/resources/en.json index 4cf482aa3b..3abf709f3a 100644 --- a/src/i18n/resources/en.json +++ b/src/i18n/resources/en.json @@ -862,6 +862,24 @@ "show-time-codes": { "label": "Show time codes", "tooltip": "Show the time codes next to the lyrics" + }, + "convert-chinese-character": { + "label": "Convert Chinese character", + "submenu": { + "disabled": { + "label": "Disabled", + "tooltip": "Disable Chinese character conversion" + }, + "simplified-to-traditional": { + "label": "Simplified to Traditional", + "tooltip": "Convert Simplified Chinese to Traditional Chinese" + }, + "traditional-to-simplified": { + "label": "Traditional to Simplified", + "tooltip": "Convert Traditional Chinese to Simplified Chinese" + } + }, + "tooltip": "Convert Chinese character to Traditional or Simplified" } }, "name": "Synced Lyrics", diff --git a/src/plugins/synced-lyrics/menu.ts b/src/plugins/synced-lyrics/menu.ts index d1b9e12f41..278999de05 100644 --- a/src/plugins/synced-lyrics/menu.ts +++ b/src/plugins/synced-lyrics/menu.ts @@ -153,6 +153,58 @@ export const menu = async ( }); }, }, + { + label: t('plugins.synced-lyrics.menu.convert-chinese-character.label'), + toolTip: t('plugins.synced-lyrics.menu.convert-chinese-character.tooltip'), + type: 'submenu', + submenu: [ + { + label: t( + 'plugins.synced-lyrics.menu.convert-chinese-character.submenu.disabled.label', + ), + toolTip: t( + 'plugins.synced-lyrics.menu.convert-chinese-character.submenu.disabled.tooltip', + ), + type: 'radio', + checked: config.convertChineseCharacter === 'disabled' || config.convertChineseCharacter === undefined, + click() { + ctx.setConfig({ + convertChineseCharacter: 'disabled', + }); + }, + }, + { + label: t( + 'plugins.synced-lyrics.menu.convert-chinese-character.submenu.simplified-to-traditional.label', + ), + toolTip: t( + 'plugins.synced-lyrics.menu.convert-chinese-character.submenu.simplified-to-traditional.tooltip', + ), + type: 'radio', + checked: config.convertChineseCharacter === 'simplifiedToTraditional', + click() { + ctx.setConfig({ + convertChineseCharacter: 'simplifiedToTraditional', + }); + }, + }, + { + label: t( + 'plugins.synced-lyrics.menu.convert-chinese-character.submenu.traditional-to-simplified.label', + ), + toolTip: t( + 'plugins.synced-lyrics.menu.convert-chinese-character.submenu.traditional-to-simplified.tooltip', + ), + type: 'radio', + checked: config.convertChineseCharacter === 'traditionalToSimplified', + click() { + ctx.setConfig({ + convertChineseCharacter: 'traditionalToSimplified', + }); + }, + }, + ], + }, { label: t('plugins.synced-lyrics.menu.show-time-codes.label'), toolTip: t('plugins.synced-lyrics.menu.show-time-codes.tooltip'), diff --git a/src/plugins/synced-lyrics/renderer/components/PlainLyrics.tsx b/src/plugins/synced-lyrics/renderer/components/PlainLyrics.tsx index 94315949cd..ea83c6d35b 100644 --- a/src/plugins/synced-lyrics/renderer/components/PlainLyrics.tsx +++ b/src/plugins/synced-lyrics/renderer/components/PlainLyrics.tsx @@ -1,6 +1,11 @@ -import { createEffect, createSignal, Show } from 'solid-js'; +import { createEffect, createMemo, createSignal, Show } from 'solid-js'; -import { canonicalize, romanize, simplifyUnicode } from '../utils'; +import { + canonicalize, + convertChineseCharacter, + romanize, + simplifyUnicode, +} from '../utils'; import { config } from '../renderer'; interface PlainLyricsProps { @@ -9,11 +14,19 @@ interface PlainLyricsProps { export const PlainLyrics = (props: PlainLyricsProps) => { const [romanization, setRomanization] = createSignal(''); + const text = createMemo(() => { + let line = props.line; + const convertChineseText = config()?.convertChineseCharacter; + if (convertChineseText && convertChineseText !== 'disabled') { + line = convertChineseCharacter(line, convertChineseText); + } + return line; + }); createEffect(() => { if (!config()?.romanization) return; - const input = canonicalize(props.line); + const input = canonicalize(text()); romanize(input).then((result) => { setRomanization(canonicalize(result)); }); @@ -31,13 +44,13 @@ export const PlainLyrics = (props: PlainLyricsProps) => { > { }; export const SyncedLine = (props: SyncedLineProps) => { - const text = createMemo(() => props.line.text.trim()); + const text = createMemo(() => { + let line = props.line.text; + const convertChineseText = config()?.convertChineseCharacter; + if (convertChineseText && convertChineseText !== 'disabled') { + line = convertChineseCharacter(line, convertChineseText); + } + return line; + }); const [romanization, setRomanization] = createSignal(''); createEffect(() => { diff --git a/src/plugins/synced-lyrics/renderer/utils.tsx b/src/plugins/synced-lyrics/renderer/utils.tsx index 1c6a410bd2..e889d2e6e7 100644 --- a/src/plugins/synced-lyrics/renderer/utils.tsx +++ b/src/plugins/synced-lyrics/renderer/utils.tsx @@ -7,6 +7,7 @@ import * as pinyin from 'tiny-pinyin'; import { romanize as romanizeThaiFrag } from '@dehoist/romanize-thai'; import { lazy } from 'lazy-var'; import { detect } from 'tinyld'; +import { sify, tify } from 'chinese-conv'; import { waitForElement } from '@/utils/wait-for-element'; import { LyricsRenderer, setIsVisible } from './renderer'; @@ -84,6 +85,22 @@ export const canonicalize = (text: string) => { ); }; +export const convertChineseCharacter = ( + text: string, + mode: 'simplifiedToTraditional' | 'traditionalToSimplified', +) => { + if (!hasChinese([text])) return text; + + switch (mode) { + case 'simplifiedToTraditional': + return tify(text); + case 'traditionalToSimplified': + return sify(text); + default: + return text; + } +}; + export const simplifyUnicode = (text?: string) => text ? text diff --git a/src/plugins/synced-lyrics/types.ts b/src/plugins/synced-lyrics/types.ts index dab1edf9ba..3a8803603b 100644 --- a/src/plugins/synced-lyrics/types.ts +++ b/src/plugins/synced-lyrics/types.ts @@ -10,6 +10,7 @@ export type SyncedLyricsPluginConfig = { showLyricsEvenIfInexact: boolean; lineEffect: LineEffect; romanization: boolean; + convertChineseCharacter: 'simplifiedToTraditional' | 'traditionalToSimplified' | 'disabled'; }; export type LineLyricsStatus = 'previous' | 'current' | 'upcoming'; From d16a516831848ba8962d4784d3c4ac8ae121f1c6 Mon Sep 17 00:00:00 2001 From: Hein Thant Maung Maung Date: Wed, 26 Nov 2025 09:32:10 +0700 Subject: [PATCH 2/4] feat(synced-lyrics): improve Chinese romanization to display tones --- package.json | 2 +- pnpm-lock.yaml | 16 ++++++++-------- src/plugins/synced-lyrics/renderer/utils.tsx | 6 ++---- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index a714776c49..f3bd2d56d0 100644 --- a/package.json +++ b/package.json @@ -122,6 +122,7 @@ "node-html-parser": "7.0.1", "node-id3": "0.2.9", "peerjs": "1.5.5", + "pinyin-pro": "^3.27.0", "semver": "7.7.2", "serve": "14.2.5", "socks": "2.8.7", @@ -130,7 +131,6 @@ "solid-js": "1.9.9", "solid-styled-components": "0.28.5", "solid-transition-group": "0.3.0", - "tiny-pinyin": "1.3.2", "tinyld": "1.3.4", "virtua": "0.42.3", "vudio": "2.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 853363db67..b0d26e1a9b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -213,6 +213,9 @@ importers: peerjs: specifier: 1.5.5 version: 1.5.5 + pinyin-pro: + specifier: ^3.27.0 + version: 3.27.0 semver: specifier: 7.7.2 version: 7.7.2 @@ -237,9 +240,6 @@ importers: solid-transition-group: specifier: 0.3.0 version: 0.3.0(solid-js@1.9.9) - tiny-pinyin: - specifier: 1.3.2 - version: 1.3.2 tinyld: specifier: 1.3.4 version: 1.3.4 @@ -3897,6 +3897,9 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + pinyin-pro@3.27.0: + resolution: {integrity: sha512-Osdgjwe7Rm17N2paDMM47yW+jUIUH3+0RGo8QP39ZTLpTaJVDK0T58hOLaMQJbcMmAebVuK2ePunTEVEx1clNQ==} + pixelmatch@5.3.0: resolution: {integrity: sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==} hasBin: true @@ -4479,9 +4482,6 @@ packages: tiny-async-pool@1.3.0: resolution: {integrity: sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==} - tiny-pinyin@1.3.2: - resolution: {integrity: sha512-uHNGu4evFt/8eNLldazeAM1M8JrMc1jshhJJfVRARTN3yT8HEEibofeQ7QETWQ5ISBjd6fKtTVBCC/+mGS6FpA==} - tiny-typed-emitter@2.1.0: resolution: {integrity: sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==} @@ -8861,6 +8861,8 @@ snapshots: picomatch@4.0.3: {} + pinyin-pro@3.27.0: {} + pixelmatch@5.3.0: dependencies: pngjs: 6.0.0 @@ -9491,8 +9493,6 @@ snapshots: dependencies: semver: 5.7.2 - tiny-pinyin@1.3.2: {} - tiny-typed-emitter@2.1.0: {} tinycolor2@1.6.0: {} diff --git a/src/plugins/synced-lyrics/renderer/utils.tsx b/src/plugins/synced-lyrics/renderer/utils.tsx index e889d2e6e7..ab44ca7ea9 100644 --- a/src/plugins/synced-lyrics/renderer/utils.tsx +++ b/src/plugins/synced-lyrics/renderer/utils.tsx @@ -3,7 +3,7 @@ import KuromojiAnalyzer from 'kuroshiro-analyzer-kuromoji'; import Kuroshiro from 'kuroshiro'; import { romanize as esHangulRomanize } from 'es-hangul'; import hanja from 'hanja'; -import * as pinyin from 'tiny-pinyin'; +import { pinyin } from 'pinyin-pro'; import { romanize as romanizeThaiFrag } from '@dehoist/romanize-thai'; import { lazy } from 'lazy-var'; import { detect } from 'tinyld'; @@ -182,9 +182,7 @@ export const romanizeHangul = (line: string) => esHangulRomanize(hanja.translate(line, 'SUBSTITUTION')); export const romanizeChinese = (line: string) => { - return line.replaceAll(/[\u4E00-\u9FFF]+/g, (match) => - pinyin.convertToPinyin(match, ' ', true), - ); + return line.replaceAll(/[\u4E00-\u9FFF]+/g, (match) => pinyin(match)); }; const thaiSegmenter = Intl.Segmenter.supportedLocalesOf('th').includes('th') From 3c98be34ba721dc35bd59f260573c04420e74460 Mon Sep 17 00:00:00 2001 From: Hein Thant Maung Maung Date: Wed, 26 Nov 2025 09:53:58 +0700 Subject: [PATCH 3/4] chore(format): Format codes --- src/menu.ts | 7 ++++++- src/plugins/synced-lyrics/menu.ts | 10 +++++++--- src/plugins/synced-lyrics/types.ts | 5 ++++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/menu.ts b/src/menu.ts index 00f2b5f154..0d63a8e481 100644 --- a/src/menu.ts +++ b/src/menu.ts @@ -481,7 +481,12 @@ export const mainMenuTemplate = async ( availableLanguages .map( (lang): Electron.MenuItemConstructorOptions => ({ - label: `${langResources[lang].translation.language?.name ?? 'Unknown'} (${langResources[lang].translation.language?.['local-name'] ?? 'Unknown'})`, + label: `${ + langResources[lang].translation.language?.name ?? 'Unknown' + } (${ + langResources[lang].translation.language?.['local-name'] ?? + 'Unknown' + })`, type: 'checkbox', checked: (config.get('options.language') ?? 'en') === lang, click() { diff --git a/src/plugins/synced-lyrics/menu.ts b/src/plugins/synced-lyrics/menu.ts index 278999de05..2307cf0c17 100644 --- a/src/plugins/synced-lyrics/menu.ts +++ b/src/plugins/synced-lyrics/menu.ts @@ -37,7 +37,7 @@ export const menu = async ( click() { ctx.setConfig({ preferredProvider: provider }); }, - }) as const, + } as const), ), ], }, @@ -155,7 +155,9 @@ export const menu = async ( }, { label: t('plugins.synced-lyrics.menu.convert-chinese-character.label'), - toolTip: t('plugins.synced-lyrics.menu.convert-chinese-character.tooltip'), + toolTip: t( + 'plugins.synced-lyrics.menu.convert-chinese-character.tooltip', + ), type: 'submenu', submenu: [ { @@ -166,7 +168,9 @@ export const menu = async ( 'plugins.synced-lyrics.menu.convert-chinese-character.submenu.disabled.tooltip', ), type: 'radio', - checked: config.convertChineseCharacter === 'disabled' || config.convertChineseCharacter === undefined, + checked: + config.convertChineseCharacter === 'disabled' || + config.convertChineseCharacter === undefined, click() { ctx.setConfig({ convertChineseCharacter: 'disabled', diff --git a/src/plugins/synced-lyrics/types.ts b/src/plugins/synced-lyrics/types.ts index 3a8803603b..4d824cb871 100644 --- a/src/plugins/synced-lyrics/types.ts +++ b/src/plugins/synced-lyrics/types.ts @@ -10,7 +10,10 @@ export type SyncedLyricsPluginConfig = { showLyricsEvenIfInexact: boolean; lineEffect: LineEffect; romanization: boolean; - convertChineseCharacter: 'simplifiedToTraditional' | 'traditionalToSimplified' | 'disabled'; + convertChineseCharacter: + | 'simplifiedToTraditional' + | 'traditionalToSimplified' + | 'disabled'; }; export type LineLyricsStatus = 'previous' | 'current' | 'upcoming'; From ae9640bfc46019133764734a1950106611b368a1 Mon Sep 17 00:00:00 2001 From: Hein Thant Maung Maung Date: Wed, 26 Nov 2025 09:57:22 +0700 Subject: [PATCH 4/4] Update src/plugins/synced-lyrics/menu.ts Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- src/plugins/synced-lyrics/menu.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/synced-lyrics/menu.ts b/src/plugins/synced-lyrics/menu.ts index 2307cf0c17..9f4fcc6d93 100644 --- a/src/plugins/synced-lyrics/menu.ts +++ b/src/plugins/synced-lyrics/menu.ts @@ -37,7 +37,7 @@ export const menu = async ( click() { ctx.setConfig({ preferredProvider: provider }); }, - } as const), + }) as const, ), ], },