diff --git a/qwen3-tts-frontend/fonts/noto-serif-jp-regular.woff2 b/qwen3-tts-frontend/fonts/noto-serif-jp-regular.woff2 new file mode 100644 index 0000000..be090b1 Binary files /dev/null and b/qwen3-tts-frontend/fonts/noto-serif-jp-regular.woff2 differ diff --git a/qwen3-tts-frontend/fonts/noto-serif-kr-regular.woff2 b/qwen3-tts-frontend/fonts/noto-serif-kr-regular.woff2 new file mode 100644 index 0000000..241df48 Binary files /dev/null and b/qwen3-tts-frontend/fonts/noto-serif-kr-regular.woff2 differ diff --git a/qwen3-tts-frontend/fonts/noto-serif-tc-regular.woff2 b/qwen3-tts-frontend/fonts/noto-serif-tc-regular.woff2 new file mode 100644 index 0000000..c9176e6 Binary files /dev/null and b/qwen3-tts-frontend/fonts/noto-serif-tc-regular.woff2 differ diff --git a/qwen3-tts-frontend/src/contexts/UserPreferencesContext.tsx b/qwen3-tts-frontend/src/contexts/UserPreferencesContext.tsx index 3036e45..b9ffbf2 100644 --- a/qwen3-tts-frontend/src/contexts/UserPreferencesContext.tsx +++ b/qwen3-tts-frontend/src/contexts/UserPreferencesContext.tsx @@ -3,6 +3,7 @@ import { authApi } from '@/lib/api' import { useAuth } from '@/contexts/AuthContext' import type { UserPreferences } from '@/types/auth' import i18n from '@/locales' +import { loadFontsForLanguage, detectBrowserLanguage } from '@/lib/fontManager' interface UserPreferencesContextType { preferences: UserPreferences | null @@ -24,6 +25,8 @@ export function UserPreferencesProvider({ children }: { children: ReactNode }) { const fetchPreferences = async () => { if (!isAuthenticated || !user) { + const browserLang = detectBrowserLanguage() + await loadFontsForLanguage(browserLang) setIsLoading(false) return } @@ -38,9 +41,11 @@ export function UserPreferencesProvider({ children }: { children: ReactNode }) { setPreferences(prefs) setHasAliyunKey(keyVerification.valid) - if (prefs.language) { - i18n.changeLanguage(prefs.language) - } + const lang = prefs.language || detectBrowserLanguage() + await Promise.all([ + i18n.changeLanguage(lang), + loadFontsForLanguage(lang), + ]) const cacheKey = `user_preferences_${user.id}` localStorage.setItem(cacheKey, JSON.stringify(prefs)) @@ -48,8 +53,14 @@ export function UserPreferencesProvider({ children }: { children: ReactNode }) { const cacheKey = `user_preferences_${user.id}` const cached = localStorage.getItem(cacheKey) if (cached) { - setPreferences(JSON.parse(cached)) + const cachedPrefs = JSON.parse(cached) + setPreferences(cachedPrefs) + if (cachedPrefs.language) { + await loadFontsForLanguage(cachedPrefs.language) + } } else { + const browserLang = detectBrowserLanguage() + await loadFontsForLanguage(browserLang) setPreferences({ default_backend: 'aliyun', onboarding_completed: false }) } } finally { @@ -87,7 +98,10 @@ export function UserPreferencesProvider({ children }: { children: ReactNode }) { } const changeLanguage = async (lang: 'zh-CN' | 'zh-TW' | 'en-US' | 'ja-JP' | 'ko-KR') => { - await i18n.changeLanguage(lang) + await Promise.all([ + i18n.changeLanguage(lang), + loadFontsForLanguage(lang), + ]) await updatePreferences({ language: lang }) } diff --git a/qwen3-tts-frontend/src/index.css b/qwen3-tts-frontend/src/index.css index 14f64fc..415e7d1 100644 --- a/qwen3-tts-frontend/src/index.css +++ b/qwen3-tts-frontend/src/index.css @@ -1,24 +1,3 @@ -@font-face { - font-family: 'Noto Serif'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url('/fonts/noto-serif-regular.woff2') format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, - U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, - U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} - -@font-face { - font-family: 'Noto Serif'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url('/fonts/noto-serif-sc-regular.woff2') format('woff2'); - unicode-range: U+4E00-9FFF, U+3400-4DBF, U+20000-2A6DF, U+2A700-2B73F, - U+2B740-2B81F, U+2B820-2CEAF, U+F900-FAFF, U+2F800-2FA1F; -} - @tailwind base; @tailwind components; @tailwind utilities; diff --git a/qwen3-tts-frontend/src/lib/fontManager.ts b/qwen3-tts-frontend/src/lib/fontManager.ts new file mode 100644 index 0000000..ae5f64a --- /dev/null +++ b/qwen3-tts-frontend/src/lib/fontManager.ts @@ -0,0 +1,153 @@ +type Language = 'zh-CN' | 'zh-TW' | 'en-US' | 'ja-JP' | 'ko-KR' + +interface FontConfig { + name: string + file: string + unicodeRange?: string +} + +const fontConfigs: Record = { + 'zh-CN': [ + { + name: 'Noto Serif', + file: '/fonts/noto-serif-sc-regular.woff2', + unicodeRange: 'U+4E00-9FFF, U+3400-4DBF, U+20000-2A6DF, U+2A700-2B73F, U+2B740-2B81F, U+2B820-2CEAF, U+F900-FAFF, U+2F800-2FA1F', + }, + { + name: 'Noto Serif', + file: '/fonts/noto-serif-regular.woff2', + unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD', + }, + ], + 'zh-TW': [ + { + name: 'Noto Serif', + file: '/fonts/noto-serif-tc-regular.woff2', + unicodeRange: 'U+4E00-9FFF, U+3400-4DBF, U+F900-FAFF', + }, + { + name: 'Noto Serif', + file: '/fonts/noto-serif-sc-regular.woff2', + unicodeRange: 'U+20000-2A6DF, U+2A700-2B73F, U+2B740-2B81F, U+2B820-2CEAF, U+2F800-2FA1F', + }, + { + name: 'Noto Serif', + file: '/fonts/noto-serif-regular.woff2', + unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD', + }, + ], + 'en-US': [ + { + name: 'Noto Serif', + file: '/fonts/noto-serif-regular.woff2', + unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD', + }, + ], + 'ja-JP': [ + { + name: 'Noto Serif', + file: '/fonts/noto-serif-jp-regular.woff2', + unicodeRange: 'U+3000-30FF, U+4E00-9FFF, U+FF00-FFEF', + }, + { + name: 'Noto Serif', + file: '/fonts/noto-serif-regular.woff2', + unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD', + }, + ], + 'ko-KR': [ + { + name: 'Noto Serif', + file: '/fonts/noto-serif-kr-regular.woff2', + unicodeRange: 'U+AC00-D7AF, U+1100-11FF, U+3130-318F', + }, + { + name: 'Noto Serif', + file: '/fonts/noto-serif-regular.woff2', + unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD', + }, + ], +} + +const loadedFonts = new Set() + +function createFontFace(config: FontConfig): FontFace { + const descriptors: FontFaceDescriptors = { + style: 'normal', + weight: '400', + display: 'swap', + } + + if (config.unicodeRange) { + descriptors.unicodeRange = config.unicodeRange + } + + return new FontFace(config.name, `url(${config.file}) format('woff2')`, descriptors) +} + +export async function loadFontsForLanguage(language: Language): Promise { + const configs = fontConfigs[language] + if (!configs) return + + const loadPromises = configs.map(async (config) => { + const fontKey = `${config.name}-${config.file}` + + if (loadedFonts.has(fontKey)) { + return + } + + try { + const fontFace = createFontFace(config) + await fontFace.load() + document.fonts.add(fontFace) + loadedFonts.add(fontKey) + } catch (error) { + console.warn(`Failed to load font ${config.file}:`, error) + } + }) + + await Promise.all(loadPromises) +} + +export function detectBrowserLanguage(): Language { + const browserLang = navigator.language.toLowerCase() + + if (browserLang.startsWith('zh-tw') || browserLang.startsWith('zh-hk') || browserLang.startsWith('zh-mo')) { + return 'zh-TW' + } + if (browserLang.startsWith('zh')) { + return 'zh-CN' + } + if (browserLang.startsWith('ja')) { + return 'ja-JP' + } + if (browserLang.startsWith('ko')) { + return 'ko-KR' + } + + return 'en-US' +} + +export function preloadBaseFont(): void { + const baseConfig: FontConfig = { + name: 'Noto Serif', + file: '/fonts/noto-serif-regular.woff2', + unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD', + } + + const fontKey = `${baseConfig.name}-${baseConfig.file}` + + if (loadedFonts.has(fontKey)) { + return + } + + const fontFace = createFontFace(baseConfig) + fontFace.load() + .then(() => { + document.fonts.add(fontFace) + loadedFonts.add(fontKey) + }) + .catch((error) => { + console.warn('Failed to preload base font:', error) + }) +} diff --git a/qwen3-tts-frontend/src/main.tsx b/qwen3-tts-frontend/src/main.tsx index ae67b55..c0f62ff 100644 --- a/qwen3-tts-frontend/src/main.tsx +++ b/qwen3-tts-frontend/src/main.tsx @@ -3,6 +3,9 @@ import { createRoot } from 'react-dom/client' import './locales' import './index.css' import App from './App.tsx' +import { preloadBaseFont } from './lib/fontManager' + +preloadBaseFont() createRoot(document.getElementById('root')!).render(