feat: add flag-icons package and improve language handling in sidebar and i18n
This commit is contained in:
@@ -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
8
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -244,10 +244,6 @@
|
||||
"downloadPath": "下载位置",
|
||||
"general": "通用",
|
||||
"language": "语言",
|
||||
"languageOptions": {
|
||||
"chinese": "简体中文",
|
||||
"english": "英语"
|
||||
},
|
||||
"light": "浅色",
|
||||
"maxConcurrentDownloads": "最大活动下载数",
|
||||
"none": "无",
|
||||
|
||||
@@ -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
105
src/shared/languages.ts
Normal 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
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user