From b2180353c9c9c3224d1a96acdf6d2ce6d4a30a48 Mon Sep 17 00:00:00 2001 From: stryde01 <2325502542@qq.com> Date: Sun, 30 Aug 2026 23:46:38 +0800 Subject: [PATCH 1/2] Localize Chinese UI strings --- src-tauri/Cargo.toml | 2 +- src/components/ContributorPRsModal.tsx | 22 +++-- src/components/cards/ProjectCardList.tsx | 5 +- src/components/modals/BugReportModal.tsx | 53 +++++----- .../modals/CategoryManagerModal.tsx | 2 +- .../modals/CheckForUpdatesModal.tsx | 32 ++---- src/components/reusables/DragHandle.tsx | 4 +- src/components/reusables/LanguageFlag.tsx | 4 +- src/components/reusables/LanguagePicker.tsx | 18 ++-- src/components/reusables/ToastContainer.tsx | 4 +- src/components/ui/Sidebar.tsx | 4 +- src/i18n/locales/en-US/changelog.json | 4 +- src/i18n/locales/en-US/common.json | 35 ++++++- src/i18n/locales/en-US/settings.json | 4 + src/i18n/locales/en-US/versions.json | 1 + src/i18n/locales/zh-CN/changelog.json | 4 +- src/i18n/locales/zh-CN/common.json | 97 +++++++++++++------ src/i18n/locales/zh-CN/dashboard.json | 26 ++--- src/i18n/locales/zh-CN/git.json | 30 +++--- src/i18n/locales/zh-CN/nav.json | 2 +- src/i18n/locales/zh-CN/onboarding.json | 6 +- src/i18n/locales/zh-CN/settings.json | 28 +++--- src/i18n/locales/zh-CN/versions.json | 7 +- src/i18n/types.ts | 86 +++++++++++++++- src/views/ChangelogView.tsx | 8 +- src/views/SettingsView.tsx | 18 ++-- src/views/TemplatesView.tsx | 10 +- src/views/VersionsView.tsx | 3 +- 28 files changed, 337 insertions(+), 182 deletions(-) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index cfa05ca..a44c539 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [lib] name = "godothub_lib" -crate-type = ["staticlib", "cdylib", "rlib"] +crate-type = ["staticlib", "rlib"] [build-dependencies] tauri-build = { version = "2", features = [] } diff --git a/src/components/ContributorPRsModal.tsx b/src/components/ContributorPRsModal.tsx index a22cab1..aaf4894 100644 --- a/src/components/ContributorPRsModal.tsx +++ b/src/components/ContributorPRsModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' import { createPortal } from 'react-dom' import { motion, AnimatePresence } from 'framer-motion' import { openUrl } from '@tauri-apps/plugin-opener' @@ -19,24 +20,24 @@ interface Props { onClose: () => void } -function stateBadge(pr: PR) { +function stateBadge(pr: PR, t: (key: string) => string) { if (pr.merged_at) { return ( - Merged + {t('contributor_pr_merged')} ) } if (pr.state === 'open') { return ( - Open + {t('contributor_pr_open')} ) } return ( - Closed + {t('contributor_pr_closed')} ) } @@ -50,6 +51,7 @@ function formatDate(iso: string) { } export function ContributorPRsModal({ login, avatarUrl, onClose }: Props) { + const { t } = useTranslation('common') const [prs, setPrs] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(false) @@ -127,7 +129,9 @@ export function ContributorPRsModal({ login, avatarUrl, onClose }: Props) { {login}

