feat: Add font loading functionality for multi-language support and preload base font

This commit is contained in:
2026-02-06 14:09:22 +08:00
parent 9e61734e25
commit 2d2c4e9f98
7 changed files with 175 additions and 26 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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 })
} }

View File

@@ -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;

View 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)
})
}

View File

@@ -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>