From 365b9c0660166157263a346cc77d7a04b5fb029c Mon Sep 17 00:00:00 2001 From: Nexmoe <16796652+nexmoe@users.noreply.github.com> Date: Sat, 25 Oct 2025 15:59:03 +0800 Subject: [PATCH] feat: add flag-icons package and improve language handling in sidebar and i18n --- package.json | 1 + pnpm-lock.yaml | 8 ++ src/main/tray.ts | 9 +- src/renderer/src/components/ui/sidebar.tsx | 18 ++-- src/renderer/src/i18n.ts | 20 ++-- src/renderer/src/locales/en.json | 4 - src/renderer/src/locales/zh.json | 4 - src/renderer/src/main.tsx | 1 + src/shared/languages.ts | 105 +++++++++++++++++++++ src/shared/types/index.ts | 7 +- 10 files changed, 149 insertions(+), 28 deletions(-) create mode 100644 src/shared/languages.ts diff --git a/package.json b/package.json index 9d2b82b..341cc20 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 799fc76..4b90af4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/main/tray.ts b/src/main/tray.ts index 7b25839..e8a4b3c 100644 --- a/src/main/tray.ts +++ b/src/main/tray.ts @@ -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] } diff --git a/src/renderer/src/components/ui/sidebar.tsx b/src/renderer/src/components/ui/sidebar.tsx index 3010e59..5ce6378 100644 --- a/src/renderer/src/components/ui/sidebar.tsx +++ b/src/renderer/src/components/ui/sidebar.tsx @@ -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) { void handleLanguageChange(option.value)} - className={isActive ? 'font-semibold' : undefined} + className={isActive ? 'font-semibold bg-muted focus:bg-muted' : undefined} + aria-current={isActive} > - {option.label} +
+
) })} diff --git a/src/renderer/src/i18n.ts b/src/renderer/src/i18n.ts index 29a34c7..b4aec66 100644 --- a/src/renderer/src/i18n.ts +++ b/src/renderer/src/i18n.ts @@ -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 } diff --git a/src/renderer/src/locales/en.json b/src/renderer/src/locales/en.json index 0e5871a..d4a52ef 100644 --- a/src/renderer/src/locales/en.json +++ b/src/renderer/src/locales/en.json @@ -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", diff --git a/src/renderer/src/locales/zh.json b/src/renderer/src/locales/zh.json index 51ad58e..31dacc4 100644 --- a/src/renderer/src/locales/zh.json +++ b/src/renderer/src/locales/zh.json @@ -244,10 +244,6 @@ "downloadPath": "下载位置", "general": "通用", "language": "语言", - "languageOptions": { - "chinese": "简体中文", - "english": "英语" - }, "light": "浅色", "maxConcurrentDownloads": "最大活动下载数", "none": "无", diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index 67c57aa..6a09aec 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -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' diff --git a/src/shared/languages.ts b/src/shared/languages.ts new file mode 100644 index 0000000..709b05a --- /dev/null +++ b/src/shared/languages.ts @@ -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 + +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 +} diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index 933c23f..f651fec 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -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',