- {loading ? 'Loading PRs...' : `${prs.length} pull request${prs.length !== 1 ? 's' : ''}`} + {loading + ? t('contributor_pr_loading') + : t(prs.length === 1 ? 'contributor_pr_count_one' : 'contributor_pr_count_other', { count: prs.length })}

))} diff --git a/src/components/cards/ProjectCardList.tsx b/src/components/cards/ProjectCardList.tsx index 1644050..4391625 100644 --- a/src/components/cards/ProjectCardList.tsx +++ b/src/components/cards/ProjectCardList.tsx @@ -452,6 +452,7 @@ export function ProjectCardList({ (dndCategoryGroups || categoryGroups)!, categories, isDndEnabled ? cardForDnd : cardFor, + t('uncategorized'), isDndEnabled, ) : unpinnedProjects.map((p) => isDndEnabled ? cardForDnd(p) : cardFor(p))), @@ -461,6 +462,7 @@ export function ProjectCardList({ (dndCategoryGroups || categoryGroups)!, categories, isDndEnabled ? cardForDnd : cardFor, + t('uncategorized'), isDndEnabled, ) : projects.map((p) => isDndEnabled ? cardForDnd(p) : cardFor(p)) @@ -520,6 +522,7 @@ function renderCategoryGroups( groups: Map, categories: Category[], cardFor: (p: Project) => ReactNode, + uncategorizedLabel: string, disableAnimation = false, ): ReactNode[] { const result: ReactNode[] = [] @@ -545,7 +548,7 @@ function renderCategoryGroups( result.push( 0} disableAnimation={disableAnimation} diff --git a/src/components/modals/BugReportModal.tsx b/src/components/modals/BugReportModal.tsx index 9b8a73f..267d10b 100644 --- a/src/components/modals/BugReportModal.tsx +++ b/src/components/modals/BugReportModal.tsx @@ -65,16 +65,16 @@ function installErrorCapture() { installErrorCapture() -async function getGPUInfo(): Promise { +async function getGPUInfo(t: (key: string) => string): Promise { try { const canvas = document.createElement('canvas') const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl') - if (!gl) return 'WebGL not available' + if (!gl) return t('gpu_webgl_unavailable') const debugInfo = ( gl as WebGLRenderingContext ).getExtension('WEBGL_debug_renderer_info') - if (!debugInfo) return 'GPU info unavailable' + if (!debugInfo) return t('gpu_info_unavailable') const renderer = (gl as WebGLRenderingContext).getParameter( debugInfo.UNMASKED_RENDERER_WEBGL, ) @@ -84,19 +84,19 @@ async function getGPUInfo(): Promise { canvas.remove() return `${vendor}, ${renderer}` } catch { - return 'GPU info unavailable' + return t('gpu_info_unavailable') } } -async function buildSystemReport(): Promise<{ specs: string; errors: string }> { - const gpu = await getGPUInfo() +async function buildSystemReport(t: (key: string) => string): Promise<{ specs: string; errors: string }> { + const gpu = await getGPUInfo(t) const specs = [ - '## System', + `## ${t('bug_report_system')}`, '', - `- **Version**: ${version}`, - `- **Date**: ${new Date().toISOString().slice(0, 10)}`, - `- **OS**: ${ + `- **${t('bug_report_version')}**: ${version}`, + `- **${t('bug_report_date')}**: ${new Date().toISOString().slice(0, 10)}`, + `- **${t('bug_report_os')}**: ${ navigator.userAgent.includes('Windows') ? 'Windows' : navigator.userAgent.includes('Mac OS X') || @@ -106,28 +106,28 @@ async function buildSystemReport(): Promise<{ specs: string; errors: string }> { ? 'Linux' : navigator.userAgent }`, - `- **Platform**: ${navigator.platform}`, - `- **Language**: ${navigator.language}`, - `- **CPU Cores**: ${navigator.hardwareConcurrency ?? 'unknown'}`, - `- **RAM**: ${ + `- **${t('bug_report_platform')}**: ${navigator.platform}`, + `- **${t('bug_report_language')}**: ${navigator.language}`, + `- **${t('bug_report_cpu_cores')}**: ${navigator.hardwareConcurrency ?? t('unknown')}`, + `- **${t('bug_report_ram')}**: ${ ( navigator as Navigator & { deviceMemory?: number } ).deviceMemory ? `${(navigator as Navigator & { deviceMemory?: number }).deviceMemory} GB` - : 'unknown' + : t('unknown') }`, - `- **Screen**: ${screen.width}x${screen.height} @${screen.colorDepth}bit`, - `- **GPU**: ${gpu}`, - `- **User Agent**: ${navigator.userAgent}`, + `- **${t('bug_report_screen')}**: ${screen.width}x${screen.height} @${screen.colorDepth}bit`, + `- **${t('bug_report_gpu')}**: ${gpu}`, + `- **${t('bug_report_user_agent')}**: ${navigator.userAgent}`, ].join('\n') - const errorLines: string[] = ['## Recent Errors'] + const errorLines: string[] = [`## ${t('bug_report_recent_errors')}`] if (capturedErrors.length > 0) { for (const err of capturedErrors) { errorLines.push(`- \`[${err.time}]\` (${err.source}) ${err.message}`) } } else { - errorLines.push('- _(none captured)_') + errorLines.push(`- _(${t('bug_report_none_captured')})_`) } return { specs, errors: errorLines.join('\n') } @@ -137,12 +137,13 @@ function assembleReport( description: string, specs: string, errors: string, + t: (key: string) => string, ): string { const desc = description.trim() return [ - '## Description', + `## ${t('bug_report_description')}`, '', - desc || '_No description provided_', + desc || `_ ${t('bug_report_no_description')} _`, '', specs, '', @@ -175,7 +176,7 @@ export function BugReportModal({ onClose }: Props) { useEffect(() => { let cancelled = false setLoading(true) - buildSystemReport().then((r) => { + buildSystemReport(t).then((r) => { if (!cancelled) { setSystem(r) setLoading(false) @@ -184,11 +185,11 @@ export function BugReportModal({ onClose }: Props) { return () => { cancelled = true } - }, []) + }, [t]) const report = useMemo( - () => (system ? assembleReport(description, system.specs, system.errors) : null), - [description, system], + () => (system ? assembleReport(description, system.specs, system.errors, t) : null), + [description, system, t], ) const handleCopy = async () => { diff --git a/src/components/modals/CategoryManagerModal.tsx b/src/components/modals/CategoryManagerModal.tsx index b60d0d8..e41ecb2 100644 --- a/src/components/modals/CategoryManagerModal.tsx +++ b/src/components/modals/CategoryManagerModal.tsx @@ -433,7 +433,7 @@ export function CategoryManagerModal({

{t('no_categories_yet')}

-

Create one above to organize your projects

+

{t('category_empty_hint')}

) : ( diff --git a/src/components/modals/CheckForUpdatesModal.tsx b/src/components/modals/CheckForUpdatesModal.tsx index 0689f83..077ee55 100644 --- a/src/components/modals/CheckForUpdatesModal.tsx +++ b/src/components/modals/CheckForUpdatesModal.tsx @@ -32,27 +32,7 @@ interface Props { const PREVIEW_VERSION = '1.0.0' -const PREVIEW_NOTES = `## What's new in v1.0.0 - The Preview Update -## 🚀 New - -- Revamped the Check for Updates modal with structured release notes -- Added screen reader announcements with an Accessibility settings tab - -## 🐛 Fixes - -- Fixed a crash when switching workspaces with pinned projects -- Fixed update checks failing silently when GitHub rate limits are hit - -## ✨ Improvements - -- Faster startup times across all platforms -- Reworked workspace modals with compact style pickers - -## ⚠️ Known Issues - -- Linux OS: AppImage won't work on some distros, use the .rpm or .deb package instead -- Windows: the taskbar may briefly show a duplicate icon until the app restarts` const PREVIEW_STATES = [ 'checking', @@ -141,7 +121,7 @@ export function CheckForUpdatesModal({ ? { type: 'available', version: PREVIEW_VERSION, - notes: PREVIEW_NOTES, + notes: t('preview_release_notes'), downloadAndInstall: () => Promise.resolve(), } : { type: 'checking' }, @@ -191,7 +171,7 @@ export function CheckForUpdatesModal({ setState({ type: 'available', version: PREVIEW_VERSION, - notes: PREVIEW_NOTES, + notes: t('preview_release_notes'), downloadAndInstall: simulateDownload, }) return @@ -255,7 +235,7 @@ export function CheckForUpdatesModal({ } catch (e) { setState({ type: 'error', message: String(e) }) } - }, [mode, githubToken, simulateDownload]) + }, [mode, githubToken, simulateDownload, t]) useEffect(() => { getVersion().then(setCurrentVersion).catch(() => setCurrentVersion(null)) @@ -277,7 +257,7 @@ export function CheckForUpdatesModal({ setState({ type: 'available', version: PREVIEW_VERSION, - notes: PREVIEW_NOTES, + notes: t('preview_release_notes'), downloadAndInstall: simulateDownload, }) break @@ -300,14 +280,14 @@ export function CheckForUpdatesModal({ setState({ type: 'portable', version: PREVIEW_VERSION, - notes: PREVIEW_NOTES, + notes: t('preview_release_notes'), }) break case 'error': setState({ type: 'error', message: - 'Preview error: GitHub API rate limit reached (HTTP 403). Add a token in Settings to keep checking.', + t('preview_error'), }) break } diff --git a/src/components/reusables/DragHandle.tsx b/src/components/reusables/DragHandle.tsx index 4719b2b..6b25d70 100644 --- a/src/components/reusables/DragHandle.tsx +++ b/src/components/reusables/DragHandle.tsx @@ -1,4 +1,5 @@ import type { DraggableAttributes, DraggableSyntheticListeners } from '@dnd-kit/core' +import { useTranslation } from 'react-i18next' import { IconGrip } from '../../lib/icons' interface DragHandleProps { @@ -18,6 +19,7 @@ export function DragHandle({ disabled = false, className = '', }: DragHandleProps) { + const { t } = useTranslation('common') if (disabled) return null return ( @@ -25,7 +27,7 @@ export function DragHandle({ ref={ref} {...attributes} {...listeners} - aria-label="Drag to reorder" + aria-label={t('drag_to_reorder')} className={`focus-ring z-20 w-5 h-10 rounded-full border flex items-center justify-center cursor-grab active:cursor-grabbing touch-none transition-all duration-200 ${ isDragging ? 'bg-accent border-accent text-white scale-110 shadow-md shadow-accent/30 opacity-100' diff --git a/src/components/reusables/LanguageFlag.tsx b/src/components/reusables/LanguageFlag.tsx index e6c513e..575a76b 100644 --- a/src/components/reusables/LanguageFlag.tsx +++ b/src/components/reusables/LanguageFlag.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next' import { US, CN, @@ -31,12 +32,13 @@ export function LanguageFlag({ className?: string title?: string }) { + const { t } = useTranslation('common') if (country === 'SYSTEM') { return ( diff --git a/src/components/reusables/LanguagePicker.tsx b/src/components/reusables/LanguagePicker.tsx index a5c3e1a..0ec3116 100644 --- a/src/components/reusables/LanguagePicker.tsx +++ b/src/components/reusables/LanguagePicker.tsx @@ -1,4 +1,5 @@ import i18n from '../../i18n' +import { useTranslation } from 'react-i18next' import { LANGUAGES, resolveLanguage, @@ -9,14 +10,14 @@ import { useSettings } from '../../hooks/useSettings' import { LanguageFlag } from './LanguageFlag' import { Dropdown } from '../ui/Dropdown' -function statusLabel(status: LanguageStatus): string { +function statusLabel(status: LanguageStatus, t: (key: string) => string): string { switch (status) { case 'complete': return '✓' case 'beta': - return 'Beta' + return t('language_beta') case 'incomplete': - return 'WIP' + return t('language_incomplete') } } @@ -36,6 +37,7 @@ export function LanguagePicker({ variant = 'dropdown', className, }: LanguagePickerProps) { + const { t: ts } = useTranslation('settings') const { settings, update } = useSettings() const current = @@ -64,10 +66,10 @@ export function LanguagePicker({ > - {lang.label} + {lang.labelKey ? ts(lang.labelKey) : lang.label} {lang.status !== 'complete' && ( - {statusLabel(lang.status)} + {statusLabel(lang.status, ts)} )} @@ -90,7 +92,7 @@ export function LanguagePicker({ className={`focus-ring cursor-pointer inline-flex items-center gap-2 px-3.5 py-2 rounded-btn bg-overlay border border-outline/50 text-xs font-medium text-ink hover:border-accent-dim transition-colors self-start ${className ?? ''}`} > - {current.label} + {current.labelKey ? ts(current.labelKey) : current.label} ({ key: lang.value, - label: lang.label, + label: lang.labelKey ? ts(lang.labelKey) : lang.label, active: isActive(lang.value), leading: , - badge: lang.status !== 'complete' ? statusLabel(lang.status) : undefined, + badge: lang.status !== 'complete' ? statusLabel(lang.status, ts) : undefined, onClick: () => handleChange(lang.value), }))} /> diff --git a/src/components/reusables/ToastContainer.tsx b/src/components/reusables/ToastContainer.tsx index 9a7cc35..174072c 100644 --- a/src/components/reusables/ToastContainer.tsx +++ b/src/components/reusables/ToastContainer.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' import { createPortal } from 'react-dom' import { AnimatePresence, motion } from 'framer-motion' import { @@ -9,6 +10,7 @@ import { import { IconCheckCircle, IconX, IconAlertTriangle } from '../../lib/icons' function ToastCard({ toast }: { toast: ToastItem }) { + const { t } = useTranslation('common') const Icon = toast.type === 'success' ? IconCheckCircle @@ -37,7 +39,7 @@ function ToastCard({ toast }: { toast: ToastItem }) { - Update available + {tc('update_available')} )} {(updateAvailable || previewUpdate) && onOpenUpdatesModal && collapsed && ( - + - +