From 1fd3cfbd44a9e15509706202e84924893754adea Mon Sep 17 00:00:00 2001 From: "yinjun.chen1" Date: Mon, 14 Sep 2026 15:38:07 +0800 Subject: [PATCH] no message --- _locales/en/messages.json | 778 +++++++++++++++++++++++++++++++++++ _locales/zh_CN/messages.json | 778 +++++++++++++++++++++++++++++++++++ content.js | 60 +-- manifest.json | 5 +- popup.html | 162 ++++---- popup.js | 263 +++++++----- tests/detect-captcha.test.js | 9 +- 7 files changed, 1839 insertions(+), 216 deletions(-) create mode 100644 _locales/en/messages.json create mode 100644 _locales/zh_CN/messages.json diff --git a/_locales/en/messages.json b/_locales/en/messages.json new file mode 100644 index 0000000..2e53ec7 --- /dev/null +++ b/_locales/en/messages.json @@ -0,0 +1,778 @@ +{ + "appTitle": { + "message": "Auto Login Manager", + "description": "App title" + }, + "extDescription": { + "message": "Auto-fill username and password for specific domains and complete login. Supports managing domain configurations via UI.", + "description": "Extension description (manifest)" + }, + "lockSetupTitle": { + "message": "Set Local Password", + "description": "Lock screen - title when setting local password" + }, + "lockSetupHint": { + "message": "Local password encrypts all configurations. It cannot be recovered if forgotten.", + "description": "Lock screen - hint when setting local password" + }, + "lockUnlockTitle": { + "message": "Locked", + "description": "Lock screen - title when locked" + }, + "lockUnlockHint": { + "message": "Enter local password to view encrypted configurations", + "description": "Lock screen - hint when locked" + }, + "lockPasswordPlaceholder": { + "message": "Enter local password", + "description": "Lock screen - local password placeholder" + }, + "lockPasswordConfirmLabel": { + "message": "Confirm Local Password", + "description": "Lock screen - confirm local password label" + }, + "lockPasswordConfirmPlaceholder": { + "message": "Re-enter local password", + "description": "Lock screen - confirm local password placeholder" + }, + "lockSetupBtn": { + "message": "Set Local Password & Enter", + "description": "Lock screen - set local password button" + }, + "lockUnlockBtn": { + "message": "Enter", + "description": "Lock screen - enter button" + }, + "lockFootnote": { + "message": "Local password is used only for local encryption and is never uploaded to any server.", + "description": "Lock screen - footnote" + }, + "lockBtnTitle": { + "message": "Lock (clear plaintext from memory)", + "description": "Lock button title" + }, + "lockConfirmPrompt": { + "message": "Confirm lock? You'll need to re-enter local password to view configurations.", + "description": "Lock confirmation dialog" + }, + "lockWarnEmpty": { + "message": "Please enter local password", + "description": "Lock screen - local password empty" + }, + "lockWarnShort": { + "message": "Local password must be at least 6 characters", + "description": "Lock screen - local password too short" + }, + "lockWarnMismatch": { + "message": "The two local passwords do not match", + "description": "Lock screen - two passwords mismatch" + }, + "lockWarnSetupFail": { + "message": "Setup failed: $1", + "description": "Lock screen - setup failed, $1 is error" + }, + "lockWarnUnlockFail": { + "message": "Enter failed: $1", + "description": "Lock screen - unlock failed, $1 is error" + }, + "lockWarnNoService": { + "message": "Cannot connect to background service. Try refreshing the panel or reloading the extension.", + "description": "Lock screen - background service unavailable" + }, + "toastPasswordSet": { + "message": "Local password set. Configurations encrypted.", + "description": "Toast - local password set" + }, + "toastUnlocked": { + "message": "Entered", + "description": "Toast - unlocked" + }, + "toastLocked": { + "message": "Locked", + "description": "Toast - locked" + }, + "currentEnvLabel": { + "message": "Current Environment", + "description": "Env bar - current env label" + }, + "envManageBtnTitle": { + "message": "Manage environments (create / rename / delete)", + "description": "Env manage button title" + }, + "addConfigTitle": { + "message": "Add Domain Configuration", + "description": "Add config section title" + }, + "domainLabel": { + "message": "Domain / URL Match Rule", + "description": "Domain input label" + }, + "domainPlaceholder": { + "message": "example.com or *.example.com or site.com/admin/*", + "description": "Domain input placeholder" + }, + "fillDomainBtn": { + "message": "Fill Current Domain", + "description": "Fill current domain button" + }, + "fillDomainBtnTitle": { + "message": "Fill current tab domain", + "description": "Fill current domain button title" + }, + "domainHelp": { + "message": "Supports wildcard *. With / matches hostname+path, e.g. oa.com/login/*; without / matches domain only.", + "description": "Domain input help text" + }, + "usernameLabel": { + "message": "Username", + "description": "Username input label" + }, + "usernamePlaceholder": { + "message": "Enter username", + "description": "Username input placeholder" + }, + "passwordLabel": { + "message": "Password", + "description": "Password input label" + }, + "passwordPlaceholder": { + "message": "Enter password", + "description": "Password input placeholder" + }, + "togglePwdAria": { + "message": "Toggle password visibility", + "description": "Eye button aria-label" + }, + "aliasLabel": { + "message": "Alias (optional)", + "description": "Alias input label" + }, + "aliasPlaceholder": { + "message": "e.g. Company OA, GitLab, Email", + "description": "Alias input placeholder" + }, + "aliasHelp": { + "message": "Helps identify this config. Leave empty to show domain.", + "description": "Alias input help" + }, + "advancedOptionsSummary": { + "message": "Advanced Options (optional, defaults work)", + "description": "Advanced options summary" + }, + "autoSubmitLabel": { + "message": "Auto Submit Login", + "description": "Auto submit label" + }, + "autoSubmitYes": { + "message": "Yes - auto click login button", + "description": "Auto submit - yes" + }, + "autoSubmitNo": { + "message": "No - fill only, do not submit", + "description": "Auto submit - no" + }, + "enabledLabel": { + "message": "Enable / Disable", + "description": "Enabled label" + }, + "enabledHelp": { + "message": "When disabled, this configuration will not take effect", + "description": "Enabled help text" + }, + "delayMsLabel": { + "message": "Fill Delay (ms, optional)", + "description": "Delay ms label" + }, + "delayMsPlaceholder": { + "message": "Empty=300ms default. 500-2000 for SPA pages.", + "description": "Delay ms placeholder" + }, + "delayMsHelp": { + "message": "Some pages (Ant Design, ASP.NET) need JS init; immediate fill may fail.", + "description": "Delay ms help" + }, + "selectorHelpTip": { + "message": "Leave the three fields below empty. The extension will auto-find the login form. Fill them only if auto-fill fails or fills the wrong field.", + "description": "Selector tutorial tip" + }, + "selectorHelpStep1": { + "message": "1. Press F12 on the login page to open DevTools, switch to the \"Elements\" tab.", + "description": "Selector tutorial step 1" + }, + "selectorHelpStep2": { + "message": "2. Click the arrow icon at the top-left of DevTools, then click the input/button on the page.", + "description": "Selector tutorial step 2" + }, + "selectorHelpStep3": { + "message": "3. The tool jumps to the HTML line. Check id, name, class, type attributes on or -

本机密码仅用于本地加密,不会上传到任何服务器

+ +

本机密码仅用于本地加密,不会上传到任何服务器

- + diff --git a/popup.js b/popup.js index f2f28b1..019856f 100644 --- a/popup.js +++ b/popup.js @@ -1,6 +1,56 @@ document.addEventListener("DOMContentLoaded", () => { const $ = (id) => document.getElementById(id); + // ---------- i18n ---------- + // t(key, ...args) 简易封装:取 chrome.i18n.getMessage,可带 $1/$2 参数 + const t = (key, ...args) => { + const msg = chrome.i18n.getMessage(key, args); + return msg || key; + }; + + // 初始化:遍历 data-i18n / data-i18n-placeholder / data-i18n-title / data-i18n-aria 应用翻译 + function applyI18n(root = document) { + root.querySelectorAll("[data-i18n]").forEach((el) => { + const key = el.dataset.i18n; + // 部分元素含 /// 内嵌标签,统一用 message 文案覆盖 textContent + // 仅在没有子标签时直接 textContent;有子标签时跳过(保留原 HTML 结构) + if (el.children.length === 0) { + const msg = chrome.i18n.getMessage(key); + if (msg) el.textContent = msg; + } else { + // 含嵌套标签:尝试只替换首个文本节点 + const msg = chrome.i18n.getMessage(key); + if (msg) { + // 简化:如果有子标签,整体替换为纯文本(保持现有视觉靠 CSS) + // 这里保留原行为:不动结构,仅替换首文本节点 + const firstText = el.firstChild; + if (firstText && firstText.nodeType === Node.TEXT_NODE) { + // 找到第一个非空白文本节点,整体替换为 message 文案中第一个空白前部分 + // 简化处理:含子标签的元素文案在 messages.json 里已写为纯文本(无标签),直接用 + firstText.nodeValue = msg; + } + } + } + }); + root.querySelectorAll("[data-i18n-placeholder]").forEach((el) => { + const key = el.dataset.i18nPlaceholder; + const msg = chrome.i18n.getMessage(key); + if (msg) el.placeholder = msg; + }); + root.querySelectorAll("[data-i18n-title]").forEach((el) => { + const key = el.dataset.i18nTitle; + const msg = chrome.i18n.getMessage(key); + if (msg) el.title = msg; + }); + root.querySelectorAll("[data-i18n-aria]").forEach((el) => { + const key = el.dataset.i18nAria; + const msg = chrome.i18n.getMessage(key); + if (msg) el.setAttribute("aria-label", msg); + }); + } + + applyI18n(); + const domainInput = $("domain"); const aliasInput = $("alias"); const usernameInput = $("username"); @@ -76,7 +126,7 @@ document.addEventListener("DOMContentLoaded", () => { const ATTACK_RATE = 1e7; function estimateStrength(pwd) { - if (!pwd) return { entropy: 0, seconds: 0, level: "empty", label: "尚未输入" }; + if (!pwd) return { entropy: 0, seconds: 0, level: "empty", label: t("strengthEmpty") }; let pool = 0; if (/[a-z]/.test(pwd)) pool += 26; if (/[A-Z]/.test(pwd)) pool += 26; @@ -88,27 +138,27 @@ document.addEventListener("DOMContentLoaded", () => { // 平均破解时间 = 一半的搜索空间 const seconds = combinations / 2 / ATTACK_RATE; let level, label; - if (seconds < 1) { level = "very-weak"; label = "极弱"; } - else if (seconds < 3600) { level = "weak"; label = "弱"; } - else if (seconds < 86400 * 30) { level = "medium"; label = "中"; } - else if (seconds < 86400 * 365 * 1000) { level = "strong"; label = "强"; } - else { level = "very-strong"; label = "极强"; } + if (seconds < 1) { level = "very-weak"; label = t("strengthVeryWeak"); } + else if (seconds < 3600) { level = "weak"; label = t("strengthWeak"); } + else if (seconds < 86400 * 30) { level = "medium"; label = t("strengthMedium"); } + else if (seconds < 86400 * 365 * 1000) { level = "strong"; label = t("strengthStrong"); } + else { level = "very-strong"; label = t("strengthVeryStrong"); } return { entropy, seconds, level, label }; } function formatTime(seconds) { - if (seconds < 1) return "瞬间破解"; - if (seconds < 60) return `约 ${Math.round(seconds)} 秒`; - if (seconds < 3600) return `约 ${Math.round(seconds / 60)} 分钟`; - if (seconds < 86400) return `约 ${Math.round(seconds / 3600)} 小时`; - if (seconds < 86400 * 30) return `约 ${Math.round(seconds / 86400)} 天`; - if (seconds < 86400 * 365) return `约 ${Math.round(seconds / 86400 / 30)} 个月`; + if (seconds < 1) return t("crackInstant"); + if (seconds < 60) return t("crackSeconds", String(Math.round(seconds))); + if (seconds < 3600) return t("crackMinutes", String(Math.round(seconds / 60))); + if (seconds < 86400) return t("crackHours", String(Math.round(seconds / 3600))); + if (seconds < 86400 * 30) return t("crackDays", String(Math.round(seconds / 86400))); + if (seconds < 86400 * 365) return t("crackMonths", String(Math.round(seconds / 86400 / 30))); const years = seconds / 86400 / 365; - if (years < 1000) return `约 ${Math.round(years)} 年`; - if (years < 1e6) return `约 ${Math.round(years / 1000)} 千年`; - if (years < 1e9) return `约 ${Math.round(years / 1e6)} 百万年`; - if (years < 1e12) return `约 ${Math.round(years / 1e9)} 亿年`; - return `约 ${(years / 1e12).toExponential(2)} 万亿年`; + if (years < 1000) return t("crackYears", String(Math.round(years))); + if (years < 1e6) return t("crackThousandYears", String(Math.round(years / 1000))); + if (years < 1e9) return t("crackMillionYears", String(Math.round(years / 1e6))); + if (years < 1e12) return t("crackBillionYears", String(Math.round(years / 1e9))); + return t("crackTrillionYears", (years / 1e12).toExponential(2)); } function updateStrength(pwd) { @@ -124,8 +174,8 @@ document.addEventListener("DOMContentLoaded", () => { }; strengthBar.style.width = widthMap[level] + "%"; strengthBar.className = "strength-bar-fg " + level; - strengthLabel.textContent = pwd ? `强度:${label}(熵 ${entropy.toFixed(1)} bits)` : "尚未输入"; - crackTime.textContent = pwd ? `破解时间:${formatTime(seconds)}` : ""; + strengthLabel.textContent = pwd ? t("strengthLabel", label, entropy.toFixed(1)) : t("strengthEmpty"); + crackTime.textContent = pwd ? t("crackTimeLabel", formatTime(seconds)) : ""; } modalPwd.addEventListener("input", () => updateStrength(modalPwd.value)); @@ -208,22 +258,22 @@ document.addEventListener("DOMContentLoaded", () => { return new Promise((resolve) => { modalResolve = resolve; if (mode === "export") { - modalTitle.textContent = "加密导出"; + modalTitle.textContent = t("pwdModalExportTitle"); encryptToggleGroup.style.display = ""; confirmGroup.style.display = ""; encryptToggle.checked = true; pwdFields.classList.remove("hidden"); modalPwd.value = ""; modalPwdConfirm.value = ""; - modalWarn.textContent = "文件密码将用于 AES-GCM 加密,无法找回,请妥善保存"; + modalWarn.textContent = t("pwdModalExportWarn"); } else { - modalTitle.textContent = "输入解密文件密码"; + modalTitle.textContent = t("pwdModalImportTitle"); encryptToggleGroup.style.display = "none"; confirmGroup.style.display = "none"; pwdFields.classList.remove("hidden"); modalPwd.value = ""; modalPwdConfirm.value = ""; - modalWarn.textContent = "请输入导出时设置的文件密码"; + modalWarn.textContent = t("pwdModalImportWarn"); } pwdModal.classList.add("show"); setTimeout(() => modalPwd.focus(), 0); @@ -255,17 +305,18 @@ document.addEventListener("DOMContentLoaded", () => { modalConfirm.addEventListener("click", () => { if (!modalResolve) return; - const mode = (modalTitle.textContent.includes("导出")) ? "export" : "import"; - if (mode === "export") { + // 通过 modal 当前模式标记判断(openPwdModal 时 mode 已确定 export/import 文案) + const isExport = modalTitle.textContent === t("pwdModalExportTitle"); + if (isExport) { if (encryptToggle.checked) { const p1 = modalPwd.value; const p2 = modalPwdConfirm.value; if (!p1) { - modalWarn.textContent = "文件密码不能为空"; + modalWarn.textContent = t("warnFilePwdEmpty"); return; } if (p1 !== p2) { - modalWarn.textContent = "两次文件密码不一致"; + modalWarn.textContent = t("warnFilePwdMismatch"); return; } modalResolve({ encrypt: true, password: p1 }); @@ -275,7 +326,7 @@ document.addEventListener("DOMContentLoaded", () => { } else { const p = modalPwd.value; if (!p) { - modalWarn.textContent = "文件密码不能为空"; + modalWarn.textContent = t("warnFilePwdEmpty"); return; } modalResolve({ encrypt: true, password: p }); @@ -389,7 +440,7 @@ document.addEventListener("DOMContentLoaded", () => { enabledCheckbox.checked = draft.enabled !== false; delayMsInput.value = draft.delayMs || ""; editingId = draft.editingId || null; - addBtn.textContent = editingId ? "更新配置" : "添加配置"; + addBtn.textContent = editingId ? t("updateConfigBtn") : t("addConfigBtn"); } function clearDraft() { @@ -403,11 +454,11 @@ document.addEventListener("DOMContentLoaded", () => { return new Promise((resolve) => { envModalResolve = resolve; if (mode === "create") { - envModalTitle.textContent = "新建环境"; + envModalTitle.textContent = t("envCreateTitle"); envNameInput.value = ""; presetEnvTags.style.display = "flex"; } else { - envModalTitle.textContent = "重命名环境"; + envModalTitle.textContent = t("envRenameTitle"); envNameInput.value = currentName; presetEnvTags.style.display = "flex"; } @@ -444,12 +495,12 @@ document.addEventListener("DOMContentLoaded", () => { if (!envModalResolve) return; const name = envNameInput.value.trim(); if (!name) { - envModalWarn.textContent = "环境名称不能为空"; + envModalWarn.textContent = t("envWarnEmpty"); envModalWarn.style.display = "block"; return; } if (name.length > 20) { - envModalWarn.textContent = "名称过长(最多 20 字符)"; + envModalWarn.textContent = t("envWarnTooLong"); envModalWarn.style.display = "block"; return; } @@ -480,7 +531,7 @@ document.addEventListener("DOMContentLoaded", () => { await setActiveEnv(envSelect.value); resetForm(); await renderConfigList(); - showToast("已切换环境"); + showToast(t("toastSwitchEnv")); }); // ---------- 环境管理对话框 ---------- @@ -500,7 +551,7 @@ document.addEventListener("DOMContentLoaded", () => { envManageList.innerHTML = ""; if (environments.length === 0) { - envManageList.innerHTML = '
暂无环境
'; + envManageList.innerHTML = `
${t("envManageEmpty")}
`; return; } @@ -511,7 +562,7 @@ document.addEventListener("DOMContentLoaded", () => { const nameSpan = document.createElement("span"); nameSpan.className = "env-manage-name"; const cnt = env.configs?.length || 0; - const activeMark = env.id === activeEnvId ? '当前' : ""; + const activeMark = env.id === activeEnvId ? `${t("envCurrentMark")}` : ""; nameSpan.innerHTML = `${escapeHtml(env.name)} (${cnt}) ${activeMark}`; const actions = document.createElement("div"); @@ -519,7 +570,7 @@ document.addEventListener("DOMContentLoaded", () => { const editBtn = document.createElement("button"); editBtn.className = "btn-edit"; - editBtn.textContent = "重命名"; + editBtn.textContent = t("renameBtn"); editBtn.addEventListener("click", async () => { const newName = await openEnvModal("rename", env.name); if (!newName) return; @@ -527,24 +578,24 @@ document.addEventListener("DOMContentLoaded", () => { await renameEnvironment(env.id, newName); await renderEnvManageList(); await renderEnvSelect(); - showToast("已重命名"); + showToast(t("toastRenamed")); } catch (err) { - showToast(err.message || "重命名失败"); + showToast(err.message || t("toastRenameFail")); } }); const delBtn = document.createElement("button"); delBtn.className = "btn-danger"; - delBtn.textContent = "删除"; + delBtn.textContent = t("deleteBtn"); delBtn.addEventListener("click", async () => { if (environments.length <= 1) { - showToast("至少保留一个环境"); + showToast(t("toastKeepOneEnv")); return; } const cnt2 = env.configs?.length || 0; const msg = cnt2 > 0 - ? `环境「${env.name}」下有 ${cnt2} 条配置,确认删除?(将一并删除这些配置)` - : `确认删除环境「${env.name}」?`; + ? t("confirmDeleteEnvWithCfgs", env.name, String(cnt2)) + : t("confirmDeleteEnv", env.name); if (!confirm(msg)) return; try { await deleteEnvironment(env.id); @@ -552,9 +603,9 @@ document.addEventListener("DOMContentLoaded", () => { await renderEnvSelect(); resetForm(); await renderConfigList(); - showToast("已删除环境"); + showToast(t("toastEnvDeleted")); } catch (err) { - showToast(err.message || "删除失败"); + showToast(err.message || t("toastDeleteFail")); } }); @@ -578,9 +629,9 @@ document.addEventListener("DOMContentLoaded", () => { await renderEnvSelect(); resetForm(); await renderConfigList(); - showToast(`已创建环境「${name}」`); + showToast(t("toastEnvCreated", name)); } catch (err) { - showToast(err.message || "创建失败"); + showToast(err.message || t("toastCreateFail")); } }); @@ -613,8 +664,8 @@ document.addEventListener("DOMContentLoaded", () => { -
暂无配置
-
在上方填写域名 / 用户名 / 密码后点击「添加配置」
+
${t("emptyStateTitle")}
+
${t("emptyStateSub")}
`; return; @@ -629,15 +680,16 @@ document.addEventListener("DOMContentLoaded", () => { const displayName = escapeHtml(cfg.alias || cfg.domain); const showDomain = cfg.alias ? escapeHtml(cfg.domain) : ""; const enabledTag = cfg.enabled === false - ? '已禁用' + ? `${t("tagDisabled")}` : ""; const delayTag = cfg.delayMs && Number(cfg.delayMs) > 300 - ? `延迟 ${cfg.delayMs}ms` + ? `${t("tagDelay", String(cfg.delayMs))}` : ""; + const autoSubmitText = cfg.autoSubmit ? t("yesShort") : t("noShort"); info.innerHTML = `
${displayName} ${enabledTag} ${delayTag}
${showDomain ? `
${showDomain}
` : ""} -
用户: ${escapeHtml(cfg.username)} | 自动提交: ${cfg.autoSubmit ? "是" : "否"}
+
${t("configDetailUser", escapeHtml(cfg.username), autoSubmitText)}
`; const actions = document.createElement("div"); @@ -646,8 +698,9 @@ document.addEventListener("DOMContentLoaded", () => { // 启用/禁用切换按钮(不删除配置) const toggleBtn = document.createElement("button"); toggleBtn.className = "btn-toggle"; - toggleBtn.textContent = cfg.enabled === false ? "启用" : "禁用"; - toggleBtn.title = cfg.enabled === false ? "点击启用此配置" : "点击禁用此配置"; + const isDisabled = cfg.enabled === false; + toggleBtn.textContent = isDisabled ? t("toggleEnable") : t("toggleDisable"); + toggleBtn.title = isDisabled ? t("toggleEnableTitle") : t("toggleDisableTitle"); toggleBtn.addEventListener("click", async () => { const updated = configs.map((c) => c.id === cfg.id ? { ...c, enabled: c.enabled === false } : c @@ -656,12 +709,12 @@ document.addEventListener("DOMContentLoaded", () => { renderConfigList(); renderEnvSelect(); if (envManageModal.classList.contains("show")) renderEnvManageList(); - showToast(cfg.enabled === false ? "已启用" : "已禁用"); + showToast(isDisabled ? t("toastEnabled") : t("toastDisabled")); }); const editBtn = document.createElement("button"); editBtn.className = "btn-edit"; - editBtn.textContent = "编辑"; + editBtn.textContent = t("editBtn"); editBtn.addEventListener("click", () => { editingId = cfg.id; domainInput.value = cfg.domain; @@ -674,18 +727,18 @@ document.addEventListener("DOMContentLoaded", () => { submitSelectorInput.value = cfg.submitSelector || ""; enabledCheckbox.checked = cfg.enabled !== false; delayMsInput.value = cfg.delayMs || ""; - addBtn.textContent = "更新配置"; + addBtn.textContent = t("updateConfigBtn"); saveDraft(); domainInput.focus(); }); const delBtn = document.createElement("button"); delBtn.className = "btn-danger"; - delBtn.textContent = "删除"; + delBtn.textContent = t("deleteBtn"); delBtn.addEventListener("click", async () => { const updated = configs.filter((c) => c.id !== cfg.id); await saveConfigs(updated); - showToast("已删除"); + showToast(t("toastDeleted")); renderConfigList(); renderEnvSelect(); if (envManageModal.classList.contains("show")) renderEnvManageList(); @@ -715,7 +768,7 @@ document.addEventListener("DOMContentLoaded", () => { submitSelectorInput.value = ""; enabledCheckbox.checked = true; delayMsInput.value = ""; - addBtn.textContent = "添加配置"; + addBtn.textContent = t("addConfigBtn"); clearDraft(); } @@ -731,7 +784,7 @@ document.addEventListener("DOMContentLoaded", () => { const password = passwordInput.value; if (!domain || !username || !password) { - showToast("域名、用户名、密码不能为空"); + showToast(t("toastRequired")); return; } @@ -756,11 +809,11 @@ document.addEventListener("DOMContentLoaded", () => { config.id = editingId; configs[idx] = config; } - showToast("配置已更新"); + showToast(t("toastUpdated")); } else { config.id = Date.now().toString(36) + Math.random().toString(36).slice(2, 8); configs.push(config); - showToast("配置已添加"); + showToast(t("toastAdded")); } await saveConfigs(configs); @@ -773,7 +826,7 @@ document.addEventListener("DOMContentLoaded", () => { fillCurrentBtn.addEventListener("click", async () => { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); if (!tab) { - showToast("无法获取当前标签页"); + showToast(t("toastNoTab")); return; } @@ -782,7 +835,7 @@ document.addEventListener("DOMContentLoaded", () => { const matched = allConfigs.filter((c) => isDomainMatch(tab.url, c.domain)); if (matched.length === 0) { - showToast("当前域名无匹配配置"); + showToast(t("toastNoMatch")); return; } @@ -793,16 +846,16 @@ document.addEventListener("DOMContentLoaded", () => { config: matched[0], }); if (resp && resp.success) { - showToast("已填充,请确认"); + showToast(t("toastFilled")); } else { - showToast("已发送但未找到登录表单,请确认页面已加载完成"); + showToast(t("toastFilledNoForm")); } } catch (err) { // content script 未注入:常见于安装扩展前已打开的页面 if (/Could not establish connection|Receiving end does not exist/i.test(err.message)) { - showToast("页面未注入脚本,请刷新页面后重试"); + showToast(t("toastScriptNotInjected")); } else { - showToast("发送失败:" + (err.message || err)); + showToast(t("toastSendFail", err.message || String(err))); } } return; @@ -815,12 +868,12 @@ document.addEventListener("DOMContentLoaded", () => { configs: matched, tabUrl: tab.url, }); - showToast(`命中 ${matched.length} 个环境配置,请在页面选择`); + showToast(t("toastMultiMatch", String(matched.length))); } catch (err) { if (/Could not establish connection|Receiving end does not exist/i.test(err.message)) { - showToast("页面未注入脚本,请刷新页面后重试"); + showToast(t("toastScriptNotInjected")); } else { - showToast("发送失败:" + (err.message || err)); + showToast(t("toastSendFail", err.message || String(err))); } } }); @@ -828,20 +881,20 @@ document.addEventListener("DOMContentLoaded", () => { fillDomainBtn.addEventListener("click", async () => { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); if (!tab || !tab.url) { - showToast("无法获取当前标签页"); + showToast(t("toastNoTab")); return; } try { const hostname = new URL(tab.url).hostname; if (!hostname) { - showToast("当前页面无有效域名"); + showToast(t("toastNoDomain")); return; } domainInput.value = hostname; saveDraft(); - showToast(`已填入: ${hostname}`); + showToast(t("toastDomainFilled", hostname)); } catch { - showToast("无法解析当前页面 URL"); + showToast(t("toastUrlParseFail")); } }); @@ -850,12 +903,12 @@ document.addEventListener("DOMContentLoaded", () => { const { environments } = await loadEnvironments(); const totalConfigs = environments.reduce((s, e) => s + (e.configs?.length || 0), 0); if (totalConfigs === 0) { - showToast("暂无配置可导出"); + showToast(t("toastNoExportable")); return; } const choice = await openPwdModal("export"); if (!choice) { - showToast("已取消导出"); + showToast(t("toastExportCancelled")); return; } const stamp = new Date().toISOString().replace(/[:T]/g, "").slice(0, 15); @@ -890,7 +943,7 @@ document.addEventListener("DOMContentLoaded", () => { a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); - showToast(`已导出 ${environments.length} 个环境 / ${totalConfigs} 条配置${choice.encrypt ? "(加密)" : ""}`); + showToast(t("toastExported", String(environments.length), String(totalConfigs), choice.encrypt ? t("encryptedMark") : "")); }); // 导入配置:兼容 v1(数组) / v2(configs) / v3(environments),加密则弹口令 modal @@ -913,7 +966,7 @@ document.addEventListener("DOMContentLoaded", () => { else if (data?.encrypted === true && data?.cipher) { const choice = await openPwdModal("import"); if (!choice) { - showToast("已取消导入"); + showToast(t("toastImportCancelled")); importFile.value = ""; return; } @@ -927,12 +980,12 @@ document.addEventListener("DOMContentLoaded", () => { } else if (Array.isArray(decrypted?.configs)) { incomingConfigs = decrypted.configs; } else { - showToast("解密后内容格式无效"); + showToast(t("toastInvalidFormat")); importFile.value = ""; return; } } catch (err) { - showToast("解密失败:文件密码错误或文件损坏"); + showToast(t("toastDecryptFail")); importFile.value = ""; return; } @@ -945,7 +998,7 @@ document.addEventListener("DOMContentLoaded", () => { else if (Array.isArray(data?.configs)) { incomingConfigs = data.configs; } else { - showToast("文件格式无效"); + showToast(t("toastFileInvalid")); importFile.value = ""; return; } @@ -1000,7 +1053,7 @@ document.addEventListener("DOMContentLoaded", () => { await saveEnvironments(existingEnvs, activeEnvId); await renderEnvSelect(); await renderConfigList(); - showToast(`已导入 ${addedEnvs} 个环境 / ${addedCfgs} 条配置,跳过 ${skippedCfgs} 条(同名或无效)`); + showToast(t("toastImportedEnvs", String(addedEnvs), String(addedCfgs), String(skippedCfgs))); return; } @@ -1036,9 +1089,9 @@ document.addEventListener("DOMContentLoaded", () => { } await saveEnvironments(existingEnvs); await renderConfigList(); - showToast(`已导入 ${added} 条到当前环境,跳过 ${skipped} 条(重复或无效)`); + showToast(t("toastImportedCfgs", String(added), String(skipped))); } catch (err) { - showToast("导入失败:" + (err.message || "解析错误")); + showToast(t("toastImportFail", err.message || "parse error")); } finally { importFile.value = ""; } @@ -1055,7 +1108,7 @@ document.addEventListener("DOMContentLoaded", () => { // 扩展上下文已失效(扩展被重载/更新),刷新页面恢复 if (!chrome.runtime?.id) { location.reload(); - reject(new Error("扩展已重新加载,正在刷新面板…")); + reject(new Error(t("toastContextReload"))); return; } chrome.runtime.sendMessage(message, (resp) => { @@ -1089,15 +1142,15 @@ document.addEventListener("DOMContentLoaded", () => { lockWarn.style.display = "none"; if (mode === "setup") { - lockTitle.textContent = "设置本机密码"; - lockHint.textContent = "本机密码用于加密存储所有配置,忘记将无法恢复"; + lockTitle.textContent = t("lockSetupTitle"); + lockHint.textContent = t("lockSetupHint"); lockConfirmGroup.style.display = "block"; - lockSubmitBtn.textContent = "设置本机密码并进入"; + lockSubmitBtn.textContent = t("lockSetupBtn"); } else { - lockTitle.textContent = "已锁定"; - lockHint.textContent = "请输入本机密码查看已加密的配置"; + lockTitle.textContent = t("lockUnlockTitle"); + lockHint.textContent = t("lockUnlockHint"); lockConfirmGroup.style.display = "none"; - lockSubmitBtn.textContent = "进入"; + lockSubmitBtn.textContent = t("lockUnlockBtn"); } setTimeout(() => lockPassword.focus(), 50); } @@ -1121,8 +1174,8 @@ document.addEventListener("DOMContentLoaded", () => { renderConfigList(); } } catch (err) { - console.warn("[AutoLogin] 获取锁状态失败:", err); - lockWarn.textContent = "无法连接后台服务,请尝试刷新面板或重新加载扩展"; + console.warn("[AutoLogin]", t("logGetLockStatusFail"), err); + lockWarn.textContent = t("lockWarnNoService"); lockWarn.style.display = "block"; showLockScreen("setup"); } @@ -1131,47 +1184,47 @@ document.addEventListener("DOMContentLoaded", () => { lockSubmitBtn.addEventListener("click", async () => { const pwd = lockPassword.value; if (!pwd) { - lockWarn.textContent = "请输入本机密码"; + lockWarn.textContent = t("lockWarnEmpty"); lockWarn.style.display = "block"; return; } if (lockMode === "setup") { if (pwd.length < 6) { - lockWarn.textContent = "本机密码至少 6 位"; + lockWarn.textContent = t("lockWarnShort"); lockWarn.style.display = "block"; return; } if (pwd !== lockPasswordConfirm.value) { - lockWarn.textContent = "两次输入的本机密码不一致"; + lockWarn.textContent = t("lockWarnMismatch"); lockWarn.style.display = "block"; return; } try { await sendMessage({ action: "setupVault", password: pwd }); - showToast("本机密码已设置,配置已加密存储"); + showToast(t("toastPasswordSet")); showMainUI(); loadDraft(); renderEnvSelect(); renderConfigList(); } catch (err) { - lockWarn.textContent = "设置失败:" + (err.message || err); + lockWarn.textContent = t("lockWarnSetupFail", err.message || String(err)); lockWarn.style.display = "block"; } } else { try { const resp = await sendMessage({ action: "unlock", password: pwd }); if (resp.success) { - showToast("已进入"); + showToast(t("toastUnlocked")); showMainUI(); loadDraft(); renderEnvSelect(); renderConfigList(); } else { - lockWarn.textContent = resp.error || "进入失败"; + lockWarn.textContent = resp.error || t("lockWarnUnlockFail", ""); lockWarn.style.display = "block"; } } catch (err) { - lockWarn.textContent = "进入失败:" + (err.message || err); + lockWarn.textContent = t("lockWarnUnlockFail", err.message || String(err)); lockWarn.style.display = "block"; } } @@ -1185,10 +1238,10 @@ document.addEventListener("DOMContentLoaded", () => { }); lockBtn.addEventListener("click", async () => { - if (!confirm("确认锁定?锁定后需重新输入本机密码才能查看配置。")) return; + if (!confirm(t("lockConfirmPrompt"))) return; await sendMessage({ action: "lock" }); showLockScreen("unlock"); - showToast("已锁定"); + showToast(t("toastLocked")); }); // 用户在弹窗内的操作视为活动,通知 background 重置自动锁定计时 diff --git a/tests/detect-captcha.test.js b/tests/detect-captcha.test.js index 5754c2a..ec5465f 100644 --- a/tests/detect-captcha.test.js +++ b/tests/detect-captcha.test.js @@ -41,7 +41,11 @@ function loadContentJs(mockDoc) { window: { location: { href: "https://example.com/login" } }, document: mockDoc, chrome: { - runtime: { onMessage: { addListener() {} } }, + runtime: { + onMessage: { addListener() {} }, + }, + // i18n mock:getMessage 返回 key 自身作为文案(fallback) + i18n: { getMessage: (key) => key || "" }, storage: { local: { get: async () => ({}), set: async () => {}, remove: async () => {} }, session: { get: async () => ({}), set: async () => {}, remove: async () => {} }, @@ -85,7 +89,8 @@ describe("detectCaptcha", () => { const r = sb.detectCaptcha(); assert.ok(r); assert.equal(r.type, "reCAPTCHA"); - assert.match(r.reason, /reCAPTCHA/); + // reason 由 i18n 提供,测试环境 fallback 到 key,断言非空字符串 + assert.ok(typeof r.reason === "string" && r.reason.length > 0); }); it("检测到 reCAPTCHA iframe", () => {