feat: add flag-icons package and improve language handling in sidebar and i18n

This commit is contained in:
Nexmoe
2025-10-25 15:59:03 +08:00
parent a1f5e0480c
commit 365b9c0660
10 changed files with 149 additions and 28 deletions

View File

@@ -47,6 +47,7 @@
"electron-log": "^5.4.3",
"electron-store": "^11.0.2",
"electron-updater": "^6.3.9",
"flag-icons": "^7.5.0",
"i18next": "^25.5.3",
"jotai": "^2.15.0",
"lucide-react": "^0.544.0",

8
pnpm-lock.yaml generated
View File

@@ -86,6 +86,9 @@ importers:
electron-updater:
specifier: ^6.3.9
version: 6.6.2
flag-icons:
specifier: ^7.5.0
version: 7.5.0
i18next:
specifier: ^25.5.3
version: 25.6.0(typescript@5.9.3)
@@ -2056,6 +2059,9 @@ packages:
filelist@1.0.4:
resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==}
flag-icons@7.5.0:
resolution: {integrity: sha512-kd+MNXviFIg5hijH766tt+3x76ele1AXlo4zDdCxIvqWZhKt4T83bOtxUOOMlTx/EcFdUMH5yvQgYlFh1EqqFg==}
foreground-child@3.3.1:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
@@ -5350,6 +5356,8 @@ snapshots:
dependencies:
minimatch: 5.1.6
flag-icons@7.5.0: {}
foreground-child@3.3.1:
dependencies:
cross-spawn: 7.0.6

View File

