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}
+
+
+ {option.name}
+
)
})}
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',