feat: implement locale switching between Simplified and Traditional Chinese with opencc-js

This commit is contained in:
kuekhaoyang
2026-03-01 13:22:06 +08:00
parent c95d7d5d95
commit 1e14948225
16 changed files with 234 additions and 74 deletions
+2
View File
@@ -13,6 +13,7 @@ import { siteConfig } from "@/lib/config/site-config";
import { AdKeywordsInjector } from "@/components/AdKeywordsInjector";
import { BackToTop } from "@/components/ui/BackToTop";
import { ScrollPositionManager } from "@/components/ScrollPositionManager";
import { LocaleProvider } from "@/components/LocaleProvider";
import fs from 'fs';
import path from 'path';
@@ -101,6 +102,7 @@ export default function RootLayout({
<ThemeProvider>
{/* 加入自动同步组件,它会在后台默默工作,我们放在 ThemeProvider 内部的最前面 */}
<AutoSync />
<LocaleProvider />
<TVProvider>
<TVNavigationInitializer />
+1
View File
@@ -209,6 +209,7 @@ function PlayerContent() {
latency: match.latency,
pic: match.vod_pic,
typeName: match.type_name,
remarks: match.vod_remarks,
});
// Update state incrementally
setDiscoveredSources([...found]);
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { settingsStore, getDefaultPremiumSources, type SortOption, type SearchDisplayMode, type ProxyMode } from '@/lib/store/settings-store';
import { settingsStore, getDefaultPremiumSources, type SortOption, type SearchDisplayMode, type ProxyMode, type LocaleOption } from '@/lib/store/settings-store';
import { premiumModeSettingsStore } from '@/lib/store/premium-mode-settings';
import type { VideoSource } from '@/lib/types';
@@ -15,6 +15,7 @@ export function usePremiumSettingsPage() {
const [fullscreenType, setFullscreenType] = useState<'auto' | 'native' | 'window'>('auto');
const [proxyMode, setProxyMode] = useState<ProxyMode>('retry');
const [rememberScrollPosition, setRememberScrollPosition] = useState(true);
const [locale, setLocale] = useState<LocaleOption>('zh-CN');
// Danmaku settings
const [danmakuApiUrl, setDanmakuApiUrl] = useState('');
@@ -26,6 +27,7 @@ export function usePremiumSettingsPage() {
// Sources come from main settings store
const settings = settingsStore.getSettings();
setPremiumSources(settings.premiumSources || []);
setLocale(settings.locale);
// Mode-specific settings come from premium mode settings store
const modeSettings = premiumModeSettingsStore.getSettings();
@@ -105,6 +107,13 @@ export function usePremiumSettingsPage() {
savePremiumModeSetting({ rememberScrollPosition: enabled });
};
const handleLocaleChange = (newLocale: LocaleOption) => {
setLocale(newLocale);
// Locale is a global setting, save to main store
const currentSettings = settingsStore.getSettings();
settingsStore.saveSettings({ ...currentSettings, locale: newLocale });
};
// --- Danmaku settings handlers ---
const handleDanmakuApiUrlChange = (url: string) => {
@@ -151,6 +160,8 @@ export function usePremiumSettingsPage() {
handleFullscreenTypeChange,
handleProxyModeChange,
handleRememberScrollPositionChange,
locale,
handleLocaleChange,
// Danmaku settings
danmakuApiUrl,
handleDanmakuApiUrlChange,
+4
View File
@@ -33,6 +33,8 @@ export default function PremiumSettingsPage() {
handleFullscreenTypeChange,
handleProxyModeChange,
handleRememberScrollPositionChange,
locale,
handleLocaleChange,
// Danmaku settings
danmakuApiUrl,
handleDanmakuApiUrlChange,
@@ -93,6 +95,8 @@ export default function PremiumSettingsPage() {
onRealtimeLatencyChange={handleRealtimeLatencyChange}
onSearchDisplayModeChange={handleSearchDisplayModeChange}
onRememberScrollPositionChange={handleRememberScrollPositionChange}
locale={locale}
onLocaleChange={handleLocaleChange}
/>
{/* Premium Source Management */}
+14 -1
View File
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { settingsStore, getDefaultSources, type SortOption, type SearchDisplayMode, type ProxyMode } from '@/lib/store/settings-store';
import { settingsStore, getDefaultSources, type SortOption, type SearchDisplayMode, type ProxyMode, type LocaleOption } from '@/lib/store/settings-store';
import type { VideoSource, SourceSubscription } from '@/lib/types';
import {
type ImportResult,
@@ -25,6 +25,7 @@ export function useSettingsPage() {
const [fullscreenType, setFullscreenType] = useState<'auto' | 'native' | 'window'>('auto');
const [proxyMode, setProxyMode] = useState<ProxyMode>('retry');
const [rememberScrollPosition, setRememberScrollPosition] = useState(true);
const [locale, setLocale] = useState<LocaleOption>('zh-CN');
// Danmaku settings
const [danmakuApiUrl, setDanmakuApiUrl] = useState('');
@@ -42,6 +43,7 @@ export function useSettingsPage() {
setFullscreenType(settings.fullscreenType);
setProxyMode(settings.proxyMode);
setRememberScrollPosition(settings.rememberScrollPosition);
setLocale(settings.locale);
setDanmakuApiUrl(settings.danmakuApiUrl);
setDanmakuOpacity(settings.danmakuOpacity);
setDanmakuFontSize(settings.danmakuFontSize);
@@ -256,6 +258,15 @@ export function useSettingsPage() {
});
};
const handleLocaleChange = (newLocale: LocaleOption) => {
setLocale(newLocale);
const currentSettings = settingsStore.getSettings();
settingsStore.saveSettings({
...currentSettings,
locale: newLocale,
});
};
const handleDanmakuApiUrlChange = (url: string) => {
setDanmakuApiUrl(url);
const currentSettings = settingsStore.getSettings();
@@ -343,6 +354,8 @@ export function useSettingsPage() {
handleProxyModeChange,
rememberScrollPosition,
handleRememberScrollPositionChange,
locale,
handleLocaleChange,
danmakuApiUrl,
handleDanmakuApiUrlChange,
danmakuOpacity,
+4
View File
@@ -56,6 +56,8 @@ export default function SettingsPage() {
handleProxyModeChange,
rememberScrollPosition,
handleRememberScrollPositionChange,
locale,
handleLocaleChange,
danmakuApiUrl,
handleDanmakuApiUrlChange,
danmakuOpacity,
@@ -102,6 +104,8 @@ export default function SettingsPage() {
onRealtimeLatencyChange={handleRealtimeLatencyChange}
onSearchDisplayModeChange={handleSearchDisplayModeChange}
onRememberScrollPositionChange={handleRememberScrollPositionChange}
locale={locale}
onLocaleChange={handleLocaleChange}
/>
{/* Per-User Source Settings (visible to all logged-in users) */}
+65
View File
@@ -0,0 +1,65 @@
'use client';
/**
* LocaleProvider - Converts the entire UI between Simplified and Traditional Chinese
* using opencc-js DOM-level conversion (MutationObserver-based).
*/
import { useEffect, useState } from 'react';
import { settingsStore, type LocaleOption } from '@/lib/store/settings-store';
export function LocaleProvider() {
const [locale, setLocale] = useState<LocaleOption>('zh-CN');
// Listen for settings changes
useEffect(() => {
const settings = settingsStore.getSettings();
setLocale(settings.locale);
const unsub = settingsStore.subscribe(() => {
const updated = settingsStore.getSettings();
setLocale(updated.locale);
});
return unsub;
}, []);
// Apply DOM-level conversion when locale changes
useEffect(() => {
if (locale !== 'zh-TW') {
// Reset to original (Simplified) by reloading lang attribute
document.documentElement.lang = 'zh-CN';
return;
}
let cleanup: (() => void) | undefined;
(async () => {
try {
const OpenCC = await import('opencc-js');
const converter = OpenCC.Converter({ from: 'cn', to: 'tw' });
// HTMLConverter converts all text nodes and observes DOM changes
OpenCC.HTMLConverter(converter, document.documentElement, 'zh-CN', 'zh-TW');
document.documentElement.lang = 'zh-TW';
// The HTMLConverter sets up a MutationObserver internally.
// To clean up, we'd need to reload or re-convert. Store a flag.
cleanup = () => {
// Reverse conversion on cleanup
const reverseConverter = OpenCC.Converter({ from: 'tw', to: 'cn' });
OpenCC.HTMLConverter(reverseConverter, document.documentElement, 'zh-TW', 'zh-CN');
document.documentElement.lang = 'zh-CN';
};
} catch (err) {
console.warn('[LocaleProvider] Failed to load opencc-js:', err);
}
})();
return () => {
cleanup?.();
};
}, [locale]);
return null;
}
+26 -2
View File
@@ -9,6 +9,7 @@ import { LatencyBadge } from '@/components/ui/LatencyBadge';
import { Button } from '@/components/ui/Button';
import { useKeyboardNavigation } from '@/lib/hooks/useKeyboardNavigation';
import { settingsStore } from '@/lib/store/settings-store';
import { extractQualityLabel } from '@/lib/utils/video';
interface Episode {
name?: string;
@@ -22,6 +23,7 @@ export interface SourceInfo {
latency?: number;
pic?: string;
typeName?: string;
remarks?: string;
}
interface EpisodeListProps {
@@ -317,9 +319,20 @@ export function EpisodeList({
</div>
)}
<div className="flex-1 min-w-0">
<div className="font-medium text-sm truncate">
<div className="font-medium text-sm truncate flex items-center gap-1.5">
{source.sourceName || source.source}
{(() => {
const qb = extractQualityLabel(source.remarks);
return qb ? (
<span className={`inline-flex items-center px-1 py-0 rounded text-[9px] font-bold text-white ${qb.color}`}>
{qb.label}
</span>
) : null;
})()}
</div>
{source.remarks && !extractQualityLabel(source.remarks) && (
<div className="text-[10px] text-[var(--text-color-secondary)] truncate mt-0.5">{source.remarks}</div>
)}
{latency !== undefined && (
<div className="mt-0.5">
<LatencyBadge latency={latency} />
@@ -386,9 +399,20 @@ export function EpisodeList({
</div>
)}
<div className="flex-1 min-w-0">
<div className="font-medium text-sm truncate">
<div className="font-medium text-sm truncate flex items-center gap-1.5">
{source.sourceName || source.source}
{(() => {
const qb = extractQualityLabel(source.remarks);
return qb ? (
<span className={`inline-flex items-center px-1 py-0 rounded text-[9px] font-bold text-white ${qb.color}`}>
{qb.label}
</span>
) : null;
})()}
</div>
{source.remarks && !extractQualityLabel(source.remarks) && (
<div className="text-[10px] text-[var(--text-color-secondary)] truncate mt-0.5">{source.remarks}</div>
)}
{latency !== undefined && (
<div className="mt-0.5">
<LatencyBadge latency={latency} />
+1
View File
@@ -67,6 +67,7 @@ export const VideoGroupCard = memo<VideoGroupCardProps>(({
latency: v.latency,
pic: v.vod_pic,
typeName: v.type_name,
remarks: v.vod_remarks,
}));
params.set('groupedSources', JSON.stringify(groupData));
}
+35 -1
View File
@@ -5,25 +5,29 @@
* Following Liquid Glass design system
*/
import { type SearchDisplayMode } from '@/lib/store/settings-store';
import { type SearchDisplayMode, type LocaleOption } from '@/lib/store/settings-store';
import { Switch } from '@/components/ui/Switch';
interface DisplaySettingsProps {
realtimeLatency: boolean;
searchDisplayMode: SearchDisplayMode;
rememberScrollPosition: boolean;
locale: LocaleOption;
onRealtimeLatencyChange: (enabled: boolean) => void;
onSearchDisplayModeChange: (mode: SearchDisplayMode) => void;
onRememberScrollPositionChange: (enabled: boolean) => void;
onLocaleChange: (locale: LocaleOption) => void;
}
export function DisplaySettings({
realtimeLatency,
searchDisplayMode,
rememberScrollPosition,
locale,
onRealtimeLatencyChange,
onSearchDisplayModeChange,
onRememberScrollPositionChange,
onLocaleChange,
}: DisplaySettingsProps) {
return (
<div className="bg-[var(--glass-bg)] border border-[var(--glass-border)] rounded-[var(--radius-2xl)] shadow-[var(--shadow-sm)] p-6 mb-6">
@@ -92,6 +96,36 @@ export function DisplaySettings({
</button>
</div>
</div>
{/* Locale / Language Toggle */}
<div className="mt-6">
<h3 className="font-medium text-[var(--text-color)] mb-2"></h3>
<p className="text-sm text-[var(--text-color-secondary)] mb-4">
/
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<button
onClick={() => onLocaleChange('zh-CN')}
className={`px-4 py-3 rounded-[var(--radius-2xl)] border text-left font-medium transition-all duration-200 cursor-pointer ${locale === 'zh-CN'
? 'bg-[var(--accent-color)] border-[var(--accent-color)] text-white shadow-[0_4px_12px_rgba(var(--accent-color-rgb),0.3)]'
: 'bg-[var(--glass-bg)] border-[var(--glass-border)] text-[var(--text-color)] hover:bg-[color-mix(in_srgb,var(--accent-color)_10%,transparent)]'
}`}
>
<div className="font-semibold"></div>
<div className="text-sm opacity-80 mt-1">使</div>
</button>
<button
onClick={() => onLocaleChange('zh-TW')}
className={`px-4 py-3 rounded-[var(--radius-2xl)] border text-left font-medium transition-all duration-200 cursor-pointer ${locale === 'zh-TW'
? 'bg-[var(--accent-color)] border-[var(--accent-color)] text-white shadow-[0_4px_12px_rgba(var(--accent-color-rgb),0.3)]'
: 'bg-[var(--glass-bg)] border-[var(--glass-border)] text-[var(--text-color)] hover:bg-[color-mix(in_srgb,var(--accent-color)_10%,transparent)]'
}`}
>
<div className="font-semibold"></div>
<div className="text-sm opacity-80 mt-1">使</div>
</button>
</div>
</div>
</div>
);
}
+5
View File
@@ -7,6 +7,8 @@ import { DEFAULT_SOURCES } from '@/lib/api/default-sources';
import { PREMIUM_SOURCES } from '@/lib/api/premium-sources';
import { createSubscription } from '@/lib/utils/source-import-utils';
export type LocaleOption = 'zh-CN' | 'zh-TW';
export type SortOption =
| 'default'
| 'relevance'
@@ -52,6 +54,7 @@ export interface AppSettings {
danmakuOpacity: number; // 0.1 - 1.0
danmakuFontSize: number; // px
danmakuDisplayArea: number; // 0.25 | 0.5 | 0.75 | 1.0
locale: LocaleOption; // 'zh-CN' (Simplified) or 'zh-TW' (Traditional)
}
import { exportSettings, importSettings, SEARCH_HISTORY_KEY, WATCH_HISTORY_KEY } from './settings-helpers';
@@ -130,6 +133,7 @@ function getDefaultAppSettings(): AppSettings {
danmakuOpacity: 0.7,
danmakuFontSize: 20,
danmakuDisplayArea: 0.5,
locale: 'zh-CN',
};
}
@@ -212,6 +216,7 @@ export const settingsStore = {
danmakuOpacity: typeof parsed.danmakuOpacity === 'number' ? parsed.danmakuOpacity : 0.7,
danmakuFontSize: typeof parsed.danmakuFontSize === 'number' ? parsed.danmakuFontSize : 20,
danmakuDisplayArea: typeof parsed.danmakuDisplayArea === 'number' ? parsed.danmakuDisplayArea : 0.5,
locale: parsed.locale === 'zh-TW' ? 'zh-TW' : 'zh-CN',
};
} catch {
// Even if localStorage fails, we should return defaults + ENV subscriptions
+27 -65
View File
@@ -1,77 +1,39 @@
/**
* Traditional Chinese to Simplified Chinese conversion table.
* Contains common character mappings for search purposes.
* Chinese Traditional Simplified conversion using opencc-js.
* Provides comprehensive, dictionary-based conversion (not a fixed mapping).
*/
const T2S_MAP: Record<string, string> = {
'愛': '爱', '礙': '碍', '闇': '暗', '罷': '罢', '備': '备', '貝': '贝', '筆': '笔', '畢': '毕', '邊': '边',
'變': '变', '標': '标', '錶': '表', '別': '别', '賓': '宾', '補': '补', '佈': '布', '參': '参', '殘': '残',
'蠶': '蚕', '倉': '仓', '層': '层', '產': '产', '長': '长', '場': '场', '廠': '厂', '車': '车', '陳': '陈',
'稱': '称', '誠': '诚', '遲': '迟', '衝': '冲', '醜': '丑', '處': '处', '觸': '触', '辭': '辞', '從': '从',
'達': '达', '帶': '带', '當': '当', '黨': '党', '導': '导', '燈': '灯', '敵': '敌', '點': '点', '電': '电',
'東': '东', '動': '动', '獨': '独', '對': '对', '噸': '吨', '奪': '夺', '發': '发', '範': '范', '飛': '飞',
'費': '费', '奮': '奋', '豐': '丰', '風': '风', '鳳': '', '復': '复', '負': '负', '蓋': '盖', '幹': '干',
'鋼': '钢', '個': '个', '給': '给', '鞏': '巩', '貢': '贡', '構': '构', '購': '购', '穀': '谷', '顧': '顾',
'關': '关', '觀': '观', '廣': '广', '歸': '归', '龜': '龟', '國': '国', '過': '过', '華': '华', '畫': '画',
'話': '话', '壞': '坏', '歡': '欢', '環': '环', '還': '还', '匯': '汇', '會': '会', '護': '护', '壺': '壶',
'滬': '沪', '劃': '划', '懷': '怀', '換': '换', '黃': '黄', '繪': '绘',
'惠': '惠', '獲': '获', '機': '机', '積': '积', '極': '极', '際': '际', '濟': '济', '計': '计', '記': '记',
'繼': '继', '紀': '纪', '夾': '夹', '價': '价', '駕': '驾', '堅': '坚', '間': '间', '簡': '简',
'見': '见', '劍': '剑', '漸': '渐', '將': '将', '獎': '', '講': '讲', '醬': '酱', '節': '节', '潔': '洁',
'結': '结', '進': '进', '盡': '尽', '經': '经', '驚': '惊', '競': '竞', '舊': '旧', '據': '据', '劇': '剧',
'軍': '军', '開': '开', '凱': '凯', '殼': '壳', '課': '课', '懇': '恳', '誇': '夸', '塊': '块', '寬': '宽',
'況': '况', '礦': '矿', '虧': '亏', '來': '来', '賴': '赖', '藍': '蓝', '蘭': '兰', '攔': '拦', '覽': '览',
'懶': '懒', '爛': '烂', '撈': '捞', '勞': '劳', '樂': '乐', '類': '类', '離': '离', '裡': '里', '禮': '礼',
'歷': '历', '勵': '励', '聯': '联', '練': '练', '糧': '粮', '兩': '两', '遼': '辽', '療': '疗', '獵': '猎',
'臨': '临', '鄰': '邻', '靈': '灵', '領': '领', '劉': '刘', '龍': '龙', '樓': '楼', '陸': '陆', '錄': '录',
'慮': '虑', '輪': '轮', '論': '论', '羅': '罗', '駱': '骆', '馬': '马', '買': '买', '賣': '卖', '麥': '麦',
'滿': '满', '貓': '猫', '貿': '贸', '門': '门', '們': '们', '夢': '梦', '滅': '灭', '廟': '庙',
'難': '难', '腦': '脑', '鬧': '闹', '釀': '酿', '鳥': '鸟', '聶': '聂', '寧': '宁', '農': '农', '歐': '欧',
'盤': '盘', '龐': '庞', '賠': '赔', '噴': '喷', '鵬': '鹏', '騙': '骗', '蘋': '苹', '評': '评', '鋪': '铺',
'樸': '朴', '齊': '齐', '氣': '气', '遷': '迁', '錢': '钱', '潛': '潜', '淺': '浅', '搶': '抢', '親': '亲',
'輕': '轻', '請': '请', '慶': '庆', '瓊': '琼', '區': '区', '權': '权', '勸': '劝', '確': '确', '讓': '让',
'擾': '扰', '熱': '热', '認': '认', '榮': '荣', '軟': '软', '銳': '锐', '賽': '赛', '傘': '伞', '喪': '丧',
'殺': '杀', '曬': '晒', '傷': '伤', '賞': '赏', '燒': '烧', '設': '设', '審': '审', '聲': '声', '勝': '胜',
'師': '师', '實': '实', '時': '时', '識': '识', '適': '适', '勢': '势', '釋': '释', '壽': '寿', '書': '书',
'術': '术', '樹': '树', '雙': '双', '誰': '谁', '順': '顺', '說': '说', '絲': '丝', '鬆': '松', '蘇': '苏',
'隨': '随', '歲': '岁', '損': '损', '鎖': '锁', '態': '态', '談': '谈', '嘆': '叹', '湯': '汤', '條': '条',
'鐵': '铁', '聽': '听', '統': '统', '圖': '图', '團': '团', '萬': '万', '網': '网', '衛': '卫', '穩': '稳',
'問': '问', '無': '无', '務': '务', '霧': '雾', '習': '习', '戲': '戏', '細': '细', '蝦': '虾', '險': '险',
'現': '现', '獻': '献', '鄉': '乡', '響': '响', '項': '项', '協': '协', '寫': '写', '興': '兴', '選': '选',
'學': '学', '壓': '压', '鴨': '鸭', '鹽': '盐', '嚴': '严', '顏': '颜', '陽': '阳', '養': '养', '樣': '样',
'搖': '摇', '葉': '叶', '業': '业', '醫': '医', '義': '义', '藝': '艺', '億': '亿', '議': '议', '陰': '阴',
'應': '应', '營': '营', '優': '优', '郵': '邮', '與': '与', '語': '语', '預': '预', '員': '员', '園': '园',
'遠': '远', '願': '愿', '閱': '阅', '運': '运', '雜': '杂', '災': '灾', '贊': '赞', '髒': '脏', '棗': '枣',
'責': '责', '戰': '战', '張': '张', '趙': '赵', '鎮': '镇', '爭': '争', '鄭': '郑', '證': '证', '織': '织',
'質': '质', '種': '种', '眾': '众', '鑄': '铸', '專': '专', '莊': '庄', '裝': '装', '準': '准', '資': '资',
'總': '总', '組': '组', '鑽': '钻', '嘗': '尝', '遞': '递', '豬': '猪', '傳': '传', '雲': '云',
'訂': '订', '檔': '档', '調': '调', '詞': '词', '釘': '钉', '鍛': '锻', '隊': '队', '噁': '恶', '額': '额',
'複': '复', '鍋': '锅', '號': '号', '鷄': '鸡', '幾': '几', '驗': '验', '薑': '姜', '膠': '胶', '階': '阶',
'腳': '脚', '較': '较', '僅': '仅', '緊': '紧', '決': '决', '絕': '绝', '覺': '觉', '擴': '扩', '闊': '阔',
'蠟': '蜡', '麗': '丽', '鏈': '链', '憐': '怜', '嶺': '岭', '媽': '妈', '瑪': '玛', '罵': '骂', '邁': '迈',
'麼': '么', '黴': '霉', '謎': '谜', '鳴': '鸣', '謀': '谋', '納': '纳', '惱': '恼', '擬': '拟', '紐': '纽',
'濃': '浓', '瘧': '疟', '拋': '抛', '貧': '贫', '頻': '频', '憑': '凭', '籤': '签', '強': '强', '竊': '窃',
'傾': '倾', '窮': '穷', '趨': '趋', '繞': '绕', '紉': '纫', '鏽': '锈',
};
import * as OpenCC from 'opencc-js';
let t2sConverter: ((text: string) => string) | null = null;
let s2tConverter: ((text: string) => string) | null = null;
function getT2SConverter() {
if (!t2sConverter) {
t2sConverter = OpenCC.Converter({ from: 'tw', to: 'cn' });
}
return t2sConverter;
}
function getS2TConverter() {
if (!s2tConverter) {
s2tConverter = OpenCC.Converter({ from: 'cn', to: 'tw' });
}
return s2tConverter;
}
/**
* Convert Traditional Chinese characters to Simplified Chinese.
* Characters not in the mapping are left unchanged.
* Convert Traditional Chinese to Simplified Chinese (comprehensive).
* Used for search query normalization.
*/
export function traditionalToSimplified(text: string): string {
let result = '';
for (const char of text) {
result += T2S_MAP[char] || char;
}
return result;
return getT2SConverter()(text);
}
/**
* Check if text contains any Traditional Chinese characters.
* Convert Simplified Chinese to Traditional Chinese (comprehensive).
* Used for UI locale conversion.
*/
export function hasTraditionalChinese(text: string): boolean {
for (const char of text) {
if (T2S_MAP[char]) return true;
}
return false;
export function simplifiedToTraditional(text: string): string {
return getS2TConverter()(text);
}
+11 -4
View File
@@ -30,14 +30,21 @@ export function parseVideoTitle(title: string): { cleanTitle: string, quality?:
* Quality keywords and their display labels, ordered by priority (highest first).
*/
const QUALITY_PATTERNS: { pattern: RegExp; label: string; color: string }[] = [
{ pattern: /4k|2160p/i, label: '4K', color: 'bg-amber-500' },
{ pattern: /蓝光|藍光|bluray|blu-ray/i, label: '蓝光', color: 'bg-blue-500' },
{ pattern: /4k|2160p|uhd/i, label: '4K', color: 'bg-amber-500' },
{ pattern: /蓝光|藍光|bluray|blu-ray|remux/i, label: '蓝光', color: 'bg-blue-500' },
{ pattern: /1080p|1080i|full\s*hd|fhd/i, label: '1080P', color: 'bg-green-500' },
{ pattern: /超清|超高清/i, label: '超清', color: 'bg-green-500' },
{ pattern: /720p|hd720/i, label: '720P', color: 'bg-teal-500' },
{ pattern: /\bhd\b|高清/i, label: 'HD', color: 'bg-teal-500' },
{ pattern: /抢先|枪版|ts版|ts\b|cam\b|hdts/i, label: 'TS', color: 'bg-orange-500' },
{ pattern: /\bhd\b|高清|hdr/i, label: 'HD', color: 'bg-teal-500' },
{ pattern: /web-?dl|webrip/i, label: 'WEB-DL', color: 'bg-indigo-500' },
{ pattern: /hdtv/i, label: 'HDTV', color: 'bg-teal-500' },
{ pattern: /dvd|dvdrip/i, label: 'DVD', color: 'bg-purple-500' },
{ pattern: /抢先|枪版|ts版|ts\b|cam\b|hdts|预告/i, label: 'TS', color: 'bg-orange-500' },
{ pattern: /标清|sd\b/i, label: 'SD', color: 'bg-gray-500' },
{ pattern: /杜比|dolby|atmos/i, label: '杜比', color: 'bg-violet-500' },
{ pattern: /国语|普通话|mandarin/i, label: '国语', color: 'bg-sky-500' },
{ pattern: /粤语|cantonese/i, label: '粤语', color: 'bg-sky-500' },
{ pattern: /中[文字]字幕|中字|双语字幕/i, label: '中字', color: 'bg-cyan-500' },
];
/**
+12
View File
@@ -16,6 +16,7 @@
"hls.js": "^1.6.15",
"lucide-react": "^0.575.0",
"next": "16.1.6",
"opencc-js": "^1.0.5",
"react": "19.2.4",
"react-dom": "19.2.4",
"zustand": "^5.0.11"
@@ -7188,6 +7189,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/opencc-js": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/opencc-js/-/opencc-js-1.0.5.tgz",
"integrity": "sha512-LD+1SoNnZdlRwtYTjnQdFrSVCAaYpuDqL5CkmOaHOkKoKh7mFxUicLTRVNLU5C+Jmi1vXQ3QL4jWdgSaa4sKjg==",
"license": "MIT"
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -13404,6 +13411,11 @@
"es-object-atoms": "^1.0.0"
}
},
"opencc-js": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/opencc-js/-/opencc-js-1.0.5.tgz",
"integrity": "sha512-LD+1SoNnZdlRwtYTjnQdFrSVCAaYpuDqL5CkmOaHOkKoKh7mFxUicLTRVNLU5C+Jmi1vXQ3QL4jWdgSaa4sKjg=="
},
"optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+1
View File
@@ -18,6 +18,7 @@
"hls.js": "^1.6.15",
"lucide-react": "^0.575.0",
"next": "16.1.6",
"opencc-js": "^1.0.5",
"react": "19.2.4",
"react-dom": "19.2.4",
"zustand": "^5.0.11"
+14
View File
@@ -0,0 +1,14 @@
declare module 'opencc-js' {
interface ConverterOptions {
from: 'cn' | 'tw' | 'twp' | 'hk' | 'jp';
to: 'cn' | 'tw' | 'twp' | 'hk' | 'jp';
}
export function Converter(options: ConverterOptions): (text: string) => string;
export function HTMLConverter(
converter: (text: string) => string,
rootNode: HTMLElement,
fromLangTag: string,
toLangTag: string
): void;
}