@@ -1,3 +1,4 @@
import { normalizeLanguageCode } from '@shared/languages'
import { app, BrowserWindow, Menu, nativeImage, Tray } from 'electron'
import appIcon from '../../resources/icon.png?asset'
import trayIcon from '../../resources/tray-icon.png?asset'
@@ -9,7 +10,7 @@ let tray: Tray | null = null
* Get translated text based on current language setting
*/
function t(key: 'showHome' | 'quit'): string {
const language = settingsManager.get('language') || 'en'
const language = normalizeLanguageCode(settingsManager.get('language'))
const translations = {
en: {
@@ -17,12 +18,12 @@ function t(key: 'showHome' | 'quit'): string {
quit: 'Quit'
},
zh: {
showHome: '打开首页',
quit: '关闭应用'
showHome: '显示主页',
quit: '退出应用'
}
}
const displayLang = language.startsWith('en') ? 'en' : 'zh'
const displayLang = language.startsWith('zh') ? 'zh' : 'en'
return translations[displayLang][key]
}

View File

@@ -7,6 +7,7 @@ import {
} from '@renderer/components/ui/dropdown-menu'
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'
import { saveSettingAtom } from '@renderer/store/settings'
import { type LanguageCode, languageList, normalizeLanguageCode } from '@shared/languages'
import { useSetAtom } from 'jotai'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
@@ -40,10 +41,7 @@ export function Sidebar({ currentPage, onPageChange }: SidebarProps) {
const { t, i18n } = useTranslation()
const saveSetting = useSetAtom(saveSettingAtom)
const languageOptions = [
{ value: 'en', label: 'English' },
{ value: 'zh', label: '中文' }
]
const languageOptions = languageList
const navigationItems: NavigationItem[] = [
{
@@ -83,11 +81,11 @@ export function Sidebar({ currentPage, onPageChange }: SidebarProps) {
}
]
const activeLanguageCode = (i18n.language ?? 'en').split('-')[0]
const activeLanguageCode = normalizeLanguageCode(i18n.language)
const currentLanguage =
languageOptions.find((option) => option.value === activeLanguageCode) ?? languageOptions[0]
const handleLanguageChange = async (value: string) => {
const handleLanguageChange = async (value: LanguageCode) => {
if (activeLanguageCode === value) {
return
}
@@ -167,9 +165,13 @@ export function Sidebar({ currentPage, onPageChange }: SidebarProps) {
<DropdownMenuItem
key={option.value}
onClick={() => void handleLanguageChange(option.value)}
className={isActive ? 'font-semibold' : undefined}
className={isActive ? 'font-semibold bg-muted focus:bg-muted' : undefined}
aria-current={isActive}
>
{option.label}
<div className="flex items-center gap-2">
<span className={`${option.flag} text-base`} aria-hidden="true" />
<span lang={option.hreflang}>{option.name}</span>
</div>
</DropdownMenuItem>
)
})}

View File

@@ -1,15 +1,23 @@
import { defaultLanguageCode, supportedLanguageCodes } from '@shared/languages'
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import en from './locales/en.json'
import zh from './locales/zh.json'
const resources = Object.fromEntries(
supportedLanguageCodes.map((code) => [
code,
{
translation: code === 'zh' || code === 'zh-TW' ? zh : en
}
])
)
i18n.use(initReactI18next).init({
resources: {
en: { translation: en },
zh: { translation: zh }
},
lng: 'en',
fallbackLng: 'en',
resources,
lng: defaultLanguageCode,
fallbackLng: defaultLanguageCode,
supportedLngs: supportedLanguageCodes,
interpolation: {
escapeValue: false
}

View File

@@ -254,10 +254,6 @@
"downloadPath": "Download location",
"general": "General",
"language": "Language",
"languageOptions": {
"chinese": "Chinese (Simplified)",
"english": "English"
},
"light": "Light",
"maxConcurrentDownloads": "Maximum number of active downloads",
"none": "None",

View File

@@ -244,10 +244,6 @@
"downloadPath": "下载位置",
"general": "通用",
"language": "语言",
"languageOptions": {
"chinese": "简体中文",
"english": "英语"
},
"light": "浅色",
"maxConcurrentDownloads": "最大活动下载数",
"none": "无",

View File

@@ -1,5 +1,6 @@
import './assets/main.css'
import './assets/global.css'
import 'flag-icons/css/flag-icons.min.css'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'

105
src/shared/languages.ts Normal file
View File

@@ -0,0 +1,105 @@
export interface LanguageDefinition {
flag: string
name: string
hreflang: string
}
export const languages = {
en: {
flag: 'fi fi-us',
name: 'English',
hreflang: 'en'
},
es: {
flag: 'fi fi-es',
name: 'Español',
hreflang: 'es'
},
ar: {
flag: 'fi fi-sa',
name: 'العربية',
hreflang: 'ar'
},
id: {
flag: 'fi fi-id',
name: 'Bahasa Indonesia',
hreflang: 'id'
},
pt: {
flag: 'fi fi-br',
name: 'Português',
hreflang: 'pt-BR'
},
fr: {
flag: 'fi fi-fr',
name: 'Français',
hreflang: 'fr'
},
it: {
flag: 'fi fi-it',
name: 'Italiano',
hreflang: 'it'
},
zh: {
flag: 'fi fi-cn',
name: '中文',
hreflang: 'zh-CN'
},
'zh-TW': {
flag: 'fi fi-tw',
name: '繁體中文',
hreflang: 'zh-TW'
},
ko: {
flag: 'fi fi-kr',
name: '한국어',
hreflang: 'ko'
},
ja: {
flag: 'fi fi-jp',
name: '日本語',
hreflang: 'ja'
},
ru: {
flag: 'fi fi-ru',
name: 'Русский',
hreflang: 'ru'
},
de: {
flag: 'fi fi-de',
name: 'Deutsch',
hreflang: 'de'
}
} as const satisfies Record<string, LanguageDefinition>
export type LanguageCode = keyof typeof languages
export const defaultLanguageCode: LanguageCode = 'en'
export const languageList = Object.entries(languages).map(([code, definition]) => ({
value: code as LanguageCode,
...definition
}))
export const supportedLanguageCodes = languageList.map((language) => language.value)
export function normalizeLanguageCode(code: string | null | undefined): LanguageCode {
if (!code) {
return defaultLanguageCode
}
const normalizedInput = code.toLowerCase()
const directMatch = supportedLanguageCodes.find(
(languageCode) => languageCode.toLowerCase() === normalizedInput
)
if (directMatch) {
return directMatch
}
const base = normalizedInput.split('-')[0] ?? ''
const baseMatch = supportedLanguageCodes.find(
(languageCode) => languageCode.split('-')[0]?.toLowerCase() === base
)
return baseMatch ?? defaultLanguageCode
}

View File

@@ -1,3 +1,6 @@
import type { LanguageCode } from '../languages'
import { defaultLanguageCode } from '../languages'
// Download related types
export interface VideoFormat {
format_id: string
@@ -146,7 +149,7 @@ export interface AppSettings {
proxy: string
configPath: string
betaProgram: boolean
language: string
language: LanguageCode
theme: string
oneClickDownload: boolean
oneClickDownloadType: 'video' | 'audio'
@@ -163,7 +166,7 @@ export const defaultSettings: AppSettings = {
proxy: '',
configPath: '',
betaProgram: false,
language: 'en',
language: defaultLanguageCode,
theme: 'system',
oneClickDownload: false,
oneClickDownloadType: 'video',