cdrop — 跨 OS 剪贴板与文件传输服务
Commilitia Drop:自托管的跨设备剪贴板同步与点对点文件传输。 - 后端 Go(chi / SQLite WAL / SSE Hub / WebRTC signaling + 状态机 / Relay ring buffer),编译进单个 distroless 镜像(前端 go:embed)。 - 前端 React + TanStack Router + Zustand,自实现 SSE + WebRTC P2P,NAT 受阻时回退服务端中继;聚珍(Juzhen)CJK 综合排版。 - 桌面端 Wails v2(macOS / Windows),瘦客户端复用 web。 - 鉴权 OIDC PKCE(自建 Casdoor 等),refresh_token 信封加密存系统密钥库;iOS Shortcut 用 HS256 scoped token。 架构文档与变更记录见 docs 分支(PROJECT_BRIEF / FRONTEND_DESIGN / CHANGELOG)。 本次为公开发布初始提交:完整开发历史(含部署细节)留存于私有归档,公开仓库自此干净起步。
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import { zhCN } from "./locales/zh-CN";
|
||||
import { zhTW } from "./locales/zh-TW";
|
||||
import { enUS } from "./locales/en-US";
|
||||
|
||||
// t() 仍是纯函数,方便 lib 层非 React 上下文里调用。
|
||||
// 切换语言由 store 触发:store.setLocale 会同时同步到此模块(写 localStorage
|
||||
// + 更新 module state);UI 通过 RouterProvider key={locale} 强制重挂载。
|
||||
|
||||
// zhCN 用 as const 锁定 key 集合作为类型推导基准,但值仍按 string 处理,
|
||||
// 否则 Partial<TranslationDict> 会要求其他 locale 重复同样的 literal。
|
||||
export type TranslationKey = keyof typeof zhCN;
|
||||
export type TranslationDict = Record<TranslationKey, string>;
|
||||
export type Locale = "zh-CN" | "zh-TW" | "en-US";
|
||||
|
||||
export const SUPPORTED_LOCALES: readonly Locale[] = [ "zh-CN", "zh-TW", "en-US" ] as const;
|
||||
|
||||
const dictionaries: Record<Locale, Partial<TranslationDict>> = {
|
||||
"zh-CN": zhCN,
|
||||
"zh-TW": zhTW,
|
||||
"en-US": enUS,
|
||||
};
|
||||
|
||||
const STORAGE_KEY = "cdrop.locale";
|
||||
|
||||
function readStorageLocale(): Locale | null
|
||||
{
|
||||
if (typeof window === "undefined") { return null; }
|
||||
const v = window.localStorage.getItem(STORAGE_KEY);
|
||||
if (v === "zh-CN" || v === "zh-TW" || v === "en-US") { return v; }
|
||||
return null;
|
||||
}
|
||||
|
||||
// 默认 locale:遍历 navigator.languages,先撞到一个中文标签(zh*)就按下面
|
||||
// 的细分逻辑判定简繁;先撞到 en 取英文;二者都没有则回退英文。
|
||||
//
|
||||
// 中文细分:
|
||||
// 1. 标签内含 "hans" → 简体;含 "hant" → 繁体
|
||||
// 2. 区域子标签:cn / sg → 简体;tw / hk / mo → 繁体
|
||||
// 3. 都不命中 → 简体(覆盖裸 "zh" 与未知地区)
|
||||
function detectBrowserLocale(): Locale
|
||||
{
|
||||
if (typeof navigator === "undefined") { return "en-US"; }
|
||||
const langs = navigator.languages && navigator.languages.length > 0
|
||||
? navigator.languages
|
||||
: ( navigator.language ? [ navigator.language ] : [] );
|
||||
for (const raw of langs)
|
||||
{
|
||||
const l = raw.toLowerCase();
|
||||
if (l.startsWith("zh")) { return classifyChinese(l); }
|
||||
if (l.startsWith("en")) { return "en-US"; }
|
||||
}
|
||||
return "en-US";
|
||||
}
|
||||
|
||||
function classifyChinese(lower: string): Locale
|
||||
{
|
||||
if (lower.includes("hans")) { return "zh-CN"; }
|
||||
if (lower.includes("hant")) { return "zh-TW"; }
|
||||
const parts = lower.split("-");
|
||||
for (const p of parts)
|
||||
{
|
||||
if (p === "cn" || p === "sg") { return "zh-CN"; }
|
||||
if (p === "tw" || p === "hk" || p === "mo") { return "zh-TW"; }
|
||||
}
|
||||
return "zh-CN";
|
||||
}
|
||||
|
||||
let currentLocale: Locale = readStorageLocale() ?? detectBrowserLocale();
|
||||
if (typeof document !== "undefined")
|
||||
{
|
||||
document.documentElement.lang = currentLocale;
|
||||
}
|
||||
|
||||
const PLACEHOLDER = /\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g;
|
||||
|
||||
export function setLocale(locale: Locale): void
|
||||
{
|
||||
currentLocale = locale;
|
||||
if (typeof window !== "undefined")
|
||||
{
|
||||
window.localStorage.setItem(STORAGE_KEY, locale);
|
||||
}
|
||||
if (typeof document !== "undefined")
|
||||
{
|
||||
document.documentElement.lang = locale;
|
||||
}
|
||||
}
|
||||
|
||||
export function getLocale(): Locale
|
||||
{
|
||||
return currentLocale;
|
||||
}
|
||||
|
||||
export function t(key: TranslationKey, vars?: Record<string, string | number>): string
|
||||
{
|
||||
// currentLocale 缺失的 key 自动 fallback 到 zh-CN。
|
||||
const template = dictionaries[currentLocale][key] ?? zhCN[key];
|
||||
if (!vars) { return template; }
|
||||
return template.replace(PLACEHOLDER, (_, name: string) =>
|
||||
{
|
||||
const v = vars[name];
|
||||
return v === undefined ? "" : String(v);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
import type { TranslationDict } from "../index";
|
||||
|
||||
// 缺失的 key 会自动 fallback 到 zh-CN(基准词典)。
|
||||
export const enUS: Partial<TranslationDict> = {
|
||||
// ---- app / shell -----------------------------------------------------
|
||||
"app.brand": "Commilitia Drop",
|
||||
"deviceType.browser": "Browser",
|
||||
"deviceType.macos": "macOS client",
|
||||
"deviceType.windows": "Windows client",
|
||||
"deviceType.linux": "Linux client",
|
||||
"deviceType.ios": "iOS client",
|
||||
"deviceType.shortcut": "Shortcut",
|
||||
"app.connected": "Online",
|
||||
"app.connecting": "Connecting…",
|
||||
"app.disconnectedTitle": "Reconnecting",
|
||||
"app.disconnectedHint":
|
||||
"Lost connection to the server — retrying automatically. No need to refresh.",
|
||||
|
||||
// ---- header / user menu ---------------------------------------------
|
||||
"nav.accountMenu": "Account menu",
|
||||
"nav.thisDeviceLabel": "This device: ",
|
||||
"nav.settings": "Settings",
|
||||
"nav.signOut": "Sign out",
|
||||
"nav.backToHome": "← Home",
|
||||
"nav.language": "语言 / Language",
|
||||
"nav.theme": "主题 / Theme",
|
||||
"nav.theme.light": "Light",
|
||||
"nav.theme.dark": "Dark",
|
||||
"nav.theme.system": "Follow system",
|
||||
"nav.themeToggle": "Toggle theme",
|
||||
|
||||
// ---- home -----------------------------------------------------------
|
||||
"home.greeting": "Hello, {{name}}",
|
||||
"home.deviceList.title": "Devices",
|
||||
"home.deviceList.onlineSuffix": "({{count}} online)",
|
||||
"home.deviceList.showOffline": "Show offline ({{count}})",
|
||||
"home.deviceList.empty":
|
||||
"No peer devices yet. Open the app in another tab with a different device name (Settings → Device name) to see it here.",
|
||||
"home.tabs.file": "File",
|
||||
"home.tabs.message": "Message",
|
||||
"home.tabs.clipboard": "Clipboard",
|
||||
"home.file.dropzone": "Drop a file here or click to browse",
|
||||
"home.file.selectedSize": "{{name}} ({{size}})",
|
||||
"home.file.rejected": "File rejected",
|
||||
"home.cta.selectDeviceAndFile": "Select a device and a file",
|
||||
"home.cta.selectFile": "Select a file",
|
||||
"home.cta.selectDevice": "Select a device",
|
||||
"home.cta.sendTo": "Send to {{name}}",
|
||||
"home.message.placeholder": "Type a message to {{name}}…",
|
||||
"home.message.placeholderEmpty": "Select a device first",
|
||||
"home.message.hint": "Enter to send, Shift+Enter for newline. Max 4 KB.",
|
||||
"home.message.title": "Messages",
|
||||
"home.message.countSuffix": "({{count}})",
|
||||
"home.message.clearAll": "Clear all",
|
||||
"home.transfer.active": "Active",
|
||||
"home.transfer.history": "Recent transfers",
|
||||
"home.clipboard.cloudTitle": "Latest from cloud",
|
||||
"home.clipboard.cloudFrom": "From {{name}} · {{time}}",
|
||||
"home.clipboard.cloudFromSelf": "From this device · {{time}}",
|
||||
"home.clipboard.cloudEmpty": "Nothing synced yet.",
|
||||
"home.clipboard.copyToLocal": "Copy to local clipboard",
|
||||
"home.clipboard.copyToLocalSuccess": "Copied to local clipboard",
|
||||
"home.clipboard.uploadTitle": "Sync local clipboard",
|
||||
"home.clipboard.uploadHint": "Plain text only, max 64 KB. Styles (bold / color) are stripped.",
|
||||
"home.clipboard.uploadButton": "Upload local clipboard",
|
||||
"home.clipboard.uploading": "Uploading…",
|
||||
"home.clipboard.uploadEmpty": "Local clipboard is empty",
|
||||
"home.clipboard.refresh": "Refresh",
|
||||
"home.clipboard.clear": "Clear cloud",
|
||||
"home.clipboard.clearConfirm": "Clear the cloud clipboard? All devices will see empty content.",
|
||||
"home.clipboard.clearSuccess": "Cloud clipboard cleared",
|
||||
"home.clipboard.hidden": "Content hidden — click Reveal to show",
|
||||
"home.clipboard.reveal": "Reveal",
|
||||
"home.clipboard.hide": "Hide",
|
||||
|
||||
// ---- setup ----------------------------------------------------------
|
||||
"setup.title": "Name this device",
|
||||
"setup.help": "The name is shown to your other devices. Avoid spaces, ≤32 chars.",
|
||||
"setup.field": "Device name",
|
||||
"setup.save": "Save and continue",
|
||||
|
||||
// ---- login ----------------------------------------------------------
|
||||
"login.title": "Sign in",
|
||||
"login.dev.help":
|
||||
"Dev mode: pick any user identifier. Two tabs with the same identifier simulate two devices of one user.",
|
||||
"login.dev.field": "Dev user id",
|
||||
"login.dev.continue": "Continue",
|
||||
"login.prod.help": "Sign in via Casdoor.",
|
||||
"login.prod.button": "Sign in with Casdoor",
|
||||
|
||||
// ---- oauth ----------------------------------------------------------
|
||||
"oauth.failed": "Sign-in failed",
|
||||
"oauth.signingIn": "Signing in…",
|
||||
"oauth.missingParams": "Missing 'code' or 'state' query parameter",
|
||||
|
||||
// ---- settings -------------------------------------------------------
|
||||
"settings.title": "Settings",
|
||||
"settings.currentDevice.title": "This device",
|
||||
"settings.deviceName.field": "Device name",
|
||||
"settings.deviceName.save": "Save",
|
||||
"settings.deviceName.unchanged": "Name is unchanged",
|
||||
"settings.deviceName.empty": "Name cannot be empty",
|
||||
"settings.deviceName.asciiOnly": "Device name must use ASCII characters only (letters, numbers, symbols).",
|
||||
"settings.deviceName.success": "Device name updated",
|
||||
"settings.unregister.current": "Unregister this device",
|
||||
"settings.unregister.currentHint":
|
||||
"Removes this device's registration and signs you out.",
|
||||
"settings.unregister.currentConfirm":
|
||||
"Unregistering this device will remove its registration and sign you out. Continue?",
|
||||
"settings.unregister.peerConfirm": "Remove device \"{{name}}\"?",
|
||||
"settings.peers.title": "Other devices",
|
||||
"settings.peers.empty": "No other devices.",
|
||||
"settings.peers.remove": "Remove",
|
||||
"settings.peers.removing": "Removing…",
|
||||
"settings.error.delete": "Action failed: {{message}}",
|
||||
"settings.online": "Online",
|
||||
"settings.offline": "Offline",
|
||||
"settings.lastSeen": "Last seen {{time}}",
|
||||
"settings.lastSeenNever": "Never connected",
|
||||
"settings.account.title": "Account",
|
||||
"settings.account.signOut": "Sign out",
|
||||
"settings.desktop.title": "Desktop",
|
||||
"settings.desktop.clipboardSync": "Auto-sync clipboard",
|
||||
"settings.desktop.clipboardSyncHint": "Copies are uploaded to the cloud, and clipboard updates from your other devices are applied here.",
|
||||
"settings.desktop.launchAtLogin": "Launch at login",
|
||||
"settings.desktop.launchAtLoginHint": "Start cdrop in the background after you sign in (lives in the menu bar).",
|
||||
"settings.desktop.downloadDir": "Download folder",
|
||||
"settings.desktop.downloadDirHint": "Received files are saved here. Leave unset to use the system Downloads folder.",
|
||||
"settings.desktop.downloadDirChoose": "Change…",
|
||||
"settings.desktop.downloadDirReset": "Reset to default",
|
||||
"settings.desktop.downloadDirPicker": "Choose download folder",
|
||||
"settings.desktop.ttlNote": "For security, cloud clipboard content is cleared automatically after 5 minutes.",
|
||||
|
||||
// ---- iOS Shortcut tokens ---------------------------------------------
|
||||
"settings.shortcut.title": "iOS Shortcut",
|
||||
"settings.shortcut.intro": "Issue a long-lived, clipboard-only, revocable token for the iOS Shortcuts app. Even if leaked it can only read/write the clipboard — nothing else.",
|
||||
"settings.shortcut.labelField": "Label",
|
||||
"settings.shortcut.labelPlaceholder": "e.g. My iPhone",
|
||||
"settings.shortcut.issue": "Issue",
|
||||
"settings.shortcut.empty": "No tokens issued yet.",
|
||||
"settings.shortcut.loadError": "Failed to load tokens.",
|
||||
"settings.shortcut.retry": "Retry",
|
||||
"settings.shortcut.revoke": "Revoke",
|
||||
"settings.shortcut.revoking": "Revoking…",
|
||||
"settings.shortcut.revokeConfirm": "Revoking immediately disables any Shortcut using this token. Continue?",
|
||||
"settings.shortcut.revoked": "Revoked",
|
||||
"settings.shortcut.revokedToast": "Token revoked",
|
||||
"settings.shortcut.expired": "Expired",
|
||||
"settings.shortcut.expiresAt": "Expires {{date}}",
|
||||
"settings.shortcut.lastUsed": "Last used {{time}}",
|
||||
"settings.shortcut.neverUsed": "Never used",
|
||||
"settings.shortcut.unavailable": "Shortcut tokens are not enabled on the server.",
|
||||
"settings.shortcut.tooMany": "You have reached the active-token limit. Revoke an old one first.",
|
||||
"settings.shortcut.issueError": "Failed: {{message}}",
|
||||
"settings.shortcut.issuedTitle": "Token issued",
|
||||
"settings.shortcut.onceWarning": "This token is shown only once. Copy it now — you won't be able to see it again after closing.",
|
||||
"settings.shortcut.tokenLabel": "Token (Bearer)",
|
||||
"settings.shortcut.copy": "Copy",
|
||||
"settings.shortcut.copyFailed": "Copy failed — select and copy manually.",
|
||||
"settings.shortcut.copied": "Copied",
|
||||
"settings.shortcut.baseLabel": "Server address",
|
||||
"settings.shortcut.guideTitle": "Set up on iOS (manual sync)",
|
||||
"settings.shortcut.guideIntro": "Build two shortcuts in the iPhone Shortcuts app, filling in the server address and token above on first run:",
|
||||
"settings.shortcut.guideDeviceName": "Add header X-Device-Name: your-device-name (ASCII only) to both shortcuts so it shows as one device in the list.",
|
||||
"settings.shortcut.guidePullTitle": "Pull (cloud → local clipboard)",
|
||||
"settings.shortcut.guidePull1": "'Get Contents of URL': set the URL to your-server-address + /api/clipboard, method GET, add header Authorization: Bearer your-token.",
|
||||
"settings.shortcut.guidePull2": "'Get Dictionary Value': key content.",
|
||||
"settings.shortcut.guidePull3": "If non-empty, 'Copy to Clipboard'; optionally add 'Show Notification'.",
|
||||
"settings.shortcut.guidePushTitle": "Push (local clipboard → cloud)",
|
||||
"settings.shortcut.guidePush1": "'Get Clipboard'; stop if empty.",
|
||||
"settings.shortcut.guidePush2": "'Get Contents of URL': same URL, method PUT, headers Authorization: Bearer your-token and Content-Type: application/json, JSON body: {\"content\": clipboard, \"content_type\": \"text/plain\"}.",
|
||||
"settings.shortcut.guidePush3": "Optionally add 'Show Notification'.",
|
||||
"settings.shortcut.guideToleranceNote": "A single run may fail on a flaky network — just retry. The automatic polling version (next phase) retries silently in the background.",
|
||||
"settings.shortcut.done": "Done",
|
||||
|
||||
// ---- transfer states / modes ----------------------------------------
|
||||
"transfer.savedTo": "Saved to {{path}}",
|
||||
"transfer.saveFailed": "Failed to save file: {{error}}",
|
||||
"transfer.action.cancel": "Cancel",
|
||||
"transfer.action.delete": "Delete",
|
||||
"transfer.action.relayNow": "Relay now",
|
||||
"transfer.action.details": "Details",
|
||||
"transfer.state.PENDING": "Pending",
|
||||
"transfer.state.ACCEPTED": "Accepted",
|
||||
"transfer.state.P2P_ACTIVE": "Transferring",
|
||||
"transfer.state.RELAY_ACTIVE": "Relaying",
|
||||
"transfer.state.DONE": "Done",
|
||||
"transfer.state.FAILED": "Failed",
|
||||
"transfer.state.CANCELLED": "Cancelled",
|
||||
"transfer.mode.p2p": "P2P",
|
||||
"transfer.mode.relay": "Relay",
|
||||
|
||||
// ---- client phase (real-time telemetry; tells "waiting" from "stuck") ----
|
||||
"transfer.phase.initializing": "Initializing…",
|
||||
"transfer.phase.waiting_accept": "Waiting for the other side to accept…",
|
||||
"transfer.phase.ice_gathering": "Gathering network candidates…",
|
||||
"transfer.phase.ice_checking": "Establishing direct tunnel…",
|
||||
"transfer.phase.ice_connected": "Tunnel established",
|
||||
"transfer.phase.dc_open": "Channel open",
|
||||
"transfer.phase.transferring": "Transferring",
|
||||
"transfer.phase.completing": "Finalizing…",
|
||||
"transfer.phase.fallback_pending": "Falling back to relay…",
|
||||
"transfer.phase.relay_uploading": "Uploading via relay",
|
||||
"transfer.phase.relay_downloading": "Downloading via relay",
|
||||
"transfer.stalled": "Looks stalled ({{seconds}}s without progress)",
|
||||
"transfer.bytesProgress": "{{sent}} / {{total}}",
|
||||
"transfer.bytesRate": "{{rate}}/s",
|
||||
|
||||
// ---- debug expansion ------------------------------------------------
|
||||
"transfer.debug.toggle": "Debug",
|
||||
"transfer.debug.iceGathering": "Gathering",
|
||||
"transfer.debug.iceConnection": "Connection",
|
||||
"transfer.debug.candidatesLocal": "Local candidates",
|
||||
"transfer.debug.candidatesRemote": "Remote candidates",
|
||||
"transfer.debug.selectedPair": "Selected pair",
|
||||
"transfer.debug.noPair": "No nominated pair yet",
|
||||
"transfer.debug.candidatesLine":
|
||||
"host {{host}} · mDNS {{mdns}} · srflx {{srflx}} · prflx {{prflx}} · relay {{relay}}",
|
||||
"transfer.debug.dataChannel": "Data channel",
|
||||
"transfer.debug.dcLine": "{{state}} · buffered {{buffered}}",
|
||||
|
||||
// ---- time -----------------------------------------------------------
|
||||
"time.justNow": "just now",
|
||||
"time.secondsAgo": "{{n}}s ago",
|
||||
"time.minutesAgo": "{{n}}m ago",
|
||||
"time.hoursAgo": "{{n}}h ago",
|
||||
|
||||
// ---- common ---------------------------------------------------------
|
||||
"common.dismiss": "Dismiss",
|
||||
"common.cancel": "Cancel",
|
||||
"common.confirm": "Confirm",
|
||||
|
||||
// ---- errors ---------------------------------------------------------
|
||||
"errors.messageEmpty": "Message is empty",
|
||||
"errors.messageOverflow": "Message exceeds 4 KB; send a file instead",
|
||||
"errors.noReceiver": "No receiver selected",
|
||||
"errors.devTokenMissing":
|
||||
"Dev mode: VITE_CDROP_DEV_TOKEN is not set in .env.local",
|
||||
"errors.noAccessToken": "No access token; user must log in",
|
||||
"errors.clipboardUnavailable": "Browser clipboard API unavailable (requires HTTPS + permission)",
|
||||
"errors.clipboardOverflow": "Content too large; exceeds {{max}} byte limit",
|
||||
"errors.clipboardWriteFailed": "Failed to write to local clipboard: {{message}}",
|
||||
};
|
||||
@@ -0,0 +1,240 @@
|
||||
// zh-CN 是 i18n 类型推导的基准词典:必须包含全部 key。其他 locale
|
||||
// 缺失的 key 会自动 fallback 到此处。
|
||||
export const zhCN = {
|
||||
// ---- 应用 / 全局 -----------------------------------------------------
|
||||
"app.brand": "Commilitia Drop",
|
||||
"deviceType.browser": "浏览器",
|
||||
"deviceType.macos": "macOS 客户端",
|
||||
"deviceType.windows": "Windows 客户端",
|
||||
"deviceType.linux": "Linux 客户端",
|
||||
"deviceType.ios": "iOS 客户端",
|
||||
"deviceType.shortcut": "快捷指令",
|
||||
"app.connected": "已连接",
|
||||
"app.connecting": "正在连接…",
|
||||
"app.disconnectedTitle": "正在重连",
|
||||
"app.disconnectedHint": "与服务器的连接已断开,正在自动重连,无需刷新页面。",
|
||||
|
||||
// ---- 顶部栏 / 用户菜单 ------------------------------------------------
|
||||
"nav.accountMenu": "账号菜单",
|
||||
"nav.thisDeviceLabel": "本机:",
|
||||
"nav.settings": "设置",
|
||||
"nav.signOut": "退出登录",
|
||||
"nav.backToHome": "← 主页",
|
||||
"nav.language": "语言 / Language",
|
||||
"nav.theme": "主题 / Theme",
|
||||
"nav.theme.light": "浅色",
|
||||
"nav.theme.dark": "深色",
|
||||
"nav.theme.system": "跟随系统",
|
||||
"nav.themeToggle": "切换主题",
|
||||
|
||||
// ---- 主页 ------------------------------------------------------------
|
||||
"home.greeting": "你好,{{name}}",
|
||||
"home.deviceList.title": "设备",
|
||||
"home.deviceList.onlineSuffix": "({{count}} 台在线)",
|
||||
"home.deviceList.showOffline": "显示离线设备({{count}})",
|
||||
"home.deviceList.empty":
|
||||
"暂无其他设备。在另一标签页或设备上以相同账户登录,并设置一个不同的设备名(设置 → 设备名称),即可在此处看到它。",
|
||||
"home.tabs.file": "文件",
|
||||
"home.tabs.message": "消息",
|
||||
"home.tabs.clipboard": "剪贴板",
|
||||
"home.file.dropzone": "拖放文件到此处或点击选择",
|
||||
"home.file.selectedSize": "{{name}}({{size}})",
|
||||
"home.file.rejected": "文件已拒绝",
|
||||
"home.cta.selectDeviceAndFile": "请选择设备与文件",
|
||||
"home.cta.selectFile": "请选择文件",
|
||||
"home.cta.selectDevice": "请选择设备",
|
||||
"home.cta.sendTo": "发送到 {{name}}",
|
||||
"home.message.placeholder": "发送消息到 {{name}}…",
|
||||
"home.message.placeholderEmpty": "请先选择设备",
|
||||
"home.message.hint": "回车发送,Shift + 回车换行,最长4 KB。",
|
||||
"home.message.title": "消息",
|
||||
"home.message.countSuffix": "({{count}})",
|
||||
"home.message.clearAll": "全部清除",
|
||||
"home.transfer.active": "进行中",
|
||||
"home.transfer.history": "最近的传输",
|
||||
"home.clipboard.cloudTitle": "云端最新",
|
||||
"home.clipboard.cloudFrom": "来自 {{name}} · {{time}}",
|
||||
"home.clipboard.cloudFromSelf": "来自本机 · {{time}}",
|
||||
"home.clipboard.cloudEmpty": "尚未同步任何内容。",
|
||||
"home.clipboard.copyToLocal": "复制到本机剪贴板",
|
||||
"home.clipboard.copyToLocalSuccess": "已复制到本机剪贴板",
|
||||
"home.clipboard.uploadTitle": "同步本机剪贴板",
|
||||
"home.clipboard.uploadHint": "仅纯文本,最大64 KB;样式(粗体/颜色等)会被剥除。",
|
||||
"home.clipboard.uploadButton": "上传本机剪贴板",
|
||||
"home.clipboard.uploading": "上传中…",
|
||||
"home.clipboard.uploadEmpty": "本机剪贴板为空",
|
||||
"home.clipboard.refresh": "刷新",
|
||||
"home.clipboard.clear": "清空云端",
|
||||
"home.clipboard.clearConfirm": "确认清空云端剪贴板?所有设备将看到空内容。",
|
||||
"home.clipboard.clearSuccess": "已清空云端剪贴板",
|
||||
"home.clipboard.hidden": "内容已隐藏 · 点击“显示”查看",
|
||||
"home.clipboard.reveal": "显示",
|
||||
"home.clipboard.hide": "隐藏",
|
||||
|
||||
// ---- 首次设置 --------------------------------------------------------
|
||||
"setup.title": "为本机命名",
|
||||
"setup.help": "此名称会展示给你的其他设备。建议避免空格,长度不超过 32 字符。",
|
||||
"setup.field": "设备名称",
|
||||
"setup.save": "保存并继续",
|
||||
|
||||
// ---- 登录 ------------------------------------------------------------
|
||||
"login.title": "登录",
|
||||
"login.dev.help":
|
||||
"Dev模式:随意填写一个用户标识。同标识的两个标签页将模拟同一用户的两台设备。",
|
||||
"login.dev.field": "Dev用户标识",
|
||||
"login.dev.continue": "继续",
|
||||
"login.prod.help": "通过Casdoor登录。",
|
||||
"login.prod.button": "使用Casdoor登录",
|
||||
|
||||
// ---- OAuth 回调 -------------------------------------------------------
|
||||
"oauth.failed": "登录失败",
|
||||
"oauth.signingIn": "正在登录…",
|
||||
"oauth.missingParams": "缺少code或state查询参数",
|
||||
|
||||
// ---- 设置页 ----------------------------------------------------------
|
||||
"settings.title": "设置",
|
||||
"settings.currentDevice.title": "当前设备",
|
||||
"settings.deviceName.field": "设备名称",
|
||||
"settings.deviceName.save": "保存",
|
||||
"settings.deviceName.unchanged": "新名称与当前一致",
|
||||
"settings.deviceName.empty": "名称不能为空",
|
||||
"settings.deviceName.asciiOnly": "设备名称只能使用 ASCII 字符(英文字母、数字、符号)。",
|
||||
"settings.deviceName.success": "设备名称已更新",
|
||||
"settings.unregister.current": "注销当前设备",
|
||||
"settings.unregister.currentHint": "将退出登录并移除此设备的注册。",
|
||||
"settings.unregister.currentConfirm":
|
||||
"注销当前设备会移除此设备的注册并退出登录,确认继续?",
|
||||
"settings.unregister.peerConfirm": "确认要移除设备“{{name}}”?",
|
||||
"settings.peers.title": "其他设备",
|
||||
"settings.peers.empty": "暂无其他设备。",
|
||||
"settings.peers.remove": "移除",
|
||||
"settings.peers.removing": "正在移除…",
|
||||
"settings.error.delete": "操作失败:{{message}}",
|
||||
"settings.online": "在线",
|
||||
"settings.offline": "离线",
|
||||
"settings.lastSeen": "上次活跃 {{time}}",
|
||||
"settings.lastSeenNever": "尚未上线",
|
||||
"settings.account.title": "账号",
|
||||
"settings.account.signOut": "退出登录",
|
||||
"settings.desktop.title": "桌面",
|
||||
"settings.desktop.clipboardSync": "剪贴板自动同步",
|
||||
"settings.desktop.clipboardSyncHint": "复制即上传到云端,并接收其他设备的剪贴板更新。",
|
||||
"settings.desktop.launchAtLogin": "开机自启",
|
||||
"settings.desktop.launchAtLoginHint": "登录系统后自动在后台启动 cdrop(菜单栏常驻)。",
|
||||
"settings.desktop.downloadDir": "下载目录",
|
||||
"settings.desktop.downloadDirHint": "接收到的文件保存到此目录;留空则使用系统下载目录。",
|
||||
"settings.desktop.downloadDirChoose": "更改…",
|
||||
"settings.desktop.downloadDirReset": "恢复默认",
|
||||
"settings.desktop.downloadDirPicker": "选择下载目录",
|
||||
"settings.desktop.ttlNote": "出于安全考虑,云端剪贴板内容会在 5 分钟后自动清除。",
|
||||
|
||||
// ---- iOS 快捷指令令牌 ------------------------------------------------
|
||||
"settings.shortcut.title": "iOS 快捷指令",
|
||||
"settings.shortcut.intro": "为 iOS“快捷指令”签发长效、仅限剪贴板、可随时吊销的专用令牌。即便令牌泄漏也只能读写剪贴板、无法触及其他数据。",
|
||||
"settings.shortcut.labelField": "备注名称",
|
||||
"settings.shortcut.labelPlaceholder": "例如:我的 iPhone",
|
||||
"settings.shortcut.issue": "签发",
|
||||
"settings.shortcut.empty": "尚未签发任何令牌。",
|
||||
"settings.shortcut.loadError": "令牌列表加载失败。",
|
||||
"settings.shortcut.retry": "重试",
|
||||
"settings.shortcut.revoke": "吊销",
|
||||
"settings.shortcut.revoking": "正在吊销…",
|
||||
"settings.shortcut.revokeConfirm": "吊销后使用此令牌的快捷指令将立即失效,确认继续?",
|
||||
"settings.shortcut.revoked": "已吊销",
|
||||
"settings.shortcut.revokedToast": "令牌已吊销",
|
||||
"settings.shortcut.expired": "已过期",
|
||||
"settings.shortcut.expiresAt": "到期 {{date}}",
|
||||
"settings.shortcut.lastUsed": "上次使用 {{time}}",
|
||||
"settings.shortcut.neverUsed": "尚未使用",
|
||||
"settings.shortcut.unavailable": "服务器未启用快捷指令令牌。",
|
||||
"settings.shortcut.tooMany": "活跃令牌已达上限,请先吊销旧令牌。",
|
||||
"settings.shortcut.issueError": "操作失败:{{message}}",
|
||||
"settings.shortcut.issuedTitle": "令牌已签发",
|
||||
"settings.shortcut.onceWarning": "令牌仅显示这一次,请立即复制保存;关闭后无法再次查看。",
|
||||
"settings.shortcut.tokenLabel": "令牌(Bearer)",
|
||||
"settings.shortcut.copy": "复制",
|
||||
"settings.shortcut.copyFailed": "复制失败,请手动选择复制",
|
||||
"settings.shortcut.copied": "已复制",
|
||||
"settings.shortcut.baseLabel": "服务器地址",
|
||||
"settings.shortcut.guideTitle": "在 iOS 上手动搭建(手动同步)",
|
||||
"settings.shortcut.guideIntro": "在 iPhone 的“快捷指令”App 中新建两个指令,首次运行时填入上面的服务器地址与令牌:",
|
||||
"settings.shortcut.guideDeviceName": "两个指令都加请求头 X-Device-Name: 你的设备名(仅 ASCII),让它在设备列表里统一显示。",
|
||||
"settings.shortcut.guidePullTitle": "拉取(云端 → 本机剪贴板)",
|
||||
"settings.shortcut.guidePull1": "“获取 URL 内容”:地址设为 你的服务器地址 加 /api/clipboard,方法 GET,请求头加 Authorization: Bearer 你的令牌。",
|
||||
"settings.shortcut.guidePull2": "“获取词典值”:取键 content。",
|
||||
"settings.shortcut.guidePull3": "内容非空时用“拷贝到剪贴板”写入,可再加“显示通知”提示已拉取。",
|
||||
"settings.shortcut.guidePushTitle": "上传(本机剪贴板 → 云端)",
|
||||
"settings.shortcut.guidePush1": "“获取剪贴板”;若为空则停止。",
|
||||
"settings.shortcut.guidePush2": "“获取 URL 内容”:地址同上,方法 PUT,请求头加 Authorization: Bearer 你的令牌 与 Content-Type: application/json,请求体选 JSON:{\"content\": 剪贴板, \"content_type\": \"text/plain\"}。",
|
||||
"settings.shortcut.guidePush3": "可加“显示通知”提示已上传。",
|
||||
"settings.shortcut.guideToleranceNote": "网络不稳定时单次运行可能失败,重试即可;自动轮询版会在后台静默重试(下一阶段提供)。",
|
||||
"settings.shortcut.done": "完成",
|
||||
|
||||
// ---- 传输状态 / 模式 -------------------------------------------------
|
||||
"transfer.savedTo": "已保存到 {{path}}",
|
||||
"transfer.saveFailed": "保存文件失败:{{error}}",
|
||||
"transfer.action.cancel": "取消",
|
||||
"transfer.action.delete": "删除",
|
||||
"transfer.action.relayNow": "立即中继",
|
||||
"transfer.action.details": "详情",
|
||||
"transfer.state.PENDING": "等待中",
|
||||
"transfer.state.ACCEPTED": "已接受",
|
||||
"transfer.state.P2P_ACTIVE": "传输中",
|
||||
"transfer.state.RELAY_ACTIVE": "中继中",
|
||||
"transfer.state.DONE": "已完成",
|
||||
"transfer.state.FAILED": "失败",
|
||||
"transfer.state.CANCELLED": "已取消",
|
||||
"transfer.mode.p2p": "P2P",
|
||||
"transfer.mode.relay": "中继",
|
||||
|
||||
// ---- 客户端 phase(实时遥测,区分等待中和卡住)----------------------
|
||||
"transfer.phase.initializing": "正在初始化…",
|
||||
"transfer.phase.waiting_accept": "等待对端接受…",
|
||||
"transfer.phase.ice_gathering": "收集网络候选…",
|
||||
"transfer.phase.ice_checking": "建立直连通道…",
|
||||
"transfer.phase.ice_connected": "通道已连接",
|
||||
"transfer.phase.dc_open": "信道已打开",
|
||||
"transfer.phase.transferring": "传输中",
|
||||
"transfer.phase.completing": "收尾中…",
|
||||
"transfer.phase.fallback_pending": "切换到中继路径…",
|
||||
"transfer.phase.relay_uploading": "中继上传中",
|
||||
"transfer.phase.relay_downloading": "中继下载中",
|
||||
"transfer.stalled": "似乎卡住了({{seconds}} 秒无进度)",
|
||||
"transfer.bytesProgress": "{{sent}} / {{total}}",
|
||||
"transfer.bytesRate": "{{rate}}/秒",
|
||||
|
||||
// ---- 调试展开区 -----------------------------------------------------
|
||||
"transfer.debug.toggle": "调试信息",
|
||||
"transfer.debug.iceGathering": "候选收集",
|
||||
"transfer.debug.iceConnection": "连接状态",
|
||||
"transfer.debug.candidatesLocal": "本端候选",
|
||||
"transfer.debug.candidatesRemote": "对端候选",
|
||||
"transfer.debug.selectedPair": "选中候选对",
|
||||
"transfer.debug.noPair": "尚未选定候选对",
|
||||
"transfer.debug.candidatesLine":
|
||||
"host {{host}} · mDNS {{mdns}} · srflx {{srflx}} · prflx {{prflx}} · relay {{relay}}",
|
||||
"transfer.debug.dataChannel": "数据信道",
|
||||
"transfer.debug.dcLine": "{{state}} · 待发送 {{buffered}}",
|
||||
|
||||
// ---- 时间 ------------------------------------------------------------
|
||||
"time.justNow": "刚刚",
|
||||
"time.secondsAgo": "{{n}} 秒前",
|
||||
"time.minutesAgo": "{{n}} 分钟前",
|
||||
"time.hoursAgo": "{{n}} 小时前",
|
||||
|
||||
// ---- 通用 ------------------------------------------------------------
|
||||
"common.dismiss": "移除",
|
||||
"common.cancel": "取消",
|
||||
"common.confirm": "确认",
|
||||
|
||||
// ---- 错误(lib/* 中抛出,呈现到 Alert)-------------------------------
|
||||
"errors.messageEmpty": "消息为空",
|
||||
"errors.messageOverflow": "消息超过4 KB;请改用文件传输",
|
||||
"errors.noReceiver": "未选择接收方",
|
||||
"errors.devTokenMissing":
|
||||
"Dev模式:未在.env.local设置VITE_CDROP_DEV_TOKEN",
|
||||
"errors.noAccessToken": "无访问令牌:用户必须先登录",
|
||||
"errors.clipboardUnavailable": "浏览器不支持剪贴板API(需HTTPS与权限)",
|
||||
"errors.clipboardOverflow": "内容过大,超过 {{max}} 字节上限",
|
||||
"errors.clipboardWriteFailed": "写入本机剪贴板失败:{{message}}",
|
||||
} as const;
|
||||
@@ -0,0 +1,244 @@
|
||||
import type { TranslationDict } from "../index";
|
||||
|
||||
// 繁體中文(臺灣用語)。在地化詞彙:檔案 / 訊息 / 剪貼簿 / 設定 / 裝置 / 線上 /
|
||||
// 連線 / 重新整理 / 伺服器 / 使用者 / 位元組 等。引號使用「」/『』直角形式
|
||||
// (與簡體 "" 不同),切勿在此檔案執行 MatchedQuotesToFullwidth。
|
||||
// 缺失的 key 會自動 fallback 到 zh-CN(基準詞典)。
|
||||
export const zhTW: Partial<TranslationDict> = {
|
||||
// ---- 應用 / 全域 -----------------------------------------------------
|
||||
"app.brand": "Commilitia Drop",
|
||||
"deviceType.browser": "瀏覽器",
|
||||
"deviceType.macos": "macOS 用戶端",
|
||||
"deviceType.windows": "Windows 用戶端",
|
||||
"deviceType.linux": "Linux 用戶端",
|
||||
"deviceType.ios": "iOS 用戶端",
|
||||
"deviceType.shortcut": "捷徑",
|
||||
"app.connected": "已連線",
|
||||
"app.connecting": "正在連線…",
|
||||
"app.disconnectedTitle": "正在重新連線",
|
||||
"app.disconnectedHint": "與伺服器的連線已中斷,正在自動重新連線,無需重新整理頁面。",
|
||||
|
||||
// ---- 頂部列 / 使用者選單 ---------------------------------------------
|
||||
"nav.accountMenu": "帳號選單",
|
||||
"nav.thisDeviceLabel": "本機:",
|
||||
"nav.settings": "設定",
|
||||
"nav.signOut": "登出",
|
||||
"nav.backToHome": "← 首頁",
|
||||
"nav.language": "語言 / Language",
|
||||
"nav.theme": "主題 / Theme",
|
||||
"nav.theme.light": "淺色",
|
||||
"nav.theme.dark": "深色",
|
||||
"nav.theme.system": "跟隨系統",
|
||||
"nav.themeToggle": "切換主題",
|
||||
|
||||
// ---- 首頁 ------------------------------------------------------------
|
||||
"home.greeting": "你好,{{name}}",
|
||||
"home.deviceList.title": "裝置",
|
||||
"home.deviceList.onlineSuffix": "({{count}} 部線上)",
|
||||
"home.deviceList.showOffline": "顯示離線裝置({{count}})",
|
||||
"home.deviceList.empty":
|
||||
"尚無其他裝置。在另一個分頁或裝置上以同一帳號登入,並設定一個不同的裝置名稱(設定 → 裝置名稱),即可在此處看到它。",
|
||||
"home.tabs.file": "檔案",
|
||||
"home.tabs.message": "訊息",
|
||||
"home.tabs.clipboard": "剪貼簿",
|
||||
"home.file.dropzone": "拖曳檔案到此處或點選選擇",
|
||||
"home.file.selectedSize": "{{name}}({{size}})",
|
||||
"home.file.rejected": "檔案已拒絕",
|
||||
"home.cta.selectDeviceAndFile": "請選擇裝置與檔案",
|
||||
"home.cta.selectFile": "請選擇檔案",
|
||||
"home.cta.selectDevice": "請選擇裝置",
|
||||
"home.cta.sendTo": "傳送到 {{name}}",
|
||||
"home.message.placeholder": "傳送訊息給 {{name}}…",
|
||||
"home.message.placeholderEmpty": "請先選擇裝置",
|
||||
"home.message.hint": "Enter傳送,Shift + Enter換行,最長4 KB。",
|
||||
"home.message.title": "訊息",
|
||||
"home.message.countSuffix": "({{count}})",
|
||||
"home.message.clearAll": "全部清除",
|
||||
"home.transfer.active": "進行中",
|
||||
"home.transfer.history": "最近的傳輸",
|
||||
"home.clipboard.cloudTitle": "雲端最新",
|
||||
"home.clipboard.cloudFrom": "來自 {{name}} · {{time}}",
|
||||
"home.clipboard.cloudFromSelf": "來自本機 · {{time}}",
|
||||
"home.clipboard.cloudEmpty": "尚未同步任何內容。",
|
||||
"home.clipboard.copyToLocal": "複製到本機剪貼簿",
|
||||
"home.clipboard.copyToLocalSuccess": "已複製到本機剪貼簿",
|
||||
"home.clipboard.uploadTitle": "同步本機剪貼簿",
|
||||
"home.clipboard.uploadHint": "僅純文字,最大64 KB;樣式(粗體 / 顏色等)會被剝除。",
|
||||
"home.clipboard.uploadButton": "上傳本機剪貼簿",
|
||||
"home.clipboard.uploading": "上傳中…",
|
||||
"home.clipboard.uploadEmpty": "本機剪貼簿為空",
|
||||
"home.clipboard.refresh": "重新整理",
|
||||
"home.clipboard.clear": "清空雲端",
|
||||
"home.clipboard.clearConfirm": "確認清空雲端剪貼簿?所有裝置將看到空內容。",
|
||||
"home.clipboard.clearSuccess": "已清空雲端剪貼簿",
|
||||
"home.clipboard.hidden": "內容已隱藏 · 點擊「顯示」查看",
|
||||
"home.clipboard.reveal": "顯示",
|
||||
"home.clipboard.hide": "隱藏",
|
||||
|
||||
// ---- 首次設定 --------------------------------------------------------
|
||||
"setup.title": "為本機命名",
|
||||
"setup.help": "此名稱會顯示給你的其他裝置。建議避免空格,長度不超過 32 個字元。",
|
||||
"setup.field": "裝置名稱",
|
||||
"setup.save": "儲存並繼續",
|
||||
|
||||
// ---- 登入 ------------------------------------------------------------
|
||||
"login.title": "登入",
|
||||
"login.dev.help":
|
||||
"Dev模式:隨意填寫一個使用者識別碼。同識別碼的兩個分頁將模擬同一使用者的兩部裝置。",
|
||||
"login.dev.field": "Dev使用者識別碼",
|
||||
"login.dev.continue": "繼續",
|
||||
"login.prod.help": "透過Casdoor登入。",
|
||||
"login.prod.button": "透過Casdoor登入",
|
||||
|
||||
// ---- OAuth 回呼 -------------------------------------------------------
|
||||
"oauth.failed": "登入失敗",
|
||||
"oauth.signingIn": "正在登入…",
|
||||
"oauth.missingParams": "缺少code或state查詢參數",
|
||||
|
||||
// ---- 設定頁 ----------------------------------------------------------
|
||||
"settings.title": "設定",
|
||||
"settings.currentDevice.title": "目前裝置",
|
||||
"settings.deviceName.field": "裝置名稱",
|
||||
"settings.deviceName.save": "儲存",
|
||||
"settings.deviceName.unchanged": "新名稱與目前一致",
|
||||
"settings.deviceName.empty": "名稱不能為空",
|
||||
"settings.deviceName.asciiOnly": "裝置名稱只能使用 ASCII 字元(英文字母、數字、符號)。",
|
||||
"settings.deviceName.success": "裝置名稱已更新",
|
||||
"settings.unregister.current": "解除註冊本裝置",
|
||||
"settings.unregister.currentHint": "將登出並移除本裝置的註冊。",
|
||||
"settings.unregister.currentConfirm":
|
||||
"解除註冊本裝置會移除其註冊並登出,確認繼續?",
|
||||
"settings.unregister.peerConfirm": "確認要移除裝置「{{name}}」?",
|
||||
"settings.peers.title": "其他裝置",
|
||||
"settings.peers.empty": "尚無其他裝置。",
|
||||
"settings.peers.remove": "移除",
|
||||
"settings.peers.removing": "正在移除…",
|
||||
"settings.error.delete": "操作失敗:{{message}}",
|
||||
"settings.online": "線上",
|
||||
"settings.offline": "離線",
|
||||
"settings.lastSeen": "上次活躍 {{time}}",
|
||||
"settings.lastSeenNever": "尚未上線",
|
||||
"settings.account.title": "帳號",
|
||||
"settings.account.signOut": "登出",
|
||||
"settings.desktop.title": "桌面",
|
||||
"settings.desktop.clipboardSync": "剪貼簿自動同步",
|
||||
"settings.desktop.clipboardSyncHint": "複製後即上傳雲端,並接收其他裝置的剪貼簿更新。",
|
||||
"settings.desktop.launchAtLogin": "開機自動啟動",
|
||||
"settings.desktop.launchAtLoginHint": "登入系統後自動在背景啟動 cdrop(常駐選單列)。",
|
||||
"settings.desktop.downloadDir": "下載資料夾",
|
||||
"settings.desktop.downloadDirHint": "接收到的檔案會儲存至此資料夾;留空則使用系統下載資料夾。",
|
||||
"settings.desktop.downloadDirChoose": "變更…",
|
||||
"settings.desktop.downloadDirReset": "還原預設",
|
||||
"settings.desktop.downloadDirPicker": "選擇下載資料夾",
|
||||
"settings.desktop.ttlNote": "基於安全考量,雲端剪貼簿內容會在 5 分鐘後自動清除。",
|
||||
|
||||
// ---- iOS 捷徑權杖 ----------------------------------------------------
|
||||
"settings.shortcut.title": "iOS 捷徑",
|
||||
"settings.shortcut.intro": "為 iOS「捷徑」簽發長效、僅限剪貼簿、可隨時撤銷的專用權杖。即使權杖外洩也只能讀寫剪貼簿、無法觸及其他資料。",
|
||||
"settings.shortcut.labelField": "備註名稱",
|
||||
"settings.shortcut.labelPlaceholder": "例如:我的 iPhone",
|
||||
"settings.shortcut.issue": "簽發",
|
||||
"settings.shortcut.empty": "尚未簽發任何權杖。",
|
||||
"settings.shortcut.loadError": "權杖清單載入失敗。",
|
||||
"settings.shortcut.retry": "重試",
|
||||
"settings.shortcut.revoke": "撤銷",
|
||||
"settings.shortcut.revoking": "正在撤銷…",
|
||||
"settings.shortcut.revokeConfirm": "撤銷後使用此權杖的捷徑將立即失效,確認繼續?",
|
||||
"settings.shortcut.revoked": "已撤銷",
|
||||
"settings.shortcut.revokedToast": "權杖已撤銷",
|
||||
"settings.shortcut.expired": "已過期",
|
||||
"settings.shortcut.expiresAt": "到期 {{date}}",
|
||||
"settings.shortcut.lastUsed": "上次使用 {{time}}",
|
||||
"settings.shortcut.neverUsed": "尚未使用",
|
||||
"settings.shortcut.unavailable": "伺服器未啟用捷徑權杖。",
|
||||
"settings.shortcut.tooMany": "使用中的權杖已達上限,請先撤銷舊權杖。",
|
||||
"settings.shortcut.issueError": "操作失敗:{{message}}",
|
||||
"settings.shortcut.issuedTitle": "權杖已簽發",
|
||||
"settings.shortcut.onceWarning": "權杖僅顯示這一次,請立即複製保存;關閉後無法再次檢視。",
|
||||
"settings.shortcut.tokenLabel": "權杖(Bearer)",
|
||||
"settings.shortcut.copy": "複製",
|
||||
"settings.shortcut.copyFailed": "複製失敗,請手動選取複製",
|
||||
"settings.shortcut.copied": "已複製",
|
||||
"settings.shortcut.baseLabel": "伺服器位址",
|
||||
"settings.shortcut.guideTitle": "在 iOS 上手動建立(手動同步)",
|
||||
"settings.shortcut.guideIntro": "在 iPhone 的「捷徑」App 中新增兩個捷徑,首次執行時填入上方的伺服器位址與權杖:",
|
||||
"settings.shortcut.guideDeviceName": "兩個捷徑都加標頭 X-Device-Name: 你的裝置名稱(僅 ASCII),讓它在裝置清單中統一顯示。",
|
||||
"settings.shortcut.guidePullTitle": "拉取(雲端 → 本機剪貼簿)",
|
||||
"settings.shortcut.guidePull1": "「取得 URL 內容」:網址設為 你的伺服器位址 加 /api/clipboard,方法 GET,標頭加 Authorization: Bearer 你的權杖。",
|
||||
"settings.shortcut.guidePull2": "「取得字典值」:取鍵 content。",
|
||||
"settings.shortcut.guidePull3": "內容非空時用「複製到剪貼簿」寫入,可再加「顯示通知」提示已拉取。",
|
||||
"settings.shortcut.guidePushTitle": "上傳(本機剪貼簿 → 雲端)",
|
||||
"settings.shortcut.guidePush1": "「取得剪貼簿」;若為空則停止。",
|
||||
"settings.shortcut.guidePush2": "「取得 URL 內容」:網址同上,方法 PUT,標頭加 Authorization: Bearer 你的權杖 與 Content-Type: application/json,請求內容選 JSON:{\"content\": 剪貼簿, \"content_type\": \"text/plain\"}。",
|
||||
"settings.shortcut.guidePush3": "可加「顯示通知」提示已上傳。",
|
||||
"settings.shortcut.guideToleranceNote": "網路不穩時單次執行可能失敗,重試即可;自動輪詢版會在背景靜默重試(下一階段提供)。",
|
||||
"settings.shortcut.done": "完成",
|
||||
|
||||
// ---- 傳輸狀態 / 模式 -------------------------------------------------
|
||||
"transfer.savedTo": "已儲存至 {{path}}",
|
||||
"transfer.saveFailed": "儲存檔案失敗:{{error}}",
|
||||
"transfer.action.cancel": "取消",
|
||||
"transfer.action.delete": "刪除",
|
||||
"transfer.action.relayNow": "立即中繼",
|
||||
"transfer.action.details": "詳情",
|
||||
"transfer.state.PENDING": "等待中",
|
||||
"transfer.state.ACCEPTED": "已接受",
|
||||
"transfer.state.P2P_ACTIVE": "傳輸中",
|
||||
"transfer.state.RELAY_ACTIVE": "中繼中",
|
||||
"transfer.state.DONE": "已完成",
|
||||
"transfer.state.FAILED": "失敗",
|
||||
"transfer.state.CANCELLED": "已取消",
|
||||
"transfer.mode.p2p": "P2P",
|
||||
"transfer.mode.relay": "中繼",
|
||||
|
||||
// ---- 客戶端 phase(即時遙測,區分等待中和卡住)---------------------
|
||||
"transfer.phase.initializing": "正在初始化…",
|
||||
"transfer.phase.waiting_accept": "等待對端接受…",
|
||||
"transfer.phase.ice_gathering": "收集網路候選…",
|
||||
"transfer.phase.ice_checking": "建立直連通道…",
|
||||
"transfer.phase.ice_connected": "通道已連線",
|
||||
"transfer.phase.dc_open": "通道已開啟",
|
||||
"transfer.phase.transferring": "傳輸中",
|
||||
"transfer.phase.completing": "收尾中…",
|
||||
"transfer.phase.fallback_pending": "切換到中繼路徑…",
|
||||
"transfer.phase.relay_uploading": "中繼上傳中",
|
||||
"transfer.phase.relay_downloading": "中繼下載中",
|
||||
"transfer.stalled": "似乎卡住了({{seconds}} 秒無進度)",
|
||||
"transfer.bytesProgress": "{{sent}} / {{total}}",
|
||||
"transfer.bytesRate": "{{rate}}/秒",
|
||||
|
||||
// ---- 偵錯展開區 -----------------------------------------------------
|
||||
"transfer.debug.toggle": "偵錯資訊",
|
||||
"transfer.debug.iceGathering": "候選收集",
|
||||
"transfer.debug.iceConnection": "連線狀態",
|
||||
"transfer.debug.candidatesLocal": "本端候選",
|
||||
"transfer.debug.candidatesRemote": "對端候選",
|
||||
"transfer.debug.selectedPair": "選中候選對",
|
||||
"transfer.debug.noPair": "尚未選定候選對",
|
||||
"transfer.debug.candidatesLine":
|
||||
"host {{host}} · mDNS {{mdns}} · srflx {{srflx}} · prflx {{prflx}} · relay {{relay}}",
|
||||
"transfer.debug.dataChannel": "資料通道",
|
||||
"transfer.debug.dcLine": "{{state}} · 待傳送 {{buffered}}",
|
||||
|
||||
// ---- 時間 ------------------------------------------------------------
|
||||
"time.justNow": "剛剛",
|
||||
"time.secondsAgo": "{{n}} 秒前",
|
||||
"time.minutesAgo": "{{n}} 分鐘前",
|
||||
"time.hoursAgo": "{{n}} 小時前",
|
||||
|
||||
// ---- 通用 ------------------------------------------------------------
|
||||
"common.dismiss": "移除",
|
||||
"common.cancel": "取消",
|
||||
"common.confirm": "確認",
|
||||
|
||||
// ---- 錯誤(lib/* 中拋出,呈現到 Alert)-----------------------------
|
||||
"errors.messageEmpty": "訊息為空",
|
||||
"errors.messageOverflow": "訊息超過4 KB;請改用檔案傳輸",
|
||||
"errors.noReceiver": "未選擇接收方",
|
||||
"errors.devTokenMissing":
|
||||
"Dev模式:未在.env.local設定VITE_CDROP_DEV_TOKEN",
|
||||
"errors.noAccessToken": "無存取權杖:使用者必須先登入",
|
||||
"errors.clipboardUnavailable": "瀏覽器不支援剪貼簿API(需HTTPS與權限)",
|
||||
"errors.clipboardOverflow": "內容過大,超過 {{max}} 位元組上限",
|
||||
"errors.clipboardWriteFailed": "寫入本機剪貼簿失敗:{{message}}",
|
||||
};
|
||||
Reference in New Issue
Block a user