add: 添加 i18n 翻译

This commit is contained in:
ci
2026-06-19 13:05:35 +08:00
parent 5ebe562a62
commit 73dfe12c95
8 changed files with 654 additions and 189 deletions
+387
View File
@@ -0,0 +1,387 @@
// i18n.js — minimal i18n. No framework, no build step.
//
// Two locales: "en" (default) and "zh". Locale is auto-detected from
// navigator.language on first load, then user-overridable via the
// switcher in the top nav. Persisted to localStorage.
//
// Usage:
// import { t, currentLocale, setLocale, onLocale } from "./i18n.js";
// t("remotes.title") // → "Remotes" or "Remotes 远程存储"
// setLocale("zh") // switches + notifies subscribers
// onLocale(() => renderApp()) // re-render on locale change
const STORAGE_KEY = "webgui:locale";
const SUPPORTED = ["en", "zh"];
function detectInitial() {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved && SUPPORTED.includes(saved)) return saved;
if (navigator.language && navigator.language.toLowerCase().startsWith("zh")) {
return "zh";
}
return "en";
}
let current = detectInitial();
const listeners = new Set();
// --- string catalog ---
// Keep keys flat with dotted namespaces; English is the source.
const STRINGS = {
en: {
// nav + chrome
"nav.remotes": "Remotes",
"nav.configure": "Configure",
"nav.jobs": "Jobs",
"nav.new_job": "New Job",
"loading.title": "Loading",
"loading.connecting": "Connecting to rclone RC…",
"error.title": "Something went wrong",
"error.unknown_remote": "Unknown backend",
"error.no_such_provider": (p) => `No provider named "${p}".`,
"error.no_such_remote": (r) => `No remote named "${r}".`,
"error.couldnt_load": "Couldn't load",
"error.couldnt_load_backends": "Couldn't load backends",
"error.couldnt_reach": "Couldn't reach rclone",
"error.couldnt_load_remotes": "Couldn't load remotes",
"error.couldnt_list": "Couldn't list",
"error.couldnt_load_jobs": "Couldn't load jobs",
"error.no_remotes_selected": "Pick source and destination remotes",
"error.not_connected": "Couldn't connect to rclone",
// footer
"footer.product": "Product",
"footer.docs": "Documentation",
"footer.community": "Community",
"footer.about": "About",
"footer.rclone_docs": "rclone Docs",
"footer.rcd_command": "rcd Command",
"footer.rc_api": "RC API",
"footer.forum": "Forum",
"footer.github": "GitHub",
"footer.issues": "Issues",
"footer.rclone_org": "rclone.org",
"footer.changelog": "Changelog",
"footer.faq": "FAQ",
"footer.colophon": "rclone — rsync for cloud storage.",
// language switcher
"lang.label": "Language",
// remotes view
"remotes.title": "Remotes",
"remotes.subtitle":
"Configured cloud storage providers. Click a tile to browse, hover for edit/delete.",
"remotes.new_remote": "New remote",
"remotes.loading": "Loading remotes…",
"remotes.empty_title": "No remotes configured",
"remotes.empty_body": "Click New remote above to add one from this GUI.",
"remotes.delete_confirm": (n) =>
`Delete remote ${n}? This cannot be undone.`,
"remotes.deleted": (n) => `Deleted ${n}`,
"remotes.delete_failed": "Delete failed",
"remotes.load_failed": "Failed to load remotes",
// browser view
"browser.browse_failed": "List failed",
"browser.empty_title": "Empty folder",
"browser.empty_body":
"No files here. Use Upload in the toolbar to add some.",
"browser.no_remote_title": "No remote selected",
"browser.no_remote_body": "Pick a remote to browse.",
"browser.pick_remote": "Pick a remote",
"browser.col.name": "Name",
"browser.col.size": "Size",
"browser.col.modified": "Modified",
"browser.col.actions": "Actions",
"browser.up": "../",
"browser.mkdir": "New folder",
"browser.upload": "Upload",
"browser.rename": "Rename",
"browser.delete": "Delete",
"browser.mkdir_title": "New folder",
"browser.mkdir_field": "Folder name",
"browser.mkdir_placeholder": "new-folder",
"browser.mkdir_created": (n) => `Created ${n}`,
"browser.mkdir_failed": "mkdir failed",
"browser.uploaded": (n) => `Uploaded ${n} file(s)`,
"browser.upload_failed": "Upload failed",
"browser.rename_title": (n) => `Rename ${n}`,
"browser.rename_field": "New name",
"browser.renamed": (n) => `Renamed to ${n}`,
"browser.rename_failed": "Rename failed",
"browser.delete_confirm": (p) => `Delete ${p}? This cannot be undone.`,
"browser.deleted": (p) => `Deleted ${p}`,
"browser.delete_file_failed": "Delete failed",
"browser.modal.cancel": "Cancel",
"browser.modal.ok": "OK",
// configure view
"configure.new_title": "New remote",
"configure.new_subtitle": "Pick a storage backend to configure.",
"configure.edit_title": (t) => `Edit ${t} remote`,
"configure.create_title": (t) => `New ${t} remote`,
"configure.edit_subtitle": "",
"configure.cancel": "Cancel",
"configure.filter_placeholder": "Filter backends (e.g. s3, sftp, local)…",
"configure.no_match": "No backends match.",
"configure.name": "Remote name",
"configure.section.required": "Required",
"configure.section.options": "Options",
"configure.section.advanced": "Advanced",
"configure.show_advanced": (n) => `Show advanced options (${n})`,
"configure.hide_advanced": (n) => `Hide advanced options (${n})`,
"configure.save": "Save changes",
"configure.create": "Create remote",
"configure.created": (n) => `Created ${n}`,
"configure.updated": (n) => `Updated ${n}`,
"configure.save_failed": "Save failed",
"configure.oauth_title": "OAuth required",
"configure.oauth_body": (p) =>
`${p} requires OAuth authorization, which this web GUI can't complete inside a container. Please configure it from a terminal first:`,
"configure.oauth_hint": "Once the remote exists, you can edit its non-secret options here.",
"configure.delete_section_title": "Delete this remote",
"configure.delete_section_body": (r) =>
`Permanently remove ${r} from rclone.conf.`,
"configure.delete_btn": "Delete remote",
"configure.password_unchanged": "(unchanged)",
"configure.password_secret": "secret",
"configure.example_pick": "— pick —",
"configure.example_custom": "Custom…",
"configure.example_custom_placeholder": "custom value",
// jobs view
"jobs.title": "Jobs",
"jobs.subtitle": "Running and recently completed transfers.",
"jobs.new_btn": "New Job",
"jobs.cancel": "Cancel",
"jobs.new_title": "New Job",
"jobs.new_subtitle": "Copy, sync, or move between remotes.",
"jobs.action": "Action",
"jobs.action_copy": "copy (mirror src → dst, keep both)",
"jobs.action_sync": "sync (mirror src → dst, delete extras on dst)",
"jobs.action_move": "move (mirror src → dst, delete src after)",
"jobs.source_remote": "Source remote",
"jobs.source_path": "Source path (optional)",
"jobs.dest_remote": "Destination remote",
"jobs.dest_path": "Destination path (optional)",
"jobs.path_placeholder": "folder/subfolder",
"jobs.start": "Start Job",
"jobs.no_remotes_option": "(no remotes)",
"jobs.empty_title": "No jobs yet",
"jobs.empty_body": "Use New Job to start a copy, sync, or move.",
"jobs.started": (a, id) => `Started ${a} job #${id}`,
"jobs.start_failed": "Job start failed",
"jobs.stop_confirm": (id) => `Stop job #${id}?`,
"jobs.stopped": (id) => `Stopped job #${id}`,
"jobs.stop_failed": "Stop failed",
"jobs.col.id": "#",
"jobs.col.job": "Job",
"jobs.col.status": "Status",
"jobs.col.progress": "Progress",
"jobs.col.speed": "Speed",
"jobs.col.eta": "ETA",
"jobs.col.files": "Files",
"jobs.col.errors": "Errors",
"jobs.col.action": "Action",
"jobs.status.running": "running",
"jobs.status.failed": "failed",
"jobs.status.done": "done",
"jobs.status.finished": "finished",
"jobs.stop": "Stop",
"jobs.submitted_cli": "— submitted via CLI —",
},
zh: {
"nav.remotes": "远程存储",
"nav.configure": "配置",
"nav.jobs": "任务",
"nav.new_job": "新建任务",
"loading.title": "加载中",
"loading.connecting": "正在连接 rclone RC…",
"error.title": "出错了",
"error.unknown_remote": "未知的存储后端",
"error.no_such_provider": (p) => `没有名为 "${p}" 的存储后端。`,
"error.no_such_remote": (r) => `没有名为 "${r}" 的远程存储。`,
"error.couldnt_load": "无法加载",
"error.couldnt_load_backends": "无法加载存储后端列表",
"error.couldnt_reach": "无法连接 rclone",
"error.couldnt_load_remotes": "无法加载远程存储列表",
"error.couldnt_list": "无法列出",
"error.couldnt_load_jobs": "无法加载任务列表",
"error.no_remotes_selected": "请选择源和目标远程存储",
"error.not_connected": "无法连接到 rclone",
"footer.product": "产品",
"footer.docs": "文档",
"footer.community": "社区",
"footer.about": "关于",
"footer.rclone_docs": "rclone 文档",
"footer.rcd_command": "rcd 命令",
"footer.rc_api": "RC API",
"footer.forum": "论坛",
"footer.github": "GitHub",
"footer.issues": "Issue 列表",
"footer.rclone_org": "rclone.org",
"footer.changelog": "更新日志",
"footer.faq": "常见问题",
"footer.colophon": "rclone — 云存储的 rsync。",
"lang.label": "语言",
"remotes.title": "远程存储",
"remotes.subtitle":
"已配置的云存储。点击卡片浏览文件,悬停可编辑/删除。",
"remotes.new_remote": "新建远程存储",
"remotes.loading": "正在加载远程存储…",
"remotes.empty_title": "尚未配置任何远程存储",
"remotes.empty_body": "点击上方的「新建远程存储」从 GUI 添加一个。",
"remotes.delete_confirm": (n) => `删除远程存储 ${n}? 此操作不可撤销。`,
"remotes.deleted": (n) => `已删除 ${n}`,
"remotes.delete_failed": "删除失败",
"remotes.load_failed": "加载远程存储失败",
"browser.browse_failed": "列出失败",
"browser.empty_title": "空文件夹",
"browser.empty_body": "这里没有文件。用工具栏里的「上传」添加一些。",
"browser.no_remote_title": "未选择远程存储",
"browser.no_remote_body": "请选择一个远程存储来浏览。",
"browser.pick_remote": "选择一个远程存储",
"browser.col.name": "名称",
"browser.col.size": "大小",
"browser.col.modified": "修改时间",
"browser.col.actions": "操作",
"browser.up": "返回上级",
"browser.mkdir": "新建文件夹",
"browser.upload": "上传",
"browser.rename": "重命名",
"browser.delete": "删除",
"browser.mkdir_title": "新建文件夹",
"browser.mkdir_field": "文件夹名称",
"browser.mkdir_placeholder": "新文件夹",
"browser.mkdir_created": (n) => `已创建 ${n}`,
"browser.mkdir_failed": "新建文件夹失败",
"browser.uploaded": (n) => `已上传 ${n} 个文件`,
"browser.upload_failed": "上传失败",
"browser.rename_title": (n) => `重命名 ${n}`,
"browser.rename_field": "新名称",
"browser.renamed": (n) => `已重命名为 ${n}`,
"browser.rename_failed": "重命名失败",
"browser.delete_confirm": (p) => `删除 ${p}? 此操作不可撤销。`,
"browser.deleted": (p) => `已删除 ${p}`,
"browser.delete_file_failed": "删除失败",
"browser.modal.cancel": "取消",
"browser.modal.ok": "确定",
"configure.new_title": "新建远程存储",
"configure.new_subtitle": "选择一个存储后端进行配置。",
"configure.edit_title": (t) => `编辑 ${t} 远程存储`,
"configure.create_title": (t) => `新建 ${t} 远程存储`,
"configure.edit_subtitle": "",
"configure.cancel": "取消",
"configure.filter_placeholder": "筛选后端(如 s3、sftp、local)…",
"configure.no_match": "没有匹配的后端。",
"configure.name": "远程存储名称",
"configure.section.required": "必填项",
"configure.section.options": "选项",
"configure.section.advanced": "高级",
"configure.show_advanced": (n) => `显示高级选项 (${n})`,
"configure.hide_advanced": (n) => `隐藏高级选项 (${n})`,
"configure.save": "保存修改",
"configure.create": "创建远程存储",
"configure.created": (n) => `已创建 ${n}`,
"configure.updated": (n) => `已更新 ${n}`,
"configure.save_failed": "保存失败",
"configure.oauth_title": "需要 OAuth 授权",
"configure.oauth_body": (p) =>
`${p} 需要 OAuth 授权,此 Web GUI 无法在容器内完成。请先在终端中配置:`,
"configure.oauth_hint": "远程存储创建后,可以在这里编辑其非机密选项。",
"configure.delete_section_title": "删除此远程存储",
"configure.delete_section_body": (r) =>
`从 rclone.conf 永久移除 ${r}。`,
"configure.delete_btn": "删除远程存储",
"configure.password_unchanged": "(保持不变)",
"configure.password_secret": "密钥",
"configure.example_pick": "— 选择 —",
"configure.example_custom": "自定义…",
"configure.example_custom_placeholder": "自定义值",
"jobs.title": "任务",
"jobs.subtitle": "正在运行和最近完成的传输。",
"jobs.new_btn": "新建任务",
"jobs.cancel": "取消",
"jobs.new_title": "新建任务",
"jobs.new_subtitle": "在远程存储之间复制、同步或移动。",
"jobs.action": "操作",
"jobs.action_copy": "copy(镜像 源 → 目标,两者都保留)",
"jobs.action_sync": "sync(镜像 源 → 目标,删除目标的额外文件)",
"jobs.action_move": "move(镜像 源 → 目标,完成后删除源)",
"jobs.source_remote": "源远程存储",
"jobs.source_path": "源路径(可选)",
"jobs.dest_remote": "目标远程存储",
"jobs.dest_path": "目标路径(可选)",
"jobs.path_placeholder": "文件夹/子文件夹",
"jobs.start": "开始任务",
"jobs.no_remotes_option": "(无远程存储)",
"jobs.empty_title": "尚无任务",
"jobs.empty_body": "用「新建任务」开始一个 copy、sync 或 move。",
"jobs.started": (a, id) => `已启动 ${a} 任务 #${id}`,
"jobs.start_failed": "任务启动失败",
"jobs.stop_confirm": (id) => `停止任务 #${id}?`,
"jobs.stopped": (id) => `已停止任务 #${id}`,
"jobs.stop_failed": "停止失败",
"jobs.col.id": "#",
"jobs.col.job": "任务",
"jobs.col.status": "状态",
"jobs.col.progress": "进度",
"jobs.col.speed": "速度",
"jobs.col.eta": "预计剩余",
"jobs.col.files": "文件",
"jobs.col.errors": "错误",
"jobs.col.action": "操作",
"jobs.status.running": "运行中",
"jobs.status.failed": "失败",
"jobs.status.done": "完成",
"jobs.status.finished": "已结束",
"jobs.stop": "停止",
"jobs.submitted_cli": "— 通过命令行提交 —",
},
};
export function t(key, ...args) {
const table = STRINGS[current] || STRINGS.en;
const val = table[key] ?? STRINGS.en[key] ?? key;
if (typeof val === "function") return val(...args);
return val;
}
export function currentLocale() {
return current;
}
export function setLocale(locale) {
if (!SUPPORTED.includes(locale)) return;
if (locale === current) return;
current = locale;
localStorage.setItem(STORAGE_KEY, locale);
document.documentElement.lang = locale === "zh" ? "zh-CN" : "en";
for (const fn of listeners) {
try {
fn(current);
} catch (e) {
console.error("i18n listener error", e);
}
}
}
export function onLocale(fn) {
listeners.add(fn);
return () => listeners.delete(fn);
}
export function supportedLocales() {
return SUPPORTED;
}
// Initialize <html lang> on first load.
document.documentElement.lang = current === "zh" ? "zh-CN" : "en";