feat: Add font loading functionality for multi-language support and preload base font
This commit is contained in:
BIN
qwen3-tts-frontend/fonts/noto-serif-jp-regular.woff2
Normal file
BIN
qwen3-tts-frontend/fonts/noto-serif-jp-regular.woff2
Normal file
Binary file not shown.
BIN
qwen3-tts-frontend/fonts/noto-serif-kr-regular.woff2
Normal file
BIN
qwen3-tts-frontend/fonts/noto-serif-kr-regular.woff2
Normal file
Binary file not shown.
BIN
qwen3-tts-frontend/fonts/noto-serif-tc-regular.woff2
Normal file
BIN
qwen3-tts-frontend/fonts/noto-serif-tc-regular.woff2
Normal file
Binary file not shown.
@@ -3,6 +3,7 @@ import { authApi } from '@/lib/api'
|
|||||||
import { useAuth } from '@/contexts/AuthContext'
|
import { useAuth } from '@/contexts/AuthContext'
|
||||||
import type { UserPreferences } from '@/types/auth'
|
import type { UserPreferences } from '@/types/auth'
|
||||||
import i18n from '@/locales'
|
import i18n from '@/locales'
|
||||||
|
import { loadFontsForLanguage, detectBrowserLanguage } from '@/lib/fontManager'
|
||||||
|
|
||||||
interface UserPreferencesContextType {
|
interface UserPreferencesContextType {
|
||||||
preferences: UserPreferences | null
|
preferences: UserPreferences | null
|
||||||
@@ -24,6 +25,8 @@ export function UserPreferencesProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
const fetchPreferences = async () => {
|
const fetchPreferences = async () => {
|
||||||
if (!isAuthenticated || !user) {
|
if (!isAuthenticated || !user) {
|
||||||
|
const browserLang = detectBrowserLanguage()
|
||||||
|
await loadFontsForLanguage(browserLang)
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -38,9 +41,11 @@ export function UserPreferencesProvider({ children }: { children: ReactNode }) {
|
|||||||
setPreferences(prefs)
|
setPreferences(prefs)
|
||||||
setHasAliyunKey(keyVerification.valid)
|
setHasAliyunKey(keyVerification.valid)
|
||||||
|
|
||||||
if (prefs.language) {
|
const lang = prefs.language || detectBrowserLanguage()
|
||||||
i18n.changeLanguage(prefs.language)
|
await Promise.all([
|
||||||
}
|
i18n.changeLanguage(lang),
|
||||||
|
loadFontsForLanguage(lang),
|
||||||
|
])
|
||||||
|
|
||||||
const cacheKey = `user_preferences_${user.id}`
|
const cacheKey = `user_preferences_${user.id}`
|
||||||
localStorage.setItem(cacheKey, JSON.stringify(prefs))
|
localStorage.setItem(cacheKey, JSON.stringify(prefs))
|
||||||
@@ -48,8 +53,14 @@ export function UserPreferencesProvider({ children }: { children: ReactNode }) {
|
|||||||
const cacheKey = `user_preferences_${user.id}`
|
const cacheKey = `user_preferences_${user.id}`
|
||||||
const cached = localStorage.getItem(cacheKey)
|
const cached = localStorage.getItem(cacheKey)
|
||||||
if (cached) {
|
if (cached) {
|
||||||
setPreferences(JSON.parse(cached))
|
const cachedPrefs = JSON.parse(cached)
|
||||||
|
setPreferences(cachedPrefs)
|
||||||
|
if (cachedPrefs.language) {
|
||||||
|
await loadFontsForLanguage(cachedPrefs.language)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
const browserLang = detectBrowserLanguage()
|
||||||
|
await loadFontsForLanguage(browserLang)
|
||||||
setPreferences({ default_backend: 'aliyun', onboarding_completed: false })
|
setPreferences({ default_backend: 'aliyun', onboarding_completed: false })
|
||||||
}
|
}
|
||||||
} finally {
|
} 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') => {
|
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 })
|
await updatePreferences({ language: lang })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|||||||
153
qwen3-tts-frontend/src/lib/fontManager.ts
Normal file
153
qwen3-tts-frontend/src/lib/fontManager.ts
Normal file
@@ -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<Language, FontConfig[]> = {
|
||||||
|
'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<string>()
|
||||||
|
|
||||||
|
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<void> {
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -3,6 +3,9 @@ import { createRoot } from 'react-dom/client'
|
|||||||
import './locales'
|
import './locales'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
|
import { preloadBaseFont } from './lib/fontManager'
|
||||||
|
|
||||||
|
preloadBaseFont()
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
|||||||
Reference in New Issue
Block a user