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
+470
View File
@@ -0,0 +1,470 @@
import * as React from "react"
export type Language = "zh-CN" | "zh-TW" | "en"
export const LANGUAGE_STORAGE_KEY = "lanqin:language"
export const languageOptions: { value: Language; label: string; shortLabel: string; htmlLang: string }[] = [
{ value: "zh-CN", label: "简体中文", shortLabel: "简", htmlLang: "zh-CN" },
{ value: "zh-TW", label: "繁體中文", shortLabel: "繁", htmlLang: "zh-TW" },
{ value: "en", label: "English", shortLabel: "EN", htmlLang: "en" },
]
const languageValues = new Set(languageOptions.map((item) => item.value))
export function getInitialLanguage(): Language {
if (typeof window === "undefined") return "zh-CN"
const stored = window.localStorage.getItem(LANGUAGE_STORAGE_KEY)
if (stored && languageValues.has(stored as Language)) return stored as Language
const browserLanguage = window.navigator.language.toLowerCase()
if (browserLanguage.startsWith("zh-tw") || browserLanguage.startsWith("zh-hk") || browserLanguage.startsWith("zh-mo")) return "zh-TW"
if (browserLanguage.startsWith("en")) return "en"
return "zh-CN"
}
export function setStoredLanguage(language: Language) {
window.localStorage.setItem(LANGUAGE_STORAGE_KEY, language)
window.dispatchEvent(new CustomEvent("lanqin:language", { detail: language }))
}
export function useLanguage() {
const [language, setLanguageState] = React.useState<Language>(getInitialLanguage)
React.useEffect(() => {
function sync() {
setLanguageState(getInitialLanguage())
}
window.addEventListener("storage", sync)
window.addEventListener("lanqin:language", sync)
return () => {
window.removeEventListener("storage", sync)
window.removeEventListener("lanqin:language", sync)
}
}, [])
const setLanguage = React.useCallback((nextLanguage: Language) => {
setStoredLanguage(nextLanguage)
setLanguageState(nextLanguage)
}, [])
return [language, setLanguage] as const
}
type Translation = { "zh-TW": string; en: string }
const exactTranslations: Record<string, Translation> = {
"收件箱": { "zh-TW": "收件匣", en: "Inbox" },
"已发送": { "zh-TW": "已傳送", en: "Sent" },
"草稿箱": { "zh-TW": "草稿匣", en: "Drafts" },
"归档": { "zh-TW": "封存", en: "Archive" },
"垃圾邮件": { "zh-TW": "垃圾郵件", en: "Spam" },
"回收站": { "zh-TW": "垃圾桶", en: "Trash" },
"全部邮件": { "zh-TW": "全部郵件", en: "All mail" },
"未读邮件": { "zh-TW": "未讀郵件", en: "Unread" },
"星标邮件": { "zh-TW": "星號郵件", en: "Starred" },
"有附件": { "zh-TW": "有附件", en: "Has attachments" },
"切换语言": { "zh-TW": "切換語言", en: "Switch language" },
"写邮件": { "zh-TW": "寫郵件", en: "Compose" },
"邮件夹": { "zh-TW": "郵件匣", en: "Folders" },
"外部邮箱": { "zh-TW": "外部信箱", en: "External mailbox" },
"标签": { "zh-TW": "標籤", en: "Labels" },
"暂无标签": { "zh-TW": "暫無標籤", en: "No labels" },
"收起侧栏": { "zh-TW": "收合側欄", en: "Collapse sidebar" },
"选择邮箱": { "zh-TW": "選擇信箱", en: "Select mailbox" },
"没有可用邮箱": { "zh-TW": "沒有可用信箱", en: "No mailboxes available" },
"邮箱地址已复制": { "zh-TW": "信箱地址已複製", en: "Mailbox address copied" },
"打开导航": { "zh-TW": "開啟導覽", en: "Open navigation" },
"邮箱导航": { "zh-TW": "信箱導覽", en: "Mailbox navigation" },
"刷新邮件": { "zh-TW": "重新整理郵件", en: "Refresh mail" },
"自动刷新中": { "zh-TW": "自動重新整理中", en: "Auto-refreshing" },
"自动刷新中...": { "zh-TW": "自動重新整理中...", en: "Auto-refreshing..." },
"自动刷新已开启": { "zh-TW": "自動重新整理已開啟", en: "Auto-refresh enabled" },
"全部已读": { "zh-TW": "全部已讀", en: "Mark all read" },
"搜索邮件": { "zh-TW": "搜尋郵件", en: "Search mail" },
"搜索远端邮件": { "zh-TW": "搜尋遠端郵件", en: "Search remote mail" },
"搜索发送队列": { "zh-TW": "搜尋傳送佇列", en: "Search send queue" },
"搜索待发送": { "zh-TW": "搜尋待傳送", en: "Search scheduled mail" },
"选择当前页邮件": { "zh-TW": "選擇目前頁面的郵件", en: "Select messages on this page" },
"加载中...": { "zh-TW": "載入中...", en: "Loading..." },
"加载更多": { "zh-TW": "載入更多", en: "Load more" },
"选择一封邮件阅读": { "zh-TW": "選擇一封郵件閱讀", en: "Select a message to read" },
"回复": { "zh-TW": "回覆", en: "Reply" },
"转发": { "zh-TW": "轉寄", en: "Forward" },
"投递时间线": { "zh-TW": "投遞時間軸", en: "Delivery timeline" },
"取消归档": { "zh-TW": "取消封存", en: "Unarchive" },
"删除": { "zh-TW": "刪除", en: "Delete" },
"附件": { "zh-TW": "附件", en: "Attachments" },
"确认": { "zh-TW": "確認", en: "Confirm" },
"取消": { "zh-TW": "取消", en: "Cancel" },
"关闭": { "zh-TW": "關閉", en: "Close" },
"打开": { "zh-TW": "開啟", en: "Open" },
"刷新": { "zh-TW": "重新整理", en: "Refresh" },
"新建文件夹": { "zh-TW": "新增資料夾", en: "New folder" },
"文件夹名称": { "zh-TW": "資料夾名稱", en: "Folder name" },
"创建": { "zh-TW": "建立", en: "Create" },
"创建中...": { "zh-TW": "建立中...", en: "Creating..." },
"例如:客户、账单、项目归档": { "zh-TW": "例如:客戶、帳單、專案封存", en: "e.g. Clients, invoices, project archive" },
"新建标签": { "zh-TW": "新增標籤", en: "New label" },
"删除标签失败": { "zh-TW": "刪除標籤失敗", en: "Failed to delete label" },
"添加标签失败": { "zh-TW": "新增標籤失敗", en: "Failed to add label" },
"移除标签失败": { "zh-TW": "移除標籤失敗", en: "Failed to remove label" },
"创建标签失败": { "zh-TW": "建立標籤失敗", en: "Failed to create label" },
"添加标签": { "zh-TW": "新增標籤", en: "Add label" },
"请先在侧栏新建标签": { "zh-TW": "請先在側欄新增標籤", en: "Create a label in the sidebar first" },
"无": { "zh-TW": "無", en: "None" },
"无主题": { "zh-TW": "無主旨", en: "No subject" },
"(无主题)": { "zh-TW": "(無主旨)", en: "(No subject)" },
"未知发件人": { "zh-TW": "未知寄件者", en: "Unknown sender" },
"未知发件人地址": { "zh-TW": "未知寄件者地址", en: "Unknown sender address" },
"发件人": { "zh-TW": "寄件者", en: "Sender" },
"发件人地址": { "zh-TW": "寄件者地址", en: "Sender address" },
"收件人": { "zh-TW": "收件者", en: "Recipients" },
"抄送": { "zh-TW": "副本", en: "Cc" },
"密送": { "zh-TW": "密件副本", en: "Bcc" },
"投递邮箱": { "zh-TW": "投遞信箱", en: "Delivered mailbox" },
"发送时间": { "zh-TW": "傳送時間", en: "Send time" },
"接收时间": { "zh-TW": "接收時間", en: "Received time" },
"未填写收件人": { "zh-TW": "未填寫收件者", en: "No recipients" },
"邮件正文": { "zh-TW": "郵件內文", en: "Message body" },
"邮件详情": { "zh-TW": "郵件詳情", en: "Message details" },
"上一封": { "zh-TW": "上一封", en: "Previous" },
"下一封": { "zh-TW": "下一封", en: "Next" },
"更多操作": { "zh-TW": "更多操作", en: "More actions" },
"邮件不存在": { "zh-TW": "郵件不存在", en: "Message not found" },
"标为已读": { "zh-TW": "標為已讀", en: "Mark as read" },
"标为未读": { "zh-TW": "標為未讀", en: "Mark as unread" },
"添加星标": { "zh-TW": "加上星號", en: "Add star" },
"取消星标": { "zh-TW": "移除星號", en: "Remove star" },
"移入回收站": { "zh-TW": "移至垃圾桶", en: "Move to trash" },
"移入垃圾邮件": { "zh-TW": "移至垃圾郵件", en: "Move to spam" },
"批量操作": { "zh-TW": "批次操作", en: "Bulk actions" },
"移动到": { "zh-TW": "移動到", en: "Move to" },
"移到最上": { "zh-TW": "移到最上方", en: "Move to top" },
"上移一位": { "zh-TW": "上移一位", en: "Move up" },
"下移一位": { "zh-TW": "下移一位", en: "Move down" },
"移到最下": { "zh-TW": "移到最下方", en: "Move to bottom" },
"删除文件夹": { "zh-TW": "刪除資料夾", en: "Delete folder" },
"远端文件夹没有邮件": { "zh-TW": "遠端資料夾沒有郵件", en: "No messages in remote folder" },
"当前筛选条件下没有远端邮件": { "zh-TW": "目前篩選條件下沒有遠端郵件", en: "No remote messages match the current filters" },
"没有待发送邮件": { "zh-TW": "沒有待傳送郵件", en: "No scheduled mail" },
"当前搜索没有匹配的定时邮件": { "zh-TW": "目前搜尋沒有符合的定時郵件", en: "No scheduled messages match your search" },
"发送队列为空": { "zh-TW": "傳送佇列為空", en: "Send queue is empty" },
"当前搜索没有匹配的发送任务": { "zh-TW": "目前搜尋沒有符合的傳送任務", en: "No send tasks match your search" },
"当前筛选条件下没有邮件": { "zh-TW": "目前篩選條件下沒有郵件", en: "No messages match the current filters" },
"暂无星标邮件": { "zh-TW": "暫無星號郵件", en: "No starred mail" },
"当前标签没有邮件": { "zh-TW": "目前標籤沒有郵件", en: "No messages with this label" },
"收件箱暂时为空": { "zh-TW": "收件匣暫時為空", en: "Inbox is empty" },
"还没有草稿": { "zh-TW": "還沒有草稿", en: "No drafts yet" },
"还没有已发送邮件": { "zh-TW": "還沒有已傳送郵件", en: "No sent mail yet" },
"回收站是空的": { "zh-TW": "垃圾桶是空的", en: "Trash is empty" },
"暂无垃圾邮件": { "zh-TW": "暫無垃圾郵件", en: "No spam" },
"当前文件夹没有邮件": { "zh-TW": "目前資料夾沒有郵件", en: "No messages in this folder" },
"待发送": { "zh-TW": "待傳送", en: "Scheduled" },
"发送队列": { "zh-TW": "傳送佇列", en: "Send queue" },
"全部状态": { "zh-TW": "全部狀態", en: "All statuses" },
"排队中": { "zh-TW": "佇列中", en: "Queued" },
"发送中": { "zh-TW": "傳送中", en: "Sending" },
"发送失败": { "zh-TW": "傳送失敗", en: "Failed" },
"已投递": { "zh-TW": "已投遞", en: "Delivered" },
"已取消": { "zh-TW": "已取消", en: "Canceled" },
"等待发送": { "zh-TW": "等待傳送", en: "Pending" },
"清除": { "zh-TW": "清除", en: "Clear" },
"未记录收件人": { "zh-TW": "未記錄收件者", en: "No recorded recipients" },
"时间线": { "zh-TW": "時間軸", en: "Timeline" },
"重试": { "zh-TW": "重試", en: "Retry" },
"暂无投递事件": { "zh-TW": "暫無投遞事件", en: "No delivery events" },
"队列事件": { "zh-TW": "佇列事件", en: "Queue event" },
"定时发送": { "zh-TW": "定時傳送", en: "Scheduled send" },
"未知": { "zh-TW": "未知", en: "Unknown" },
"同步": { "zh-TW": "同步", en: "Sync" },
"直连": { "zh-TW": "直連", en: "Direct" },
"还没有可用邮箱": { "zh-TW": "還沒有可用信箱", en: "No mailbox available" },
"请在个人中心申请邮箱,或联系管理员为当前账号分配邮箱。": { "zh-TW": "請在個人中心申請信箱,或聯絡管理員為目前帳號分配信箱。", en: "Apply for a mailbox in Profile, or contact an administrator to assign one to this account." },
"前往个人中心": { "zh-TW": "前往個人中心", en: "Go to profile" },
"无邮箱前台权限": { "zh-TW": "無信箱前台權限", en: "No mailbox access" },
"当前账号未开启邮箱前台访问权限。": { "zh-TW": "目前帳號未開啟信箱前台存取權限。", en: "Mailbox access is not enabled for this account." },
"无邮件查看权限": { "zh-TW": "無郵件檢視權限", en: "No mail read permission" },
"当前账号可以访问邮箱前台,但未开启邮件查看权限。": { "zh-TW": "目前帳號可以存取信箱前台,但未開啟郵件檢視權限。", en: "This account can access mailbox UI, but mail reading is not enabled." },
"无定时发送权限": { "zh-TW": "無定時傳送權限", en: "No scheduled send permission" },
"当前账号不能查看或管理定时发送任务。": { "zh-TW": "目前帳號不能檢視或管理定時傳送任務。", en: "This account cannot view or manage scheduled send tasks." },
"无发送队列权限": { "zh-TW": "無傳送佇列權限", en: "No send queue permission" },
"当前账号不能查看发送队列。": { "zh-TW": "目前帳號不能檢視傳送佇列。", en: "This account cannot view the send queue." },
"操作失败": { "zh-TW": "操作失敗", en: "Operation failed" },
"请稍后重试": { "zh-TW": "請稍後重試", en: "Please try again later" },
"已取消定时发送": { "zh-TW": "已取消定時傳送", en: "Scheduled send canceled" },
"已移除失败记录": { "zh-TW": "已移除失敗記錄", en: "Failed record removed" },
"文件夹已创建": { "zh-TW": "資料夾已建立", en: "Folder created" },
"创建文件夹失败": { "zh-TW": "建立資料夾失敗", en: "Failed to create folder" },
"文件夹排序失败": { "zh-TW": "資料夾排序失敗", en: "Failed to reorder folders" },
"文件夹已删除": { "zh-TW": "資料夾已刪除", en: "Folder deleted" },
"删除文件夹失败": { "zh-TW": "刪除資料夾失敗", en: "Failed to delete folder" },
"已重新加入发送队列": { "zh-TW": "已重新加入傳送佇列", en: "Added back to send queue" },
"重试失败": { "zh-TW": "重試失敗", en: "Retry failed" },
"已取消发送任务": { "zh-TW": "已取消傳送任務", en: "Send task canceled" },
"取消失败": { "zh-TW": "取消失敗", en: "Cancel failed" },
"当前没有未读邮件": { "zh-TW": "目前沒有未讀郵件", en: "No unread messages" },
"删除所选邮件?": { "zh-TW": "刪除所選郵件?", en: "Delete selected messages?" },
"删除邮件": { "zh-TW": "刪除郵件", en: "Delete messages" },
"批量操作失败": { "zh-TW": "批次操作失敗", en: "Bulk action failed" },
"删除这封邮件?": { "zh-TW": "刪除這封郵件?", en: "Delete this message?" },
"这封草稿已在待发送队列中": { "zh-TW": "這封草稿已在待傳送佇列中", en: "This draft is already scheduled" },
"请先取消定时发送,再继续编辑。": { "zh-TW": "請先取消定時傳送,再繼續編輯。", en: "Cancel the scheduled send before editing." },
"打开草稿失败": { "zh-TW": "開啟草稿失敗", en: "Failed to open draft" },
"文件夹内的邮件会移回收件箱,不会被删除。": { "zh-TW": "資料夾內的郵件會移回收件匣,不會被刪除。", en: "Messages in this folder will be moved back to Inbox, not deleted." },
"编辑草稿": { "zh-TW": "編輯草稿", en: "Edit draft" },
"打开邮件": { "zh-TW": "開啟郵件", en: "Open message" },
"发件邮箱": { "zh-TW": "寄件信箱", en: "From mailbox" },
"未选择": { "zh-TW": "未選擇", en: "Not selected" },
"分别发送": { "zh-TW": "分別傳送", en: "Send separately" },
"主 题": { "zh-TW": "主 旨", en: "Subject" },
"输入主题": { "zh-TW": "輸入主旨", en: "Enter subject" },
"写信": { "zh-TW": "寫信", en: "Compose" },
"正在保存草稿...": { "zh-TW": "正在儲存草稿...", en: "Saving draft..." },
"草稿保存失败": { "zh-TW": "草稿儲存失敗", en: "Failed to save draft" },
"发送": { "zh-TW": "傳送", en: "Send" },
"发送中...": { "zh-TW": "傳送中...", en: "Sending..." },
"发送成功": { "zh-TW": "傳送成功", en: "Sent" },
"定时": { "zh-TW": "定時", en: "Schedule" },
"继续": { "zh-TW": "繼續", en: "Continue" },
"继续发送": { "zh-TW": "繼續傳送", en: "Continue sending" },
"继续分别发送": { "zh-TW": "繼續分別傳送", en: "Continue sending separately" },
"继续定时发送": { "zh-TW": "繼續定時傳送", en: "Continue scheduling" },
"请选择发件邮箱": { "zh-TW": "請選擇寄件信箱", en: "Select a from mailbox" },
"确认发送这封邮件?": { "zh-TW": "確認傳送這封郵件?", en: "Send this message?" },
"确认定时发送?": { "zh-TW": "確認定時傳送?", en: "Schedule this message?" },
"请选择发送时间": { "zh-TW": "請選擇傳送時間", en: "Select a send time" },
"发送时间需要晚于当前时间": { "zh-TW": "傳送時間需要晚於目前時間", en: "Send time must be in the future" },
"确认定时": { "zh-TW": "確認定時", en: "Confirm schedule" },
"正在设置...": { "zh-TW": "正在設定...", en: "Scheduling..." },
"输入正文": { "zh-TW": "輸入內文", en: "Write your message" },
"正文": { "zh-TW": "內文", en: "Body" },
"撤销": { "zh-TW": "復原", en: "Undo" },
"重做": { "zh-TW": "重做", en: "Redo" },
"插入": { "zh-TW": "插入", en: "Insert" },
"链接": { "zh-TW": "連結", en: "Link" },
"图片链接": { "zh-TW": "圖片連結", en: "Image link" },
"分隔线": { "zh-TW": "分隔線", en: "Divider" },
"日程": { "zh-TW": "行程", en: "Event" },
"表情": { "zh-TW": "表情", en: "Emoji" },
"格式": { "zh-TW": "格式", en: "Format" },
"预览": { "zh-TW": "預覽", en: "Preview" },
"签名": { "zh-TW": "簽名", en: "Signature" },
"清除格式": { "zh-TW": "清除格式", en: "Clear formatting" },
"加粗": { "zh-TW": "粗體", en: "Bold" },
"斜体": { "zh-TW": "斜體", en: "Italic" },
"下划线": { "zh-TW": "底線", en: "Underline" },
"删除线": { "zh-TW": "刪除線", en: "Strikethrough" },
"文字颜色": { "zh-TW": "文字顏色", en: "Text color" },
"高亮": { "zh-TW": "醒目提示", en: "Highlight" },
"无序列表": { "zh-TW": "項目符號清單", en: "Bulleted list" },
"有序列表": { "zh-TW": "編號清單", en: "Numbered list" },
"减少缩进": { "zh-TW": "減少縮排", en: "Decrease indent" },
"增加缩进": { "zh-TW": "增加縮排", en: "Increase indent" },
"左对齐": { "zh-TW": "靠左對齊", en: "Align left" },
"居中": { "zh-TW": "置中", en: "Center" },
"右对齐": { "zh-TW": "靠右對齊", en: "Align right" },
"引用": { "zh-TW": "引用", en: "Quote" },
"代码块": { "zh-TW": "程式碼區塊", en: "Code block" },
"邮件预览": { "zh-TW": "郵件預覽", en: "Message preview" },
"编辑链接": { "zh-TW": "編輯連結", en: "Edit link" },
"插入链接": { "zh-TW": "插入連結", en: "Insert link" },
"编辑图片": { "zh-TW": "編輯圖片", en: "Edit image" },
"插入图片": { "zh-TW": "插入圖片", en: "Insert image" },
"链接地址": { "zh-TW": "連結地址", en: "Link URL" },
"图片地址": { "zh-TW": "圖片地址", en: "Image URL" },
"显示文字": { "zh-TW": "顯示文字", en: "Display text" },
"默认使用链接地址": { "zh-TW": "預設使用連結地址", en: "Defaults to the URL" },
"替代文字": { "zh-TW": "替代文字", en: "Alt text" },
"图片说明": { "zh-TW": "圖片說明", en: "Image description" },
"更新": { "zh-TW": "更新", en: "Update" },
"默认字体": { "zh-TW": "預設字體", en: "Default font" },
"微软雅黑": { "zh-TW": "微軟正黑體", en: "Microsoft YaHei" },
"小号": { "zh-TW": "小號", en: "Small" },
"中号": { "zh-TW": "中號", en: "Medium" },
"大号": { "zh-TW": "大號", en: "Large" },
"默认": { "zh-TW": "預設", en: "Default" },
"红色": { "zh-TW": "紅色", en: "Red" },
"蓝色": { "zh-TW": "藍色", en: "Blue" },
"绿色": { "zh-TW": "綠色", en: "Green" },
"紫色": { "zh-TW": "紫色", en: "Purple" },
"黄色": { "zh-TW": "黃色", en: "Yellow" },
"粉色": { "zh-TW": "粉色", en: "Pink" },
"无高亮": { "zh-TW": "無醒目提示", en: "No highlight" },
"新建日程": { "zh-TW": "新增行程", en: "New event" },
"输入日程主题": { "zh-TW": "輸入行程主旨", en: "Enter event title" },
"请输入日程主题": { "zh-TW": "請輸入行程主旨", en: "Enter an event title" },
"开始": { "zh-TW": "開始", en: "Start" },
"全天": { "zh-TW": "全天", en: "All day" },
"持续": { "zh-TW": "持續", en: "Duration" },
"自定义": { "zh-TW": "自訂", en: "Custom" },
"提醒": { "zh-TW": "提醒", en: "Reminder" },
"重复": { "zh-TW": "重複", en: "Repeat" },
"农历": { "zh-TW": "農曆", en: "Lunar" },
"位置": { "zh-TW": "位置", en: "Location" },
"请输入位置": { "zh-TW": "請輸入位置", en: "Enter location" },
"描述": { "zh-TW": "描述", en: "Description" },
"输入描述": { "zh-TW": "輸入描述", en: "Enter description" },
"确定": { "zh-TW": "確定", en: "OK" },
"准时": { "zh-TW": "準時", en: "On time" },
"永不": { "zh-TW": "永不", en: "Never" },
"每天": { "zh-TW": "每天", en: "Daily" },
"每周": { "zh-TW": "每週", en: "Weekly" },
"每月": { "zh-TW": "每月", en: "Monthly" },
"每年": { "zh-TW": "每年", en: "Yearly" },
"明早 9 点": { "zh-TW": "明早 9 點", en: "Tomorrow 9 AM" },
"下周一 9 点": { "zh-TW": "下週一 9 點", en: "Next Monday 9 AM" },
"时间": { "zh-TW": "時間", en: "Time" },
}
const templateTranslations: { pattern: RegExp; "zh-TW": string; en: string }[] = [
{ pattern: /^已刷新 (.+)$/, "zh-TW": "已重新整理 $1", en: "Refreshed $1" },
{ pattern: /^已选 (\d+) 封$/, "zh-TW": "已選 $1 封", en: "$1 selected" },
{ pattern: /^(\d+) \/ (\d+) 封邮件$/, "zh-TW": "$1 / $2 封郵件", en: "$1 / $2 messages" },
{ pattern: /^(\d+) \/ (\d+) 封$/, "zh-TW": "$1 / $2 封", en: "$1 / $2 messages" },
{ pattern: /^(\d+) \/ (\d+) 封定时邮件$/, "zh-TW": "$1 / $2 封定時郵件", en: "$1 / $2 scheduled messages" },
{ pattern: /^(\d+) \/ (\d+) 个发送任务$/, "zh-TW": "$1 / $2 個傳送任務", en: "$1 / $2 send tasks" },
{ pattern: /^收到 (\d+) 封新邮件$/, "zh-TW": "收到 $1 封新郵件", en: "$1 new messages" },
{ pattern: /^新邮件:(.+)$/, "zh-TW": "新郵件:$1", en: "New message: $1" },
{ pattern: /^(.+) 等发来新邮件$/, "zh-TW": "$1 等寄來新郵件", en: "$1 and others sent new mail" },
{ pattern: /^已标记 (\d+) 封邮件为已读$/, "zh-TW": "已標記 $1 封郵件為已讀", en: "Marked $1 messages as read" },
{ pattern: /^已处理 (\d+) 封邮件$/, "zh-TW": "已處理 $1 封郵件", en: "Processed $1 messages" },
{ pattern: /^将删除当前选中的 (\d+) 封邮件,此操作无法从邮件列表中恢复。$/, "zh-TW": "將刪除目前選取的 $1 封郵件,此操作無法從郵件清單中復原。", en: "This will delete the $1 selected messages. This cannot be undone from the message list." },
{ pattern: /^邮件“(.+)”将被删除。$/, "zh-TW": "郵件「$1」將被刪除。", en: "Message “$1” will be deleted." },
{ pattern: /^删除文件夹“(.+)”?$/, "zh-TW": "刪除資料夾「$1」?", en: "Delete folder “$1”?" },
{ pattern: /^删除标签 (.+)$/, "zh-TW": "刪除標籤 $1", en: "Delete label $1" },
{ pattern: /^移除标签 (.+)$/, "zh-TW": "移除標籤 $1", en: "Remove label $1" },
{ pattern: /^移除 (.+)$/, "zh-TW": "移除 $1", en: "Remove $1" },
{ pattern: /^已将 (\d+) 封邮件移回收件箱$/, "zh-TW": "已將 $1 封郵件移回收件匣", en: "Moved $1 messages back to Inbox" },
{ pattern: /^发给 (.+)$/, "zh-TW": "寄給 $1", en: "To $1" },
{ pattern: /^来源:(.+)$/, "zh-TW": "來源:$1", en: "Source: $1" },
{ pattern: /^尝试:(\d+)\/(\d+)$/, "zh-TW": "嘗試:$1/$2", en: "Attempts: $1/$2" },
{ pattern: /^下次:(.+)$/, "zh-TW": "下次:$1", en: "Next: $1" },
{ pattern: /^投递于 (.+)$/, "zh-TW": "投遞於 $1", en: "Delivered at $1" },
{ pattern: /^尝试次数:(\d+)$/, "zh-TW": "嘗試次數:$1", en: "Attempts: $1" },
{ pattern: /^已分别发送 (\d+) 封邮件$/, "zh-TW": "已分別傳送 $1 封郵件", en: "Sent $1 messages separately" },
{ pattern: /^已定时发送 (.+)$/, "zh-TW": "已定時傳送 $1", en: "Scheduled for $1" },
{ pattern: /^草稿已保存 (.+)$/, "zh-TW": "草稿已儲存 $1", en: "Draft saved $1" },
{ pattern: /^发送时间:(.+)$/, "zh-TW": "傳送時間:$1", en: "Send time: $1" },
{ pattern: /^当前单个附件上限 (.+)$/, "zh-TW": "目前單個附件上限 $1", en: "Current per-attachment limit: $1" },
{ pattern: /^(\d+)分钟$/, "zh-TW": "$1分鐘", en: "$1 min" },
{ pattern: /^(\d+)分钟前$/, "zh-TW": "$1分鐘前", en: "$1 min before" },
{ pattern: /^(\d+) 分钟后$/, "zh-TW": "$1 分鐘後", en: "In $1 min" },
{ pattern: /^(\d+)小时$/, "zh-TW": "$1小時", en: "$1 hr" },
{ pattern: /^(\d+)小时前$/, "zh-TW": "$1小時前", en: "$1 hr before" },
{ pattern: /^(\d+) 小时后$/, "zh-TW": "$1 小時後", en: "In $1 hr" },
{ pattern: /^(\d+)天$/, "zh-TW": "$1天", en: "$1 day" },
{ pattern: /^(\d+)天前$/, "zh-TW": "$1天前", en: "$1 day before" },
{ pattern: /^(.+)前$/, "zh-TW": "$1前", en: "$1 before" },
]
export function translateUiText(value: string, language: Language): string {
if (language === "zh-CN" || !value) return value
const match = value.match(/^(\s*)([\s\S]*?)(\s*)$/)
const leading = match?.[1] || ""
const text = match?.[2] || value
const trailing = match?.[3] || ""
if (!text) return value
const translated = translateCore(text, language)
return `${leading}${translated}${trailing}`
}
function translateCore(text: string, language: Exclude<Language, "zh-CN">): string {
const exact = exactTranslations[text]
if (exact) return exact[language]
for (const rule of templateTranslations) {
if (rule.pattern.test(text)) return text.replace(rule.pattern, rule[language])
}
return text
}
const textSources = new WeakMap<Text, string>()
const textLastApplied = new WeakMap<Text, string>()
const attrSources = new WeakMap<Element, Partial<Record<string, string>>>()
const attrLastApplied = new WeakMap<Element, Partial<Record<string, string>>>()
const translatableAttributes = ["placeholder", "title", "aria-label"] as const
let translateTimer: number | undefined
export function LanguageDomSync() {
const [language] = useLanguage()
React.useEffect(() => {
document.documentElement.lang = languageOptions.find((item) => item.value === language)?.htmlLang || language
scheduleLocalize(language)
const observer = new MutationObserver(() => scheduleLocalize(language))
observer.observe(document.documentElement, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: [...translatableAttributes] })
return () => {
observer.disconnect()
if (translateTimer) window.clearTimeout(translateTimer)
}
}, [language])
return null
}
function scheduleLocalize(language: Language) {
if (typeof window === "undefined") return
if (translateTimer) window.clearTimeout(translateTimer)
translateTimer = window.setTimeout(() => localizeDocument(language), 0)
}
function localizeDocument(language: Language) {
if (!document.body) return
localizeElement(document.body, language)
}
function localizeElement(root: Element, language: Language) {
if (shouldSkipElement(root)) return
localizeAttributes(root, language)
for (const child of Array.from(root.childNodes)) {
if (child.nodeType === Node.TEXT_NODE) localizeTextNode(child as Text, language)
else if (child.nodeType === Node.ELEMENT_NODE) localizeElement(child as Element, language)
}
}
function localizeTextNode(node: Text, language: Language) {
if (!node.parentElement || shouldSkipElement(node.parentElement)) return
const current = node.textContent || ""
if (!current.trim()) return
let source = textSources.get(node)
const last = textLastApplied.get(node)
if (!source || (last !== undefined && current !== last && shouldTranslate(current))) {
source = current
textSources.set(node, source)
}
if (!source || !shouldTranslate(source)) return
const next = translateUiText(source, language)
textLastApplied.set(node, next)
if (current !== next) node.textContent = next
}
function localizeAttributes(element: Element, language: Language) {
if (shouldSkipElement(element)) return
for (const attr of translatableAttributes) {
const current = element.getAttribute(attr)
if (!current || !current.trim()) continue
let sources = attrSources.get(element)
if (!sources) {
sources = {}
attrSources.set(element, sources)
}
let applied = attrLastApplied.get(element)
if (!applied) {
applied = {}
attrLastApplied.set(element, applied)
}
if (!sources[attr] || (applied[attr] !== undefined && current !== applied[attr] && shouldTranslate(current))) sources[attr] = current
const source = sources[attr]
if (!source || !shouldTranslate(source)) continue
const next = translateUiText(source, language)
applied[attr] = next
if (current !== next) element.setAttribute(attr, next)
}
}
function shouldSkipElement(element: Element) {
const tag = element.tagName.toLowerCase()
if (["script", "style", "code", "pre", "textarea", "option"].includes(tag)) return true
return Boolean(element.closest("[data-lanqin-i18n-ignore], [contenteditable='true'], .ProseMirror"))
}
function shouldTranslate(value: string) {
const text = value.trim()
if (!text) return false
return Boolean(exactTranslations[text]) || templateTranslations.some((rule) => rule.pattern.test(text))
}