i18n(web): 新增界面语言切换与自动翻译支持

- 新增语言状态管理、存储同步与 DOM 自动翻译能力,支持简体中文、繁體中文和英文。
- 在邮件页加入语言切换入口,并接入全局语言同步。
- 补充大量邮件界面文案翻译映射,提升多语言可用性。
This commit is contained in:
LanQin
2026-06-27 01:36:15 +08:00
parent bce259ca8a
commit ef7af15010
3 changed files with 493 additions and 1 deletions
+21 -1
View File
@@ -17,6 +17,7 @@ import { api, ExternalImapAccount, ExternalImapFolder, ListResponse, Mailbox, Ma
import { cn, decodeMimeHeader, formatBytes, formatDate, formatDateTime, generateLabelColor } from "@/lib/utils"
import { applyTheme, getInitialTheme } from "@/lib/theme"
import { useDisplayMode } from "@/lib/display-mode"
import { Language, languageOptions, useLanguage } from "@/lib/language"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge"
@@ -102,6 +103,7 @@ export function MailPage() {
const [expandedExternalAccountIds, setExpandedExternalAccountIds] = React.useState<string[]>([])
const [externalFolder, setExternalFolder] = React.useState("INBOX")
const [darkMode, setDarkMode] = React.useState(getInitialTheme)
const [language, setLanguage] = useLanguage()
const [displayMode] = useDisplayMode()
const isMobile = useIsMobile()
const [refreshing, setRefreshing] = React.useState(false)
@@ -980,6 +982,8 @@ export function MailPage() {
email={me.data?.user.email || selectedMailbox?.address}
darkMode={darkMode}
onToggleTheme={() => setDarkMode((value) => !value)}
language={language}
onLanguageChange={setLanguage}
onSettings={openSettings}
/>
<div className={cn("mt-2 flex gap-2", sidebarCollapsed && "justify-center")}>
@@ -2591,8 +2595,9 @@ function NewLabelButton({ collapsed, pending, onCreate, editing, onEditingChange
)
}
function AccountHeader({ collapsed, name, email, darkMode, onToggleTheme, onSettings }: { collapsed: boolean; name: string; email?: string; darkMode: boolean; onToggleTheme: () => void; onSettings: () => void }) {
function AccountHeader({ collapsed, name, email, darkMode, language, onToggleTheme, onLanguageChange, onSettings }: { collapsed: boolean; name: string; email?: string; darkMode: boolean; language: Language; onToggleTheme: () => void; onLanguageChange: (language: Language) => void; onSettings: () => void }) {
const displayName = cleanAccountName(name, email)
const currentLanguage = languageOptions.find((item) => item.value === language) || languageOptions[0]
if (collapsed) {
return (
<div className="flex justify-center">
@@ -2616,6 +2621,21 @@ function AccountHeader({ collapsed, name, email, darkMode, onToggleTheme, onSett
<Button type="button" variant="ghost" size="icon" className="size-8 rounded-md text-muted-foreground" onClick={onToggleTheme}>
{darkMode ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="button" variant="ghost" size="icon" className="size-8 rounded-md text-muted-foreground" aria-label="切换语言" title="切换语言">
<span className="text-sm font-medium leading-none">{currentLanguage.shortLabel}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
{languageOptions.map((item) => (
<DropdownMenuItem key={item.value} onSelect={() => onLanguageChange(item.value)} className="gap-2">
<span className="min-w-0 flex-1">{item.label}</span>
<Check className={cn("h-4 w-4 text-emerald-500", item.value === language ? "opacity-100" : "opacity-0")} />
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<Button type="button" variant="ghost" size="icon" className="size-8 rounded-md text-muted-foreground" onClick={onSettings}>
<Settings className="h-4 w-4" />
</Button>