diff --git a/.gitignore b/.gitignore index 3d9719b..0db637d 100644 --- a/.gitignore +++ b/.gitignore @@ -16,5 +16,6 @@ __pycache__/ .worktrees/ /release-artifacts/ /tests/ +/.reverse-skill-proxy/ # Local exported runtime settings /config.json diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b17f4d6 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,16 @@ +# Reverse-Skill Proxy For GuJumpgate + +This workspace includes an optional Vietnamese reverse-skill proxy bundle for CTF / reverse / exploit tasks. + +## Startup contract + +- When the task is CTF, reverse engineering, binary exploitation, web/API security, prompt injection, mobile analysis, cloud/container drift, or Windows/AD pivoting, read `.reverse-skill-proxy/ROUTER.vi.md` first. +- Start with the narrowest matching route instead of loading unrelated skill families. +- If challenge type is still unclear, start with `ctf-sandbox-orchestrator` and let it route you. +- If the bundle or route is missing, fall back to the default workflow without blocking the rest of the task. + +## Local safety expectations + +- Prefer passive inspection first. +- Do not disrupt already-running local services, browser sessions, or extension state unless explicitly asked. +- Keep updates short: outcome -> evidence -> verification -> next step. diff --git a/README.md b/README.md index 20e7360..cc98f65 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,40 @@ ![开始运行扩展流程](docs/images/github-readme-1779194981001.png) +## Reverse-skill tiếng Việt cho contributor + +Nếu bạn xử lý các tác vụ như CTF / reverse / web security trong repo này, bạn có thể bật lớp reverse-skill tiếng Việt tùy chọn. Phần tích hợp này **không sửa luồng runtime của extension**; nó chỉ tạo thêm một bundle local để agent/Codex đọc và route task chính xác hơn. + +### Bật nhanh trong 30 giây + +```bash +python reverse_skill_proxy.py --bundle-dir .reverse-skill-proxy --workspace-root . +``` + +Sau khi chạy xong, repo sẽ có: + +- `.reverse-skill-proxy/ROUTER.vi.md` +- `.reverse-skill-proxy/MANIFEST.json` +- `AGENTS.md` + +### Khi nào nên dùng + +- Phân tích CTF / reverse / pwn trong workspace này +- Kiểm tra web / API security +- Xử lý prompt injection / LLM security / cloud drift +- Muốn agent đọc router tiếng Việt trước khi chọn skill family hẹp hơn + +### Cơ chế fallback + +- Nếu máy chưa có `~/.hermes-shop/skills/reverse-skill` +- Hoặc chưa có `~/.hermes-shop/skills/ctf-sandbox-orchestrator` + +Script vẫn tạo router và manifest; nó chỉ ghi lại các nguồn còn thiếu trong `MANIFEST.json`, không chặn phần việc còn lại. + +Xem thêm: +- [`docs/reverse-skill-vi-integration.md`](docs/reverse-skill-vi-integration.md) +- [`docs/reverse-skill-contributor-quickstart.md`](docs/reverse-skill-contributor-quickstart.md) + ## 版权与来源说明 本项目基于开源项目 [QLHazyCoder/FlowPilot](https://github.com/QLHazyCoder/FlowPilot) 进行修改、移植与二次开发,其部分早期代码与 [whwh1233/StepFlow-Duck](https://github.com/whwh1233/StepFlow-Duck) 具有共同历史。 diff --git a/background/auto-run-controller.js b/background/auto-run-controller.js index f322b0e..146fcdb 100644 --- a/background/auto-run-controller.js +++ b/background/auto-run-controller.js @@ -80,7 +80,7 @@ if (typeof runAutoSequenceFromNode === 'function') { return runAutoSequenceFromNode(startNodeId, context); } - throw new Error('自动运行节点执行器未接入。'); + throw new Error('Chưa tích hợp bộ thực thi node cho chế độ chạy tự động.'); } function createAutoRunRoundSummary(round) { @@ -268,7 +268,7 @@ const failedRounds = summaries.filter((item) => item.status === 'failed'); const pendingRounds = summaries.filter((item) => item.status === 'pending'); - await addLog('=== 自动运行汇总 ===', failedRounds.length ? 'warn' : 'ok'); + await addLog('=== Tổng kết chạy tự động ===', failedRounds.length ? 'warn' : 'ok'); await addLog( `总轮数:${totalRuns};成功:${successRounds.length};失败:${failedRounds.length};未完成:${pendingRounds.length}`, failedRounds.length ? 'warn' : 'ok' @@ -442,7 +442,7 @@ async function autoRunLoop(totalRuns, options = {}) { let currentRuntime = runtime.get(); if (currentRuntime.autoRunActive) { - await addLog('自动运行已在进行中', 'warn'); + await addLog('Chạy tự động đang diễn ra', 'warn'); return; } diff --git a/background/ip-proxy-core.js b/background/ip-proxy-core.js index 6c5dbe3..0d38438 100644 --- a/background/ip-proxy-core.js +++ b/background/ip-proxy-core.js @@ -1141,9 +1141,9 @@ function getAccountListParseFailureHint(state = {}, provider = DEFAULT_IP_PROXY_ return ''; } if (normalizedProvider === '711proxy') { - return '账号列表已填写,但未解析出有效条目。请按每行 host:port:username:password 填写。'; + return 'Đã điền danh sách tài khoản nhưng không phân tích được mục hợp lệ nào. Hãy điền mỗi dòng theo định dạng host:port:username:password.'; } - return '账号列表已填写,但未解析出有效条目。请检查列表格式。'; + return 'Đã điền danh sách tài khoản nhưng không phân tích được mục hợp lệ nào. Hãy kiểm tra lại định dạng danh sách.'; } function resolveIpProxyPoolTargetCountForMode(state = {}, mode = normalizeIpProxyMode(state?.ipProxyMode)) { @@ -1170,7 +1170,7 @@ function buildProxyPoolSummary(pool = [], preferredIndex = 0) { count: 0, index: 0, current: null, - display: '暂无可用代理', + display: 'Chưa có proxy khả dụng', }; } const index = normalizeIpProxyCurrentIndex(preferredIndex, 0) % normalizedPool.length; diff --git a/background/message-router.js b/background/message-router.js index fbd066c..c85aee9 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -274,7 +274,7 @@ : Boolean(signupTabId); if (!signupTabId || !signupTabAlive) { - throw new Error('手动执行步骤 4 前,请先执行步骤 1 或步骤 2,确保认证页仍然打开并停留在验证码页。'); + throw new Error('Trước khi chạy thủ công bước 4, hãy chạy bước 1 hoặc bước 2 trước để đảm bảo trang xác thực vẫn đang mở và đang dừng ở trang mã xác minh.'); } } diff --git a/background/phone-verification-flow.js b/background/phone-verification-flow.js index 65dae71..5edb342 100644 --- a/background/phone-verification-flow.js +++ b/background/phone-verification-flow.js @@ -544,7 +544,7 @@ function assertFiveSimMaxPriceCompatibleWithOperator(operator, maxPriceLimit) { const normalizedOperator = normalizeFiveSimCountryCode(operator, DEFAULT_FIVE_SIM_OPERATOR); if (maxPriceLimit !== null && maxPriceLimit !== undefined && normalizedOperator !== DEFAULT_FIVE_SIM_OPERATOR) { - throw new Error('5sim 价格上限仅支持运营商为 "any" 时使用;请清空价格上限,或先把运营商切换为 any。'); + throw new Error('Giới hạn giá của 5sim chỉ hỗ trợ khi nhà mạng là "any"; hãy xóa giới hạn giá hoặc chuyển nhà mạng sang any trước.'); } } @@ -1340,7 +1340,7 @@ return 'SMSPool'; } if (provider === PHONE_SMS_PROVIDER_CHATGPT_API) { - return 'ChatGPT API 接码'; + return 'Nhận mã qua ChatGPT API'; } return 'HeroSMS'; } @@ -1348,23 +1348,23 @@ function formatStep9Reason(reason = '') { const text = String(reason || '').trim(); if (!text) { - return '未知'; + return 'Không xác định'; } const normalized = text.toLowerCase(); const reasonMap = { - returned_to_add_phone_loop: '反复返回添加手机号页', - phone_number_used: '手机号已被使用', - sms_not_received: '未收到短信', - sms_timeout: '短信超时', - resend_throttled: '重发短信被限流', - code_rejected: '验证码被拒绝', - add_phone_rejected: '添加手机号被拒绝', - activation_not_found: '接码订单不存在或已失效', - resend_phone_banned: 'OpenAI 无法向该号码发送短信', - phone_max_usage_exceeded: '手机号达到使用上限', - resend_server_error: '重发短信后进入服务器错误页', - whatsapp_resend_channel: '页面重发入口切换为 WhatsApp 通道', - unknown: '未知', + returned_to_add_phone_loop: 'Liên tục quay lại trang thêm số điện thoại', + phone_number_used: 'Số điện thoại đã được dùng', + sms_not_received: 'Chưa nhận được SMS', + sms_timeout: 'SMS hết thời gian chờ', + resend_throttled: 'Gửi lại SMS bị giới hạn tần suất', + code_rejected: 'Mã xác minh bị từ chối', + add_phone_rejected: 'Thêm số điện thoại bị từ chối', + activation_not_found: 'Đơn nhận mã không tồn tại hoặc đã hết hiệu lực', + resend_phone_banned: 'OpenAI không thể gửi SMS tới số này', + phone_max_usage_exceeded: 'Số điện thoại đã đạt giới hạn sử dụng', + resend_server_error: 'Sau khi gửi lại SMS thì chuyển sang trang lỗi máy chủ', + whatsapp_resend_channel: 'Nút gửi lại trên trang đã chuyển sang kênh WhatsApp', + unknown: 'Không xác định', }; if (reasonMap[normalized]) { return reasonMap[normalized]; @@ -1379,22 +1379,22 @@ function formatPhoneSmsApiFailureReason(reason = '') { const text = String(reason || '').trim(); if (!text) { - return '未知错误'; + return 'Lỗi không xác định'; } if (/\bBAD_KEY\b|\bWRONG_KEY\b|\bINVALID_KEY\b/i.test(text)) { - return 'API Key 无效(BAD_KEY)'; + return 'API Key không hợp lệ (BAD_KEY)'; } if (/\bNO_BALANCE\b|\bNOT_ENOUGH_BALANCE\b/i.test(text)) { - return '余额不足'; + return 'Số dư không đủ'; } if (/\bBANNED\b|\bACCOUNT_BANNED\b/i.test(text)) { - return '账号已被封禁'; + return 'Tài khoản đã bị khóa'; } if (/\bNO_NUMBERS\b/i.test(text)) { - return '暂无可用号码(NO_NUMBERS)'; + return 'Hiện không có số khả dụng (NO_NUMBERS)'; } if (/no\s+free\s+phones|numbers?\s+not\s+found|no\s+numbers\s+available|no\s+numbers\s+within|暂无可用号码|均无可用号码|无可用号码/i.test(text)) { - return '暂无可用号码'; + return 'Hiện không có số khả dụng'; } const wrongMaxPrice = text.match(/\bWRONG_MAX_PRICE(?::|\s+requires\s+)?(\d+(?:\.\d+)?)?\b/i); if (wrongMaxPrice) { @@ -1403,25 +1403,25 @@ : '价格上限不符合平台要求(WRONG_MAX_PRICE)'; } if (/rate\s*limit|too\s+many\s+requests|限流/i.test(text)) { - return '请求限流'; + return 'Yêu cầu bị giới hạn tần suất'; } if (/unauthorized|forbidden|invalid\s+token|bad\s+key|wrong\s+key/i.test(text)) { - return 'API Key 无效'; + return 'API Key không hợp lệ'; } if (/order\s+not\s+found|activation\s+not\s+found|no\s+such\s+order/i.test(text)) { - return '订单不存在或已失效'; + return 'Đơn hàng không tồn tại hoặc đã hết hiệu lực'; } if (/timed\s*out|timeout/i.test(text)) { - return '请求超时'; + return 'Yêu cầu hết thời gian chờ'; } if (/failed\s+to\s+fetch|networkerror|load\s+failed/i.test(text)) { - return '网络请求失败'; + return 'Yêu cầu mạng thất bại'; } if (/empty\s+response/i.test(text)) { - return '空响应'; + return 'Phản hồi rỗng'; } if (/unknown\s+terminal\s+error/i.test(text)) { - return '未知终止错误'; + return 'Lỗi kết thúc không xác định'; } return text; } @@ -1429,16 +1429,16 @@ function formatHeroSmsActionName(action = '') { const normalized = String(action || '').trim().toLowerCase(); if (normalized === 'getnumber' || normalized === 'getnumberv2') { - return '获取手机号'; + return 'Lấy số điện thoại'; } if (normalized === 'getstatus' || normalized === 'getstatusv2') { - return '查询短信状态'; + return 'Kiểm tra trạng thái SMS'; } if (normalized === 'setstatus') { - return '更新订单状态'; + return 'Cập nhật trạng thái đơn hàng'; } if (normalized === 'getprices' || normalized === 'getpricesextended') { - return '查询价格'; + return 'Kiểm tra giá'; } return action ? `${action} 请求` : '请求'; } @@ -1446,7 +1446,7 @@ function formatPhoneSmsActionLabel(actionLabel = '') { const text = String(actionLabel || '').trim(); if (!text) { - return '接码平台请求'; + return 'Yêu cầu tới nền tảng nhận mã'; } const normalized = text.toLowerCase(); const heroMatch = text.match(/^HeroSMS\s+(.+)$/i); @@ -1454,34 +1454,34 @@ return `HeroSMS ${formatHeroSmsActionName(heroMatch[1])}`; } if (normalized === '5sim guest prices') { - return '5sim 查询游客价格'; + return '5sim kiểm tra giá khách'; } if (normalized === '5sim user prices') { - return '5sim 查询账号价格'; + return '5sim kiểm tra giá tài khoản'; } if (normalized === '5sim buy activation') { - return '5sim 购买手机号'; + return '5sim mua số điện thoại'; } if (normalized === '5sim check activation') { - return '5sim 查询短信状态'; + return '5sim kiểm tra trạng thái SMS'; } if (normalized === '5sim reuse activation') { - return '5sim 复用手机号'; + return '5sim tái sử dụng số điện thoại'; } if (normalized === 'nexsms getcountrybyservice') { - return 'NexSMS 查询服务国家'; + return 'NexSMS kiểm tra quốc gia dịch vụ'; } if (normalized === 'nexsms price lookup') { - return 'NexSMS 查询价格'; + return 'NexSMS kiểm tra giá'; } if (normalized === 'nexsms purchase') { - return 'NexSMS 购买手机号'; + return 'NexSMS mua số điện thoại'; } if (normalized === 'nexsms close activation') { - return 'NexSMS 关闭订单'; + return 'NexSMS đóng đơn hàng'; } if (normalized === 'nexsms get sms messages') { - return 'NexSMS 查询短信'; + return 'NexSMS kiểm tra SMS'; } return text; } @@ -1522,7 +1522,7 @@ const providerLabel = getPhoneSmsProviderLabel(providerId); let text = String(message || '').trim(); if (!text) { - return '未知错误'; + return 'Lỗi không xác định'; } text = text.replace(/^Step\s+\d+\s*[::]\s*/i, '').trim(); const heroFailureMatch = text.match(/^HeroSMS\s+([A-Za-z0-9]+)\s+failed\s*:\s*(.+)$/i); @@ -1533,10 +1533,10 @@ return text.replace(/^HeroSMS\s+/, '').trim(); } if (/countries\s+are\s+empty|未选择国家/i.test(text)) { - return '未选择国家,请先在接码设置中至少选择 1 个国家'; + return 'Chưa chọn quốc gia, hãy chọn ít nhất 1 quốc gia trong phần cài đặt nhận mã trước'; } if (/failed\s+to\s+acquire\s+(?:a\s+)?phone(?:\s+number|\s+activation)?/i.test(text)) { - return '获取手机号失败'; + return 'Lấy số điện thoại thất bại'; } if (/no\s+numbers\s+available\s+across|no\s+free\s+phones|numbers?\s+not\s+found|no\s+numbers\s+within|暂无可用号码|均无可用号码|无可用号码|\bNO_NUMBERS\b/i.test(text)) { return formatPhoneSmsApiFailureReason(text); @@ -1568,7 +1568,7 @@ async function logPreservedPhoneActivationOnStop(activation) { const normalizedActivation = normalizeActivation(activation); const identifier = normalizedActivation?.phoneNumber || normalizedActivation?.activationId || '当前接码手机号'; - await addLog(`已停止:按设置保留 ${identifier},不自动释放接码订单。`, 'warn'); + await addLog(`Đã dừng: giữ lại ${identifier} theo cấu hình, không tự động giải phóng đơn nhận mã.`, 'warn'); } function createResolvedFiveSimProvider() { @@ -2082,20 +2082,20 @@ function getFreeReuseEligibility(activation) { const normalizedActivation = normalizeActivation(activation); if (!normalizedActivation) { - return { ok: false, reason: 'activation_missing', message: '接码订单无效。' }; + return { ok: false, reason: 'activation_missing', message: 'Đơn nhận mã không hợp lệ.' }; } if (!supportsFreePhoneReuseProvider(normalizedActivation.provider)) { return { ok: false, reason: 'provider_not_supported', - message: `${getPhoneSmsProviderLabel(normalizedActivation.provider)} 当前不支持白嫖复用。`, + message: `${getPhoneSmsProviderLabel(normalizedActivation.provider)} hiện chưa hỗ trợ tái sử dụng miễn phí.`, }; } if (!normalizedActivation.phoneCodeReceived) { return { ok: false, reason: 'code_not_received', - message: '当前号码尚未成功收到验证码,不能进入白嫖复用。', + message: 'Số hiện tại chưa nhận được mã xác minh thành công, không thể vào chế độ tái sử dụng miễn phí.', }; } if ( @@ -2434,7 +2434,7 @@ function buildPhoneCodeTimeoutError(lastResponse = '') { const suffix = lastResponse ? ` HeroSMS 最后状态:${lastResponse}` : ''; - return new Error(`${PHONE_CODE_TIMEOUT_ERROR_PREFIX}等待手机验证码超时。${suffix}`); + return new Error(`${PHONE_CODE_TIMEOUT_ERROR_PREFIX}Hết thời gian chờ mã xác minh điện thoại.${suffix}`); } function isSignupEmailVerificationPageState(pageState = {}) { @@ -2870,7 +2870,7 @@ if (provider === PHONE_SMS_PROVIDER_5SIM) { const apiKey = normalizeApiKey(state.fiveSimApiKey || state.heroSmsApiKey); if (!apiKey) { - throw new Error('5sim API Key 缺失,请先在侧边栏保存接码 API Key。'); + throw new Error('Thiếu 5sim API Key, hãy lưu API Key nhận mã trong thanh bên trước.'); } const configuredMaxPrice = normalizeHeroSmsPriceLimit(state.fiveSimMaxPrice); const operator = normalizeFiveSimCountryCode(state.fiveSimOperator, DEFAULT_FIVE_SIM_OPERATOR); @@ -2892,7 +2892,7 @@ if (provider === PHONE_SMS_PROVIDER_NEXSMS) { const apiKey = normalizeApiKey(state.nexSmsApiKey || state.heroSmsApiKey); if (!apiKey) { - throw new Error('NexSMS API Key 缺失,请先在侧边栏保存接码 API Key。'); + throw new Error('Thiếu NexSMS API Key, hãy lưu API Key nhận mã trong thanh bên trước.'); } return { provider, @@ -2906,7 +2906,7 @@ if (provider === PHONE_SMS_PROVIDER_SMSBOWER) { const apiKey = normalizeApiKey(state.smsBowerApiKey || state.heroSmsApiKey); if (!apiKey) { - throw new Error('SMSBower API Key 缺失,请先在侧边栏保存接码 API Key。'); + throw new Error('Thiếu SMSBower API Key, hãy lưu API Key nhận mã trong thanh bên trước.'); } return { provider, @@ -2925,7 +2925,7 @@ if (provider === PHONE_SMS_PROVIDER_SMS_VERIFICATION_NUMBER) { const apiKey = normalizeApiKey(state.smsVerificationNumberApiKey || state.heroSmsApiKey); if (!apiKey) { - throw new Error('SMS Verification Number API Key 缺失,请先在侧边栏保存接码 API Key。'); + throw new Error('Thiếu API Key của SMS Verification Number, hãy lưu API Key nhận mã trong thanh bên trước.'); } return { provider, @@ -2944,7 +2944,7 @@ if (provider === PHONE_SMS_PROVIDER_GRIZZLYSMS) { const apiKey = normalizeApiKey(state.grizzlySmsApiKey || state.heroSmsApiKey); if (!apiKey) { - throw new Error('GrizzlySMS API Key 缺失,请先在侧边栏保存接码 API Key。'); + throw new Error('Thiếu GrizzlySMS API Key, hãy lưu API Key nhận mã trong thanh bên trước.'); } return { provider, @@ -2962,7 +2962,7 @@ if (provider === PHONE_SMS_PROVIDER_SMSPOOL) { const apiKey = normalizeApiKey(state.smsPoolApiKey || state.heroSmsApiKey); if (!apiKey) { - throw new Error('SMSPool API Key 缺失,请先在侧边栏保存接码 API Key。'); + throw new Error('Thiếu SMSPool API Key, hãy lưu API Key nhận mã trong thanh bên trước.'); } return { provider, @@ -2986,7 +2986,7 @@ const apiKey = normalizeApiKey(state.heroSmsApiKey); if (!apiKey) { - throw new Error('HeroSMS API Key 缺失,请先在侧边栏保存接码 API Key。'); + throw new Error('Thiếu HeroSMS API Key, hãy lưu API Key nhận mã trong thanh bên trước.'); } return { provider, @@ -2999,7 +2999,7 @@ function resolveHeroSmsPhoneConfig(state = {}) { const apiKey = normalizeApiKey(state.heroSmsApiKey); if (!apiKey) { - throw new Error('HeroSMS API Key 缺失,请先在侧边栏保存接码 API Key。'); + throw new Error('Thiếu HeroSMS API Key, hãy lưu API Key nhận mã trong thanh bên trước.'); } return { provider: PHONE_SMS_PROVIDER_HERO, diff --git a/background/steps/create-plus-checkout.js b/background/steps/create-plus-checkout.js index 03448f1..787e231 100644 --- a/background/steps/create-plus-checkout.js +++ b/background/steps/create-plus-checkout.js @@ -13,8 +13,8 @@ const PLUS_CHECKOUT_MODE_JP_PP = 'jp_pp'; const DEFAULT_PLUS_CHECKOUT_MODE = PLUS_CHECKOUT_MODE_US_PP; const PLUS_CHECKOUT_MODE_LABELS = Object.freeze({ - [PLUS_CHECKOUT_MODE_US_PP]: '美区PP Plus Checkout', - [PLUS_CHECKOUT_MODE_JP_PP]: '日区PP Plus Checkout', + [PLUS_CHECKOUT_MODE_US_PP]: 'Plus Checkout PayPal khu vực Mỹ', + [PLUS_CHECKOUT_MODE_JP_PP]: 'Plus Checkout PayPal khu vực Nhật', }); const PLUS_ACCOUNT_ACCESS_STRATEGY_SMS_OAUTH = 'sms_oauth'; const DEFAULT_GPC_HELPER_API_URL = 'https://your-gpc-helper-domain.example'; @@ -343,9 +343,9 @@ function getCheckoutModeLabel(state = {}) { const paymentMethod = normalizePlusPaymentMethod(state?.plusPaymentMethod); if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) { - return 'GPC 订阅页'; + return 'Trang đăng ký GPC'; } - return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? 'GoPay 订阅页' : 'Plus Checkout'; + return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? 'Trang đăng ký GoPay' : 'Plus Checkout'; } async function refreshOAuthTimeoutWindowAfterHostedCheckoutSuccess() { @@ -568,9 +568,9 @@ if (!errorInspection) { return inspections; } - const message = String(errorInspection?.result?.customCheckoutErrorMessage || '').trim() || '页面出现错误提示框。'; + const message = String(errorInspection?.result?.customCheckoutErrorMessage || '').trim() || 'Trang hiển thị hộp thông báo lỗi.'; const frameLabel = Number(errorInspection?.frame?.frameId) === 0 - ? '主页面' + ? 'trang chính' : `iframe(${Number(errorInspection?.frame?.frameId) || 0})`; throw new Error(`${PLUS_CHECKOUT_START_NEW_FLOW_ERROR_PREFIX}步骤 6:${contextLabel ? `${contextLabel}:` : ''}custom checkout 检测到错误提示框(${frameLabel}):${message}`); } @@ -675,7 +675,7 @@ async function waitForBillingFrame(tabId) { const result = await waitForFrameMatch(tabId, pickBillingFrame, { - label: '账单地址 iframe', + label: 'iframe địa chỉ thanh toán', }); return { frameId: result.picked.frame.frameId, diff --git a/background/steps/fetch-login-code.js b/background/steps/fetch-login-code.js index b507afb..d164f1a 100644 --- a/background/steps/fetch-login-code.js +++ b/background/steps/fetch-login-code.js @@ -121,7 +121,7 @@ return async (details = {}) => getOAuthFlowRemainingMs({ step: visibleStep, - actionLabel: details.actionLabel || '登录验证码流程', + actionLabel: details.actionLabel || 'luồng mã xác minh đăng nhập', oauthUrl: expectedOauthUrl, }); } diff --git a/background/steps/fill-plus-checkout.js b/background/steps/fill-plus-checkout.js index 5e7ad43..1d5bb88 100644 --- a/background/steps/fill-plus-checkout.js +++ b/background/steps/fill-plus-checkout.js @@ -24,20 +24,20 @@ const GPC_TASK_STALE_STATUS_TIMEOUT_MS = 60000; const RANDOMUSER_ADDRESS_ENDPOINT = 'https://randomuser.me/api/?nat=us&inc=location&noinfo'; const GPC_REMOTE_STAGE_LABELS = { - auto_otp_wait: '等待自动 OTP', - checkout_order_start: '创建订单', - checkout_start: '创建订单', - completed: '充值完成', - gopay_validate_pin: '校验 PIN', - otp_ready: '等待 PIN', - otp_submitted_local: 'OTP 已提交', - payment_processing: '支付处理中', - pin_submitted_local: 'PIN 已提交', - sms_otp_wait: '等待短信 OTP', - whatsapp_otp_wait: '等待 WhatsApp OTP', + auto_otp_wait: 'Chờ OTP tự động', + checkout_order_start: 'Tạo đơn hàng', + checkout_start: 'Tạo đơn hàng', + completed: 'Nạp tiền hoàn tất', + gopay_validate_pin: 'Xác minh PIN', + otp_ready: 'Chờ PIN', + otp_submitted_local: 'Đã gửi OTP', + payment_processing: 'Đang xử lý thanh toán', + pin_submitted_local: 'Đã gửi PIN', + sms_otp_wait: 'Chờ OTP SMS', + whatsapp_otp_wait: 'Chờ OTP WhatsApp', }; const GPC_WAITING_FOR_LABELS = { - auto_otp: '自动 OTP', + auto_otp: 'OTP tự động', otp: 'OTP', pin: 'PIN', }; diff --git a/background/steps/submit-signup-email.js b/background/steps/submit-signup-email.js index c97074f..bc1bf26 100644 --- a/background/steps/submit-signup-email.js +++ b/background/steps/submit-signup-email.js @@ -98,7 +98,7 @@ }, { timeoutMs: 30000, retryDelayMs: 500, - logMessage: '步骤 2:正在检查官网注册入口状态...', + logMessage: 'Bước 2: Đang kiểm tra trạng thái cổng đăng ký trên trang chính thức...', }); if (result?.error) { return ''; diff --git a/background/verification-flow.js b/background/verification-flow.js index 435c36b..2618305 100644 --- a/background/verification-flow.js +++ b/background/verification-flow.js @@ -95,7 +95,7 @@ } function getVerificationCodeLabel(step) { - return step === 4 ? '注册' : '登录'; + return step === 4 ? 'đăng ký' : 'đăng nhập'; } function isIcloudMail(mail) { diff --git a/content/auth-page-recovery.js b/content/auth-page-recovery.js index 81d54c0..d2833af 100644 --- a/content/auth-page-recovery.js +++ b/content/auth-page-recovery.js @@ -158,7 +158,7 @@ if (retryState.maxCheckAttemptsBlocked) { throw new Error( - 'CF_SECURITY_BLOCKED::您已触发Cloudflare 安全防护系统,已完全停止流程,请不要短时间内多次进行重新发送验证码,连续刷新、反复点击重试会加重风控;请先关闭页面等待 15-30 分钟,让系统的临时限制自动解除。或者更换浏览器' + 'CF_SECURITY_BLOCKED::Bạn đã kích hoạt lớp bảo vệ Cloudflare và quy trình đã bị dừng hoàn toàn. Đừng gửi lại mã quá nhiều lần trong thời gian ngắn; việc refresh liên tục hoặc bấm thử lại lặp đi lặp lại sẽ làm tăng mức kiểm soát rủi ro. Hãy đóng trang và chờ 15-30 phút để giới hạn tạm thời tự gỡ, hoặc đổi sang trình duyệt khác.' ); } @@ -217,7 +217,7 @@ if (finalRetryState.maxCheckAttemptsBlocked) { throw new Error( - 'CF_SECURITY_BLOCKED::您已触发Cloudflare 安全防护系统,已完全停止流程,请不要短时间内多次进行重新发送验证码,连续刷新、反复点击重试会加重风控;请先关闭页面等待 15-30 分钟,让系统的临时限制自动解除。或者更换浏览器' + 'CF_SECURITY_BLOCKED::Bạn đã kích hoạt lớp bảo vệ Cloudflare và quy trình đã bị dừng hoàn toàn. Đừng gửi lại mã quá nhiều lần trong thời gian ngắn; việc refresh liên tục hoặc bấm thử lại lặp đi lặp lại sẽ làm tăng mức kiểm soát rủi ro. Hãy đóng trang và chờ 15-30 phút để giới hạn tạm thời tự gỡ, hoặc đổi sang trình duyệt khác.' ); } diff --git a/content/gmail-mail.js b/content/gmail-mail.js index f9eb84c..cacea67 100644 --- a/content/gmail-mail.js +++ b/content/gmail-mail.js @@ -43,11 +43,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { sendResponse(result); }).catch((err) => { if (isStopError(err)) { - log(`步骤 ${message.step}:已被用户停止。`, 'warn'); + log(`Bước ${message.step}: đã bị người dùng dừng.`, 'warn'); sendResponse({ stopped: true, error: err.message }); return; } - log(`步骤 ${message.step}:Gmail 轮询失败:${err.message}`, 'warn'); + log(`Bước ${message.step}: Gmail thăm dò thất bại: ${err.message}`, 'warn'); sendResponse({ error: err.message }); }); return true; @@ -658,7 +658,7 @@ async function handlePollEmail(step, payload) { }); if (previewCode) { if (excludedCodeSet.has(previewCode)) { - log(`步骤 ${step}:跳过排除的验证码:${previewCode}`, 'info'); + log(`Bước ${step}: bỏ qua mã xác minh nằm trong danh sách loại trừ: ${previewCode}`, 'info'); continue; } if (seenCodes.has(previewCode)) { @@ -688,7 +688,7 @@ async function handlePollEmail(step, payload) { continue; } if (excludedCodeSet.has(bodyCode)) { - log(`步骤 ${step}:跳过排除的验证码:${bodyCode}`, 'info'); + log(`Bước ${step}: bỏ qua mã xác minh nằm trong danh sách loại trừ: ${bodyCode}`, 'info'); continue; } if (seenCodes.has(bodyCode)) { @@ -720,7 +720,7 @@ async function handlePollEmail(step, payload) { } throw new Error( - `${(maxAttempts * intervalMs / 1000).toFixed(0)} 秒后仍未在 Gmail 中找到匹配邮件。请手动检查 Gmail 收件箱。` + `${(maxAttempts * intervalMs / 1000).toFixed(0)} giây trôi qua nhưng vẫn chưa tìm thấy email khớp trong Gmail. Hãy tự kiểm tra hộp thư Gmail.` ); } diff --git a/content/gopay-flow.js b/content/gopay-flow.js index 570e0b9..7d841f3 100644 --- a/content/gopay-flow.js +++ b/content/gopay-flow.js @@ -32,7 +32,7 @@ if (document.documentElement.getAttribute(GOPAY_FLOW_LISTENER_SENTINEL) !== '1') } }); } else { - console.log('[MultiPage:gopay-flow] 消息监听已存在,跳过重复注册'); + console.log('[MultiPage:gopay-flow] Trình nghe tin nhắn đã tồn tại, bỏ qua đăng ký lặp lại'); } async function performGoPayOperationWithDelay(metadata, operation) { @@ -60,7 +60,7 @@ async function handleGoPayCommand(message) { case 'GOPAY_GET_PAY_NOW_TARGET': return getGoPayPayNowTarget(); default: - throw new Error(`gopay-flow.js 不处理消息:${message.type}`); + throw new Error(`gopay-flow.js không xử lý tin nhắn: ${message.type}`); } } @@ -75,7 +75,7 @@ async function waitUntil(predicate, options = {}) { return value; } if (timeoutMs > 0 && Date.now() - startedAt >= timeoutMs) { - throw new Error(options.timeoutMessage || `${options.label || 'GoPay 页面状态'}等待超时`); + throw new Error(options.timeoutMessage || `${options.label || 'Trạng thái trang GoPay'} đã chờ quá thời gian`); } await sleep(intervalMs); } @@ -97,7 +97,7 @@ async function waitForDocumentComplete(options = {}) { label: 'GoPay DOM', }); } catch (_) { - // GoPay linking 页面有时长时间保持 loading;后续定位控件本身还有等待/重试。 + // Trang liên kết GoPay đôi khi giữ trạng thái loading rất lâu; các bước tìm điều khiển phía sau vẫn có cơ chế chờ/thử lại. } await sleep(settleMs); } @@ -230,7 +230,7 @@ function detectGoPayTerminalError(text = getPageBodyText()) { if (/waktunya\s+habis|ulang(?:i)?\s+prosesnya\s+dari\s+awal|time(?:'s|\s+is)?\s+(?:out|expired)|session\s+expired|expired|kedaluwarsa/i.test(normalizedText)) { return { code: 'expired', - message: 'GoPay 支付会话已超时,需要重新创建 Plus Checkout。', + message: 'Phiên thanh toán GoPay đã hết hạn, cần tạo lại Plus Checkout.', rawText: normalizedText.slice(0, 240), }; } @@ -238,7 +238,7 @@ function detectGoPayTerminalError(text = getPageBodyText()) { if (/technical\s+error|don[’']t\s+worry|try\s+again|terjadi\s+kesalahan|error\s+teknis/i.test(normalizedText)) { return { code: 'technical-error', - message: 'GoPay 页面显示技术错误,需要重新发起支付授权。', + message: 'Trang GoPay hiển thị lỗi kỹ thuật, cần khởi tạo lại ủy quyền thanh toán.', rawText: normalizedText.slice(0, 240), }; } @@ -246,7 +246,7 @@ function detectGoPayTerminalError(text = getPageBodyText()) { if (/payment\s+failed|pembayaran\s+gagal|transaksi\s+gagal|ditolak|declined|failed/i.test(normalizedText)) { return { code: 'failed', - message: 'GoPay 页面显示支付失败,需要重新发起支付授权。', + message: 'Trang GoPay hiển thị thanh toán thất bại, cần khởi tạo lại ủy quyền thanh toán.', rawText: normalizedText.slice(0, 240), }; } @@ -371,7 +371,7 @@ function dispatchPointerMouseSequence(target) { async function humanClickElement(el, options = {}) { if (!el) { - throw new Error('GoPay 页面未找到可点击元素。'); + throw new Error('Trang GoPay không tìm thấy phần tử có thể bấm.'); } el.scrollIntoView?.({ block: 'center', inline: 'center' }); await sleep(Math.max(0, Number(options.beforeMs) || 120)); @@ -519,7 +519,7 @@ async function ensureGoPayCountryCode(countryCode = '+86') { const toggle = findCountryCodeToggle(); if (!toggle) { - throw new Error(`GoPay 页面未找到国家区号切换控件,当前识别区号:${selected || '未知'},目标区号:${normalized}`); + throw new Error(`Trang GoPay không tìm thấy điều khiển đổi mã quốc gia, mã hiện nhận diện: ${selected || 'không rõ'}, mã đích: ${normalized}`); } robustClick(toggle); await sleep(500); @@ -537,7 +537,7 @@ async function ensureGoPayCountryCode(countryCode = '+86') { } const option = await waitUntil(() => findCountryCodeOption(normalized), { - label: `GoPay 国家区号 ${normalized}`, + label: `Mã quốc gia GoPay ${normalized}`, intervalMs: 250, timeoutMs: 8000, }); @@ -549,7 +549,7 @@ async function ensureGoPayCountryCode(countryCode = '+86') { countryDropdown.style.display = 'none'; } if (nextSelected !== normalized) { - throw new Error(`GoPay 国家区号切换失败:目标 ${normalized},当前 ${nextSelected || '未知'}`); + throw new Error(`Đổi mã quốc gia GoPay thất bại: đích ${normalized}, hiện tại ${nextSelected || 'không rõ'}`); } return { changed: true, @@ -639,10 +639,10 @@ async function submitGoPayPhone(payload = {}) { const countryCode = normalizeGoPayCountryCode(payload.countryCode || payload.gopayCountryCode || '+86'); const phone = normalizeGoPayNationalPhone(payload.phone || payload.gopayPhone || '', countryCode); if (!phone) { - throw new Error('GoPay 手机号为空,请先在侧边栏配置。'); + throw new Error('Số điện thoại GoPay đang trống, hãy cấu hình trước trong thanh bên.'); } const input = await waitUntil(() => findPhoneInput(), { - label: 'GoPay 手机号输入框', + label: 'Ô nhập số điện thoại GoPay', intervalMs: 250, timeoutMs: 15000, }); @@ -676,11 +676,11 @@ async function submitGoPayOtp(payload = {}) { await waitForDocumentComplete(); const code = normalizeOtp(payload.code || payload.otp || ''); if (!code) { - throw new Error('GoPay WhatsApp 验证码为空。'); + throw new Error('Mã xác minh WhatsApp của GoPay đang trống.'); } const { filled, clickResult } = await delayOperation({ stepKey: 'gopay-approve', kind: 'submit', label: 'submit-otp' }, async () => { const filledOtp = await waitUntil(() => fillVisibleOtpInputs(code), { - label: 'GoPay 验证码输入框', + label: 'Ô nhập mã xác minh GoPay', intervalMs: 250, timeoutMs: 15000, }); @@ -706,11 +706,11 @@ async function submitGoPayPin(payload = {}) { await waitForDocumentComplete(); const pin = normalizeOtp(payload.pin || payload.gopayPin || ''); if (!pin) { - throw new Error('GoPay PIN 为空,请先在侧边栏配置。'); + throw new Error('PIN GoPay đang trống, hãy cấu hình trước trong thanh bên.'); } const { filled, clickResult } = await delayOperation({ stepKey: 'gopay-approve', kind: 'submit', label: 'submit-pin' }, async () => { const filledPin = await waitUntil(() => fillVisiblePinInputs(pin), { - label: 'GoPay PIN 输入框', + label: 'Ô nhập PIN GoPay', intervalMs: 250, timeoutMs: 15000, }); diff --git a/content/icloud-mail.js b/content/icloud-mail.js index a3e5ab3..d1a0b1f 100644 --- a/content/icloud-mail.js +++ b/content/icloud-mail.js @@ -389,12 +389,12 @@ if (shouldHandlePollEmailInCurrentFrame) { continue; } if (excludedCodeSet.has(code)) { - log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info'); + log(`Bước ${step}: bỏ qua mã xác minh nằm trong danh sách loại trừ: ${code}`, 'info'); continue; } const source = useFallback && existingSignatures.has(signature) ? '回退匹配邮件' : '新邮件'; - log(`步骤 ${step}:已找到验证码:${code}(来源:${source})`, 'ok'); + log(`Bước ${step}: đã tìm thấy mã xác minh: ${code} (nguồn: ${source})`, 'ok'); persistPollSessionBaseline( pollSessionKey, new Set(collectThreadItems().map(buildItemSignature)), @@ -425,7 +425,7 @@ if (shouldHandlePollEmailInCurrentFrame) { ); throw new Error( - `${Math.round((maxAttempts * intervalMs) / 1000)} 秒后仍未在 iCloud 邮箱中找到新的匹配邮件。请手动检查收件箱。` + `${Math.round((maxAttempts * intervalMs) / 1000)} giây trôi qua nhưng vẫn chưa tìm thấy email mới khớp trong iCloud. Hãy tự kiểm tra hộp thư.` ); } } diff --git a/content/inbucket-mail.js b/content/inbucket-mail.js index accdc5b..c00769c 100644 --- a/content/inbucket-mail.js +++ b/content/inbucket-mail.js @@ -272,7 +272,7 @@ async function handleMailboxPollEmail(step, payload) { }); if (!code) continue; if (excludedCodeSet.has(code)) { - log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info'); + log(`Bước ${step}: bỏ qua mã xác minh nằm trong danh sách loại trừ: ${code}`, 'info'); continue; } @@ -284,7 +284,7 @@ async function handleMailboxPollEmail(step, payload) { const source = existingMailIds.has(mail.mailId) ? '回退匹配邮件' : '新邮件'; log( - `步骤 ${step}:已找到验证码:${code}(来源:${source},发件人:${mail.sender || '未知'},主题:${(mail.subject || '').slice(0, 60)})`, + `Bước ${step}: đã tìm thấy mã xác minh: ${code} (nguồn: ${source}, người gửi: ${mail.sender || 'không rõ'}, tiêu đề: ${(mail.subject || '').slice(0, 60)})`, 'ok' ); @@ -306,7 +306,7 @@ async function handleMailboxPollEmail(step, payload) { } throw new Error( - `${(maxAttempts * intervalMs / 1000).toFixed(0)} 秒后仍未在 Inbucket 邮箱中找到匹配的验证码邮件。` + + `${(maxAttempts * intervalMs / 1000).toFixed(0)} giây trôi qua nhưng vẫn chưa tìm thấy email mã xác minh khớp trong Inbucket.` + '请手动检查邮箱页面。' ); } diff --git a/content/mail-163.js b/content/mail-163.js index 95a4bf4..989eac5 100644 --- a/content/mail-163.js +++ b/content/mail-163.js @@ -54,11 +54,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { sendResponse(result); }).catch(err => { if (isStopError(err)) { - log(`步骤 ${message.step}:已被用户停止。`, 'warn'); + log(`Bước ${message.step}: đã bị người dùng dừng.`, 'warn'); sendResponse({ stopped: true, error: err.message }); return; } - log(`步骤 ${message.step}:邮箱轮询失败:${err.message}`, 'warn'); + log(`Bước ${message.step}: thăm dò email thất bại: ${err.message}`, 'warn'); sendResponse({ error: err.message }); }); return true; diff --git a/content/mail-2925.js b/content/mail-2925.js index 8d481fa..e432702 100644 --- a/content/mail-2925.js +++ b/content/mail-2925.js @@ -86,7 +86,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { ensureMail2925Session(message.payload).then((result) => { sendResponse(result); }).catch((err) => { - sendResponse({ error: err?.message || String(err || '2925 登录失败') }); + sendResponse({ error: err?.message || String(err || 'Đăng nhập 2925 thất bại') }); }); return true; } @@ -97,12 +97,12 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { sendResponse(result); }).catch((err) => { if (isStopError(err)) { - log(`步骤 ${message.step}:已被用户停止。`, 'warn'); + log(`Bước ${message.step}: đã bị người dùng dừng.`, 'warn'); sendResponse({ stopped: true, error: err.message }); return; } - log(`步骤 ${message.step}:邮箱轮询失败:${err.message}`, 'warn'); + log(`Bước ${message.step}: thăm dò email thất bại: ${err.message}`, 'warn'); sendResponse({ error: err.message }); }); return true; @@ -112,7 +112,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { Promise.resolve(deleteAllMailboxEmails(message.step)).then((deleted) => { sendResponse({ ok: true, deleted }); }).catch((err) => { - sendResponse({ ok: false, error: err?.message || String(err || '删除邮件失败') }); + sendResponse({ ok: false, error: err?.message || String(err || 'Xóa email thất bại') }); }); return true; } @@ -205,7 +205,7 @@ function normalizeNodeText(value) { } function buildMail2925LimitError(message = '') { - const normalized = normalizeNodeText(message || '子邮箱已达上限邮箱') || '子邮箱已达上限邮箱'; + const normalized = normalizeNodeText(message || 'Đã đạt giới hạn hộp thư phụ') || 'Đã đạt giới hạn hộp thư phụ'; return new Error(`${MAIL2925_LIMIT_ERROR_PREFIX}${normalized}`); } diff --git a/content/paypal-flow.js b/content/paypal-flow.js index e03cee2..6ef6644 100644 --- a/content/paypal-flow.js +++ b/content/paypal-flow.js @@ -43,7 +43,7 @@ if (document.documentElement.getAttribute(PAYPAL_FLOW_LISTENER_SENTINEL) !== '1' } }); } else { - console.log('[MultiPage:paypal-flow] 消息监听已存在,跳过重复注册'); + console.log('[MultiPage:paypal-flow] Trình nghe tin nhắn đã tồn tại, bỏ qua đăng ký lặp lại'); } async function performPayPalOperationWithDelay(metadata, operation) { @@ -67,7 +67,7 @@ async function handlePayPalCommand(message) { case 'PAYPAL_RUN_HOSTED_CHECKOUT_STEP': return runHostedCheckoutStep(message.payload || {}); default: - throw new Error(`paypal-flow.js 不处理消息:${message.type}`); + throw new Error(`paypal-flow.js không xử lý tin nhắn: ${message.type}`); } } @@ -717,17 +717,17 @@ async function switchHostedNationalityToUnitedStatesIfNeeded(countryCode = '') { if (!button) { return false; } - log('PayPal guest checkout:日区页面切换国籍为 United States,避免日文姓名校验。', 'info'); + log('PayPal guest checkout: trang khu vực JP sẽ chuyển quốc tịch sang United States để tránh kiểm tra tên tiếng Nhật.', 'info'); simulateClick(button); let option = null; try { option = await waitUntil(() => findHostedUnitedStatesNationalityOption(), { intervalMs: 250, timeoutMs: 5000, - timeoutMessage: 'PayPal guest checkout 未找到 United States 国籍选项。', + timeoutMessage: 'PayPal guest checkout không tìm thấy tùy chọn quốc tịch United States.', }); } catch (error) { - log(`PayPal guest checkout:国籍选项查找失败,继续按当前国籍填写。${error?.message || error}`, 'warn'); + log(`PayPal guest checkout: tìm tùy chọn quốc tịch thất bại, sẽ tiếp tục điền theo quốc tịch hiện tại. ${error?.message || error}`, 'warn'); return false; } simulateClick(option); @@ -737,7 +737,7 @@ async function switchHostedNationalityToUnitedStatesIfNeeded(countryCode = '') { timeoutMs: 8000, }); } catch { - log('PayPal guest checkout:等待国籍切换到 United States 超时,继续尝试填写英文姓名。', 'warn'); + log('PayPal guest checkout: chờ chuyển quốc tịch sang United States quá thời gian, tiếp tục thử điền tên tiếng Anh.', 'warn'); } await sleep(1000); return true; @@ -801,7 +801,7 @@ async function switchHostedGuestCheckoutToEnglishIfNeeded(countryCode = '') { if (!button) { return false; } - log('PayPal guest checkout:检测到日区页面,先切换到 English 后再填写。', 'info'); + log('PayPal guest checkout: phát hiện trang khu vực JP, sẽ chuyển sang English trước khi điền.', 'info'); simulateClick(button); try { await waitUntil(() => isHostedGuestCheckoutLikelyEnglish() || !findHostedEnglishLanguageButton(), { @@ -809,7 +809,7 @@ async function switchHostedGuestCheckoutToEnglishIfNeeded(countryCode = '') { timeoutMs: 8000, }); } catch { - log('PayPal guest checkout:等待 English 页面完成超时,继续按当前页面状态尝试填写。', 'warn'); + log('PayPal guest checkout: chờ trang English hoàn tất quá thời gian, sẽ tiếp tục thử điền theo trạng thái hiện tại.', 'warn'); } await waitForDocumentComplete(); await sleep(1000); @@ -943,7 +943,7 @@ async function clickHostedGenericSubmitButton(retries = 0) { const button = findHostedGuestSubmitButton() || findEmailNextButton() || findLoginNextButton(); if (!button) { if (retries >= 10) { - throw new Error('PayPal hosted checkout 未找到可点击的继续/提交按钮。'); + throw new Error('PayPal hosted checkout không tìm thấy nút tiếp tục/gửi có thể bấm.'); } await sleep(1000); return clickHostedGenericSubmitButton(retries + 1); @@ -952,7 +952,7 @@ async function clickHostedGenericSubmitButton(retries = 0) { const buttonText = normalizeText(button.textContent || ''); if (button.disabled) { if (retries >= 10) { - throw new Error('PayPal hosted checkout 按钮长时间处于 disabled 状态。'); + throw new Error('Nút PayPal hosted checkout bị disabled quá lâu.'); } await sleep(1000); return clickHostedGenericSubmitButton(retries + 1); @@ -961,7 +961,7 @@ async function clickHostedGenericSubmitButton(retries = 0) { const rect = button.getBoundingClientRect(); if (rect.height === 0) { if (retries >= 10) { - throw new Error('PayPal hosted checkout 按钮长时间不可见。'); + throw new Error('Nút PayPal hosted checkout không hiển thị trong thời gian dài.'); } await sleep(1000); return clickHostedGenericSubmitButton(retries + 1); diff --git a/content/plus-checkout.js b/content/plus-checkout.js index 972077e..18bffa1 100644 --- a/content/plus-checkout.js +++ b/content/plus-checkout.js @@ -83,7 +83,7 @@ if (document.documentElement.getAttribute(PLUS_CHECKOUT_LISTENER_SENTINEL) !== ' } }); } else { - console.log('[MultiPage:plus-checkout] 消息监听已存在,跳过重复注册'); + console.log('[MultiPage:plus-checkout] Trình nghe tin nhắn đã tồn tại, bỏ qua đăng ký lặp lại'); } async function handlePlusCheckoutCommand(message) { @@ -113,13 +113,13 @@ async function handlePlusCheckoutCommand(message) { case 'PLUS_CHECKOUT_GET_STATE': return inspectPlusCheckoutState(message.payload || {}); default: - throw new Error(`plus-checkout.js 不处理消息:${message.type}`); + throw new Error(`plus-checkout.js không xử lý tin nhắn: ${message.type}`); } } async function waitUntil(predicate, options = {}) { const intervalMs = Math.max(50, Math.floor(Number(options.intervalMs) || 250)); - const label = String(options.label || '条件').trim() || '条件'; + const label = String(options.label || 'điều kiện').trim() || 'điều kiện'; const timeoutMs = Math.max(0, Math.floor(Number(options.timeoutMs) || 0)); const startedAt = Date.now(); while (true) { @@ -129,7 +129,7 @@ async function waitUntil(predicate, options = {}) { return value; } if (timeoutMs > 0 && Date.now() - startedAt >= timeoutMs) { - throw new Error(`${label}等待超时`); + throw new Error(`${label} chờ quá thời gian`); } await sleep(intervalMs); } @@ -137,7 +137,7 @@ async function waitUntil(predicate, options = {}) { async function waitForDocumentComplete() { await waitUntil(() => document.readyState === 'complete', { - label: '页面加载完成', + label: 'trang đã tải xong', intervalMs: 200, timeoutMs: PLUS_CHECKOUT_DOCUMENT_COMPLETE_TIMEOUT_MS, }); @@ -420,11 +420,11 @@ function getHostedOpenAiCardFallbackState() { const cardPreview = paymentTextPreview.find((text) => /银行卡|card|cardholder|cvc|expiry|有效期|security/i.test(text)) || ''; const reasons = []; - if (!hasPayPalButton) reasons.push('未找到 PayPal 按钮'); - if (!hasPayPalTarget) reasons.push('未识别到 PayPal 付款方式'); - if (!hasGoPayTarget) reasons.push('未识别到 GoPay 付款方式'); - if (cardFieldsVisible) reasons.push('银行卡字段可见'); - if (cardAccordionSelected) reasons.push('银行卡卡片已选中'); + if (!hasPayPalButton) reasons.push('Không tìm thấy nút PayPal'); + if (!hasPayPalTarget) reasons.push('Không nhận diện được phương thức thanh toán PayPal'); + if (!hasGoPayTarget) reasons.push('Không nhận diện được phương thức thanh toán GoPay'); + if (cardFieldsVisible) reasons.push('Trường thẻ ngân hàng đang hiển thị'); + if (cardAccordionSelected) reasons.push('Thẻ ngân hàng đã được chọn'); if (paypalDisabledSignals) reasons.push('页面信号显示 paypal=never'); if (cardPreview) reasons.push(`支付文案包含“${cardPreview.slice(0, 40)}”`); diff --git a/content/qq-mail.js b/content/qq-mail.js index 73b9eba..6ee4ad6 100644 --- a/content/qq-mail.js +++ b/content/qq-mail.js @@ -148,11 +148,11 @@ async function handlePollEmail(step, payload) { }); if (code) { if (excludedCodeSet.has(code)) { - log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info'); + log(`Bước ${step}: bỏ qua mã xác minh nằm trong danh sách loại trừ: ${code}`, 'info'); continue; } const source = useFallback && existingMailIds.has(mailId) ? '回退首封匹配邮件' : '新邮件'; - log(`步骤 ${step}:已找到验证码:${code}(来源:${source},主题:${subject.slice(0, 40)})`, 'ok'); + log(`Bước ${step}: đã tìm thấy mã xác minh: ${code} (nguồn: ${source}, tiêu đề: ${subject.slice(0, 40)})`, 'ok'); return { ok: true, code, emailTimestamp: Date.now(), mailId }; } } diff --git a/content/signup-page.js b/content/signup-page.js index 86ec506..024ba8e 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -51,7 +51,7 @@ if (document.documentElement.getAttribute(SIGNUP_PAGE_LISTENER_SENTINEL) !== '1' const reportedNodeId = resolveCommandNodeId(message); if (isStopError(err)) { if (reportedStep) { - log(`步骤 ${reportedStep || 8}:已被用户停止。`, 'warn'); + log(`Bước ${reportedStep || 8}: đã bị người dùng dừng.`, 'warn'); } sendResponse({ stopped: true, error: err.message }); return; @@ -72,7 +72,7 @@ if (document.documentElement.getAttribute(SIGNUP_PAGE_LISTENER_SENTINEL) !== '1' } }); } else { - console.log('[MultiPage:signup-page] 消息监听已存在,跳过重复注册'); + console.log('[MultiPage:signup-page] Trình nghe tin nhắn đã tồn tại, bỏ qua đăng ký lặp lại'); } const SIGNUP_PAGE_NODE_HANDLERS = Object.freeze({ @@ -108,7 +108,7 @@ async function handleCommand(message) { const nodeId = String(message.nodeId || message.payload?.nodeId || '').trim(); const handler = SIGNUP_PAGE_NODE_HANDLERS[nodeId]; if (!handler) { - throw new Error(`signup-page.js 不处理节点 ${nodeId}`); + throw new Error(`signup-page.js không xử lý node ${nodeId}`); } return await handler(message.payload || {}); } @@ -390,13 +390,13 @@ async function readChatGptSessionExportData() { credentials: 'include', }); if (!sessionResponse.ok) { - throw new Error(`读取 ChatGPT 会话失败(HTTP ${sessionResponse.status})。`); + throw new Error(`Đọc phiên ChatGPT thất bại (HTTP ${sessionResponse.status}).`); } const session = await sessionResponse.json().catch(() => ({})); const accessToken = String(session?.accessToken || '').trim(); if (!accessToken) { - throw new Error('当前页面未返回可用 accessToken,无法导出本地CPA JSON 无RT。'); + throw new Error('Trang hiện tại không trả về accessToken khả dụng, không thể xuất JSON CPA cục bộ không có RT.'); } return { @@ -1307,7 +1307,7 @@ async function fillSignupEmailAndContinue(email, step) { const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay; return typeof gate === 'function' ? gate(metadata, operation) : operation(); }; - if (!email) throw new Error(`未提供邮箱地址,步骤 ${step} 无法继续。`); + if (!email) throw new Error(`Chưa cung cấp địa chỉ email, bước ${step} không thể tiếp tục.`); const normalizedEmail = String(email || '').trim().toLowerCase(); const snapshot = await waitForSignupEntryState({ @@ -2665,7 +2665,7 @@ async function step3_fillEmailPassword(payload) { return typeof gate === 'function' ? gate(metadata, operation) : operation(); }; const { email, password } = payload; - if (!password) throw new Error('未提供密码,步骤 3 需要可用密码。'); + if (!password) throw new Error('Chưa cung cấp mật khẩu, bước 3 cần có mật khẩu khả dụng.'); const normalizedEmail = String(email || '').trim().toLowerCase(); const accountIdentifierType = String(payload?.accountIdentifierType || '').trim().toLowerCase() === 'phone' ? 'phone' @@ -3737,7 +3737,7 @@ async function recoverCurrentAuthRetryPage(payload = {}) { } if (retryState.maxCheckAttemptsBlocked) { - throw new Error('CF_SECURITY_BLOCKED::您已触发Cloudflare 安全防护系统,已完全停止流程,请不要短时间内多次进行重新发送验证码,连续刷新、反复点击重试会加重风控;请先关闭页面等待 15-30 分钟,让系统的临时限制自动解除。或者更换浏览器'); + throw new Error('CF_SECURITY_BLOCKED::Bạn đã kích hoạt lớp bảo vệ Cloudflare và quy trình đã bị dừng hoàn toàn. Đừng gửi lại mã quá nhiều lần trong thời gian ngắn; việc refresh liên tục hoặc bấm thử lại lặp đi lặp lại sẽ làm tăng mức kiểm soát rủi ro. Hãy đóng trang và chờ 15-30 phút để giới hạn tạm thời tự gỡ, hoặc đổi sang trình duyệt khác.'); } if (retryState.userAlreadyExistsBlocked) { throw createSignupUserAlreadyExistsError(); @@ -3782,7 +3782,7 @@ async function recoverCurrentAuthRetryPage(payload = {}) { }; } if (finalRetryState.maxCheckAttemptsBlocked) { - throw new Error('CF_SECURITY_BLOCKED::您已触发Cloudflare 安全防护系统,已完全停止流程,请不要短时间内多次进行重新发送验证码,连续刷新、反复点击重试会加重风控;请先关闭页面等待 15-30 分钟,让系统的临时限制自动解除。或者更换浏览器'); + throw new Error('CF_SECURITY_BLOCKED::Bạn đã kích hoạt lớp bảo vệ Cloudflare và quy trình đã bị dừng hoàn toàn. Đừng gửi lại mã quá nhiều lần trong thời gian ngắn; việc refresh liên tục hoặc bấm thử lại lặp đi lặp lại sẽ làm tăng mức kiểm soát rủi ro. Hãy đóng trang và chờ 15-30 phút để giới hạn tạm thời tự gỡ, hoặc đổi sang trình duyệt khác.'); } if (finalRetryState.userAlreadyExistsBlocked) { throw createSignupUserAlreadyExistsError(); @@ -4499,7 +4499,7 @@ function getLoginAuthStateLabel(snapshot) { const state = snapshot?.state; switch (state) { case 'verification_page': - return '登录验证码页'; + return 'Trang mã xác minh đăng nhập'; case 'password_page': return '密码页'; case 'email_page': @@ -4507,7 +4507,7 @@ function getLoginAuthStateLabel(snapshot) { case 'phone_entry_page': return '手机号输入页'; case 'phone_verification_page': - return '手机验证码页'; + return 'Trang mã xác minh SĐT'; case 'login_timeout_error_page': return '登录超时报错页'; case 'oauth_consent_page': @@ -4563,7 +4563,7 @@ async function waitForLoginVerificationPageReady(timeout = 10000, visibleStep = } throw new Error( - `当前未进入登录验证码页面,请先重新完成步骤 ${Number(visibleStep) >= 11 ? 10 : 7}。当前状态:${getLoginAuthStateLabel(snapshot)}。URL: ${snapshot?.url || location.href}` + `Hiện chưa vào trang mã xác minh đăng nhập, hãy hoàn tất lại bước ${Number(visibleStep) >= 11 ? 10 : 7} trước. Trạng thái hiện tại: ${getLoginAuthStateLabel(snapshot)}. URL: ${snapshot?.url || location.href}` ); } @@ -4760,7 +4760,7 @@ async function finalizeStep6VerificationReady(options = {}) { if (snapshot.state === 'verification_page' || (allowPhoneVerificationPage && snapshot.state === 'phone_verification_page')) { log( - snapshot.state === 'phone_verification_page' ? '登录手机验证码页面已稳定就绪。' : '登录验证码页面已稳定就绪。', + snapshot.state === 'phone_verification_page' ? 'Trang mã xác minh SĐT đăng nhập đã ổn định.' : 'Trang mã xác minh đăng nhập đã ổn định.', 'ok', { step: visibleStep, stepKey: 'oauth-login' } ); @@ -4789,7 +4789,7 @@ async function finalizeStep6VerificationReady(options = {}) { return createStep6LoginTimeoutRecoverableResult( 'login_timeout_error_page', snapshot, - '登录验证码页面准备就绪前进入登录超时报错页。', + 'Quá trình chờ trang mã xác minh đăng nhập sẵn sàng đã rơi vào trang lỗi timeout đăng nhập.', { visibleStep } ); } @@ -4802,7 +4802,7 @@ async function finalizeStep6VerificationReady(options = {}) { } if (snapshot.state === 'add_phone_page') { - throw new Error(`登录验证码页面准备过程中页面进入手机号页面。URL: ${snapshot.url}`); + throw new Error(`Trang đã vào màn hình nhập số điện thoại trong lúc chờ trang mã xác minh đăng nhập. URL: ${snapshot.url}`); } } @@ -4810,7 +4810,7 @@ async function finalizeStep6VerificationReady(options = {}) { const snapshot = normalizeStep6Snapshot(rawSnapshot); if (snapshot.state === 'verification_page' || (allowPhoneVerificationPage && snapshot.state === 'phone_verification_page')) { log( - snapshot.state === 'phone_verification_page' ? '登录手机验证码页面已稳定就绪。' : '登录验证码页面已稳定就绪。', + snapshot.state === 'phone_verification_page' ? 'Trang mã xác minh SĐT đăng nhập đã ổn định.' : 'Trang mã xác minh đăng nhập đã ổn định.', 'ok', { step: visibleStep, stepKey: 'oauth-login' } ); @@ -4836,7 +4836,7 @@ async function finalizeStep6VerificationReady(options = {}) { return createStep6LoginTimeoutRecoverableResult( 'login_timeout_error_page', snapshot, - '登录验证码页面准备就绪前进入登录超时报错页。', + 'Quá trình chờ trang mã xác minh đăng nhập sẵn sàng đã rơi vào trang lỗi timeout đăng nhập.', { visibleStep } ); } @@ -4848,7 +4848,7 @@ async function finalizeStep6VerificationReady(options = {}) { } return createStep6RecoverableResult('verification_page_finalize_unknown', snapshot, { - message: `登录验证码页面状态在收尾确认阶段未稳定,准备重新执行步骤 ${visibleStep}。`, + message: `Trạng thái trang mã xác minh đăng nhập chưa ổn định ở bước xác nhận cuối, sẽ chạy lại bước ${visibleStep}.`, loginVerificationRequestedAt, }); } @@ -4863,9 +4863,9 @@ function throwForStep6FatalState(snapshot, visibleStep = 7) { case 'oauth_consent_page': return; case 'add_phone_page': - throw new Error(`当前页面已进入手机号页面,未经过登录验证码页,无法完成步骤 ${visibleStep}。URL: ${snapshot.url}`); + throw new Error(`Trang hiện đã vào màn hình nhập số điện thoại, chưa đi qua trang mã xác minh đăng nhập nên không thể hoàn tất bước ${visibleStep}. URL: ${snapshot.url}`); case 'unknown': - throw new Error(`无法识别当前登录页面状态。URL: ${snapshot?.url || location.href}`); + throw new Error(`Không nhận diện được trạng thái trang đăng nhập hiện tại. URL: ${snapshot?.url || location.href}`); default: return; } @@ -5486,7 +5486,7 @@ async function fillVerificationCode(step, payload) { } else if (outcome.emailVerificationRequired) { log(`步骤 ${step}:手机验证码已通过,页面进入邮箱验证码验证。`, 'ok'); } else if (outcome.addPhonePage) { - log(`步骤 ${step}:验证码提交后页面进入手机号页面,当前流程将停止自动授权。`, 'warn'); + log(`Bước ${step}: sau khi gửi mã xác minh, trang đã nhảy sang màn hình nhập số điện thoại; luồng hiện tại sẽ dừng auto-authorize.`, 'warn'); } else { if (typeof clearStep405RecoveryCount === 'function') clearStep405RecoveryCount(step); log(`步骤 ${step}:验证码已通过${outcome.assumed ? '(按成功推定)' : ''}。`, 'ok'); @@ -5527,7 +5527,7 @@ async function fillVerificationCode(step, payload) { } else if (outcome.emailVerificationRequired) { log(`步骤 ${step}:手机验证码已通过,页面进入邮箱验证码验证。`, 'ok'); } else if (outcome.addPhonePage) { - log(`步骤 ${step}:验证码提交后页面进入手机号页面,当前流程将停止自动授权。`, 'warn'); + log(`Bước ${step}: sau khi gửi mã xác minh, trang đã nhảy sang màn hình nhập số điện thoại; luồng hiện tại sẽ dừng auto-authorize.`, 'warn'); } else { if (typeof clearStep405RecoveryCount === 'function') clearStep405RecoveryCount(step); log(`步骤 ${step}:验证码已通过${outcome.assumed ? '(按成功推定)' : ''}。`, 'ok'); @@ -5712,8 +5712,8 @@ async function waitForStep6EmailSubmitTransition(emailSubmittedAt, timeout = 120 timeoutRecoveryVia: 'email_submit_timeout_recovered', allowPasswordAction: true, stalledReason: 'email_submit_stalled', - stalledMessage: '提交邮箱后长时间未进入密码页或登录验证码页。', - addPhoneMessage: (snapshot) => `提交邮箱后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`, + stalledMessage: 'Gửi email xong nhưng chờ quá lâu vẫn chưa vào trang mật khẩu hoặc trang mã xác minh đăng nhập.', + addPhoneMessage: (snapshot) => `Sau khi gửi email, trang nhảy thẳng sang màn hình nhập số điện thoại mà không đi qua trang mã xác minh đăng nhập. URL: ${snapshot.url}`, }); } @@ -5730,8 +5730,8 @@ async function waitForStep6PhoneSubmitTransition(phoneSubmittedAt, timeout = 120 allowPasswordAction: true, allowFinalPhoneAction: true, stalledReason: 'phone_submit_stalled', - stalledMessage: '提交手机号后长时间未进入密码页或手机验证码页。', - addPhoneMessage: (snapshot) => `提交手机号后页面直接进入手机号补全页面,未经过登录验证码页。URL: ${snapshot.url}`, + stalledMessage: 'Gửi SĐT xong nhưng chờ quá lâu vẫn chưa vào trang mật khẩu hoặc trang mã xác minh SĐT.', + addPhoneMessage: (snapshot) => `Sau khi gửi SĐT, trang nhảy thẳng sang màn hình bổ sung số điện thoại mà không đi qua trang mã xác minh đăng nhập. URL: ${snapshot.url}`, }); } @@ -5746,8 +5746,8 @@ async function waitForStep6PasswordSubmitTransition(passwordSubmittedAt, timeout timeoutRecoveryVia: 'password_submit_timeout_recovered', allowFinalSwitchAction: true, stalledReason: 'password_submit_stalled', - stalledMessage: '提交密码后仍未进入登录验证码页。', - addPhoneMessage: (snapshot) => `提交密码后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`, + stalledMessage: 'Gửi mật khẩu xong nhưng vẫn chưa vào trang mã xác minh đăng nhập.', + addPhoneMessage: (snapshot) => `Sau khi gửi mật khẩu, trang nhảy thẳng sang màn hình nhập số điện thoại mà không đi qua trang mã xác minh đăng nhập. URL: ${snapshot.url}`, }); } @@ -5758,11 +5758,11 @@ async function waitForStep6SwitchTransition(loginVerificationRequestedAt, timeou via: 'switch_to_one_time_code_login', oauthConsentVia: 'switch_to_one_time_code_oauth_consent', loginVerificationRequestedAt, - timeoutRecoveryMessage: '切换到一次性验证码登录后进入登录超时报错页。', + timeoutRecoveryMessage: 'Sau khi chuyển sang đăng nhập bằng mã dùng một lần, trang rơi vào trang lỗi timeout đăng nhập.', timeoutRecoveryVia: 'switch_to_one_time_code_timeout_recovered', stalledReason: 'one_time_code_switch_stalled', - stalledMessage: '点击一次性验证码登录后仍未进入登录验证码页。', - addPhoneMessage: (snapshot) => `切换到一次性验证码登录后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`, + stalledMessage: 'Sau khi bấm đăng nhập bằng mã dùng một lần, trang vẫn chưa vào trang mã xác minh đăng nhập.', + addPhoneMessage: (snapshot) => `Sau khi chuyển sang đăng nhập bằng mã dùng một lần, trang nhảy thẳng sang màn hình nhập số điện thoại mà không đi qua trang mã xác minh đăng nhập. URL: ${snapshot.url}`, }); if (transition.action === 'done' || transition.action === 'recoverable') { @@ -6294,7 +6294,7 @@ async function step6LoginFromPasswordPage(payload, snapshot) { }); } if (transition.action === 'recoverable') { - log(transition.result.message || `提交密码后仍未进入登录验证码页面,准备重新执行步骤 ${visibleStep}。`, 'warn', { step: visibleStep, stepKey: 'oauth-login' }); + log(transition.result.message || `Gửi mật khẩu xong nhưng vẫn chưa vào trang mã xác minh đăng nhập, sẽ chạy lại bước ${visibleStep}.`, 'warn', { step: visibleStep, stepKey: 'oauth-login' }); return transition.result; } if (transition.action === 'password') { @@ -6396,7 +6396,7 @@ async function step6_login(payload) { const snapshot = normalizeStep6Snapshot(await waitForKnownLoginAuthState(15000)); if (snapshot.state === 'verification_page' || snapshot.state === 'phone_verification_page') { - log('认证页已在登录验证码页,开始确认页面是否稳定。', 'info', { step: visibleStep, stepKey: 'oauth-login' }); + log('Trang xác thực đã ở sẵn trang mã xác minh đăng nhập, bắt đầu kiểm tra độ ổn định của trang.', 'info', { step: visibleStep, stepKey: 'oauth-login' }); return finalizeStep6VerificationReady({ visibleStep, loginVerificationRequestedAt: null, @@ -6490,7 +6490,7 @@ async function step6_login(payload) { } throwForStep6FatalState(snapshot, visibleStep); - throw new Error(`无法识别当前登录页面状态。URL: ${snapshot?.url || location.href}`); + throw new Error(`Không nhận diện được trạng thái trang đăng nhập hiện tại. URL: ${snapshot?.url || location.href}`); } async function waitForAddEmailPageReady(timeout = 15000) { @@ -7312,7 +7312,7 @@ async function waitForStep5SubmitOutcome(options = {}) { async function step5_fillNameBirthday(payload) { const { firstName, lastName, age, year, month, day, prefillOnly = false } = payload; - if (!firstName || !lastName) throw new Error('未提供姓名数据。'); + if (!firstName || !lastName) throw new Error('Chưa cung cấp họ tên.'); const performOperationWithDelay = typeof getOperationDelayRunner === 'function' ? getOperationDelayRunner() : async (metadata, operation) => { @@ -7324,7 +7324,7 @@ async function step5_fillNameBirthday(payload) { const resolvedAge = age ?? (year ? new Date().getFullYear() - Number(year) : null); const hasBirthdayData = [year, month, day].every(value => value != null && !Number.isNaN(Number(value))); if (!hasBirthdayData && (resolvedAge == null || Number.isNaN(Number(resolvedAge)))) { - throw new Error('未提供生日或年龄数据。'); + throw new Error('Chưa cung cấp dữ liệu ngày sinh hoặc tuổi.'); } const fullName = `${firstName} ${lastName}`; @@ -7549,7 +7549,7 @@ async function step5_fillNameBirthday(payload) { const completeBtn = await waitForStep5SubmitButton(5000) || await waitForElementByText('button', /完成|create|continue|finish|done|agree/i, 5000).catch(() => null); if (!completeBtn) { - throw new Error('未找到“完成帐户创建”按钮。URL: ' + location.href); + throw new Error('Không tìm thấy nút “Hoàn tất tạo tài khoản”. URL: ' + location.href); } const isAgeMode = !birthdayMode && Boolean(ageInput); diff --git a/content/sub2api-panel.js b/content/sub2api-panel.js index 6d3d25b..89f6e9c 100644 --- a/content/sub2api-panel.js +++ b/content/sub2api-panel.js @@ -24,7 +24,7 @@ if (document.documentElement.getAttribute(SUB2API_PANEL_LISTENER_SENTINEL) !== ' }).catch((err) => { if (isStopError(err)) { if (message.payload?.visibleStep || message.step) { - log('已被用户停止。', 'warn', { step: message.payload?.visibleStep || message.step }); + log('Đã bị người dùng dừng.', 'warn', { step: message.payload?.visibleStep || message.step }); } sendResponse({ stopped: true, error: err.message }); return; @@ -38,7 +38,7 @@ if (document.documentElement.getAttribute(SUB2API_PANEL_LISTENER_SENTINEL) !== ' } }); } else { - console.log('[MultiPage:sub2api-panel] 消息监听已存在,跳过重复注册'); + console.log('[MultiPage:sub2api-panel] Trình nghe tin nhắn đã tồn tại, bỏ qua đăng ký lặp lại'); } function getSub2ApiOrigin(payload = {}) { @@ -58,7 +58,7 @@ function normalizeRedirectUri() { parsed.pathname = '/auth/callback'; } if (parsed.pathname !== '/auth/callback') { - throw new Error('SUB2API 回调地址必须是 /auth/callback,例如 http://localhost:1455/auth/callback'); + throw new Error('Địa chỉ callback SUB2API phải là /auth/callback, ví dụ: http://localhost:1455/auth/callback'); } return parsed.toString(); } @@ -72,7 +72,7 @@ async function handleStep(step, payload = {}) { case 13: return step9_submitOpenAiCallback({ ...(payload || {}), visibleStep: step }); default: - throw new Error(`sub2api-panel.js 不处理步骤 ${step}`); + throw new Error(`sub2api-panel.js không xử lý bước ${step}`); } } @@ -82,7 +82,7 @@ async function handleNode(nodeId, payload = {}) { case 'platform-verify': return step9_submitOpenAiCallback(payload); default: - throw new Error(`sub2api-panel.js 不处理节点 ${normalizedNodeId}`); + throw new Error(`sub2api-panel.js không xử lý node ${normalizedNodeId}`); } } @@ -120,11 +120,11 @@ async function requestJson(origin, path, options = {}) { if (json.code === 0) { return json.data; } - throw new Error(json.message || json.detail || `请求失败(${path})`); + throw new Error(json.message || json.detail || `Yêu cầu thất bại (${path})`); } if (!response.ok) { - throw new Error((json && (json.message || json.detail)) || `请求失败(HTTP ${response.status}):${path}`); + throw new Error((json && (json.message || json.detail)) || `Yêu cầu thất bại (HTTP ${response.status}): ${path}`); } return json; @@ -132,7 +132,7 @@ async function requestJson(origin, path, options = {}) { function storeAuthSession(loginData) { if (!loginData?.access_token) { - throw new Error('SUB2API 登录返回缺少 access_token。'); + throw new Error('Đăng nhập SUB2API không trả về access_token.'); } localStorage.setItem('auth_token', loginData.access_token); @@ -156,13 +156,13 @@ async function loginSub2Api(payload = {}) { const origin = getSub2ApiOrigin(payload); if (!email) { - throw new Error('缺少 SUB2API 登录邮箱,请先在侧边栏填写。'); + throw new Error('Thiếu email đăng nhập SUB2API, vui lòng điền trước trong thanh bên.'); } if (!password) { - throw new Error('缺少 SUB2API 登录密码,请先在侧边栏填写。'); + throw new Error('Thiếu mật khẩu đăng nhập SUB2API, vui lòng điền trước trong thanh bên.'); } - log('步骤:正在登录 SUB2API 后台...'); + log('Đang đăng nhập vào bảng quản trị SUB2API...'); const loginData = await requestJson(origin, '/api/v1/auth/login', { method: 'POST', body: { @@ -195,7 +195,7 @@ async function getGroupByName(origin, token, groupName) { }); if (!group) { - throw new Error(`SUB2API 中未找到名为“${targetName}”的 openai 分组。`); + throw new Error(`Không tìm thấy nhóm openai tên “${targetName}” trong SUB2API.`); } return group; @@ -242,7 +242,7 @@ async function getGroupsByNames(origin, token, groupNames) { } if (missing.length) { - throw new Error(`SUB2API 中未找到以下 openai 分组:${missing.join('、')}。`); + throw new Error(`Không tìm thấy các nhóm openai sau trong SUB2API: ${missing.join(', ')}.`); } return matched; @@ -272,7 +272,7 @@ function resolveSub2ApiAccountPriority(payload = {}, backgroundState = {}) { } const numeric = Number(rawValue); if (!Number.isSafeInteger(numeric) || numeric < 1) { - throw new Error('SUB2API 账号优先级必须是大于等于 1 的整数。'); + throw new Error('Độ ưu tiên tài khoản SUB2API phải là số nguyên lớn hơn hoặc bằng 1.'); } return numeric; } @@ -378,7 +378,7 @@ async function resolveSub2ApiProxy(origin, token, preference = '') { token, }); if (!Array.isArray(proxies)) { - throw new Error('SUB2API 代理列表返回格式异常,无法自动选择代理。'); + throw new Error('Danh sách proxy SUB2API trả về định dạng bất thường, không thể tự chọn proxy.'); } const { proxy, reason, candidates } = findSub2ApiProxy(proxies, preference); @@ -403,7 +403,7 @@ async function resolveSub2ApiProxy(origin, token, preference = '') { if (reason === 'no-preference') { throw new Error(`SUB2API 存在多个可用代理,请在侧边栏填写默认代理名称或 ID;留空则不使用代理。可用代理:${available}`); } - throw new Error('SUB2API 没有可用代理;请检查默认代理配置,或将其留空以禁用代理。'); + throw new Error('SUB2API không có proxy khả dụng; hãy kiểm tra cấu hình proxy mặc định hoặc để trống để tắt proxy.'); } function buildDraftAccountName(groupName) { @@ -429,23 +429,23 @@ function parseLocalhostCallback(rawUrl, visibleStep = 10) { try { parsed = new URL(rawUrl); } catch { - throw new Error('提供的回调 URL 不是合法链接。'); + throw new Error('URL callback được cung cấp không hợp lệ.'); } if (!['http:', 'https:'].includes(parsed.protocol)) { - throw new Error('回调 URL 协议不正确。'); + throw new Error('Giao thức URL callback không đúng.'); } if (!['localhost', '127.0.0.1'].includes(parsed.hostname)) { throw new Error(`步骤 ${visibleStep} 只接受 localhost / 127.0.0.1 回调地址。`); } if (parsed.pathname !== '/auth/callback') { - throw new Error('回调 URL 路径必须是 /auth/callback。'); + throw new Error('Đường dẫn URL callback phải là /auth/callback.'); } const code = (parsed.searchParams.get('code') || '').trim(); const state = (parsed.searchParams.get('state') || '').trim(); if (!code || !state) { - throw new Error('回调 URL 中缺少 code 或 state。'); + throw new Error('URL callback thiếu code hoặc state.'); } return { diff --git a/content/vps-panel.js b/content/vps-panel.js index 4b4d341..2da2fbb 100644 --- a/content/vps-panel.js +++ b/content/vps-panel.js @@ -66,7 +66,7 @@ if (document.documentElement.getAttribute(VPS_PANEL_LISTENER_SENTINEL) !== '1') }); if (isStopError(err)) { if (message.payload?.visibleStep || message.step) { - log('已被用户停止。', 'warn', { step: message.payload?.visibleStep || message.step }); + log('Đã bị người dùng dừng.', 'warn', { step: message.payload?.visibleStep || message.step }); } sendResponse({ stopped: true, error: err.message }); return; @@ -80,7 +80,7 @@ if (document.documentElement.getAttribute(VPS_PANEL_LISTENER_SENTINEL) !== '1') } }); } else { - console.log('[MultiPage:vps-panel] 消息监听已存在,跳过重复注册'); + console.log('[MultiPage:vps-panel] Trình nghe tin nhắn đã tồn tại, bỏ qua đăng ký lặp lại'); } async function handleStep(step, payload) { @@ -91,7 +91,7 @@ async function handleStep(step, payload) { case 13: return await step9_vpsVerify({ ...(payload || {}), visibleStep: step }); default: - throw new Error(`vps-panel.js 不处理步骤 ${step}`); + throw new Error(`vps-panel.js không xử lý bước ${step}`); } } @@ -101,7 +101,7 @@ async function handleNode(nodeId, payload = {}) { case 'platform-verify': return await step9_vpsVerify(payload); default: - throw new Error(`vps-panel.js 不处理节点 ${normalizedNodeId}`); + throw new Error(`vps-panel.js không xử lý node ${normalizedNodeId}`); } } @@ -232,10 +232,10 @@ function getStatusBadgeEntries() { } function summarizeStatusBadgeEntries(entries) { - if (!entries.length) return '无可见状态徽标'; + if (!entries.length) return 'Không có huy hiệu trạng thái hiển thị'; return entries .map((entry, index) => { - const text = entry.text || '(空文本)'; + const text = entry.text || '(không có văn bản)'; const className = entry.className ? ` class=${getInlineTextSnippet(entry.className, 80)}` : ''; const errorVisual = entry.errorVisualSummary ? ` error=${getInlineTextSnippet(entry.errorVisualSummary, 80)}` : ''; return `#${index + 1}="${getInlineTextSnippet(text, 80)}"${className}${errorVisual}`; @@ -447,7 +447,7 @@ function getStep9PageErrorEntries() { return entries; } -function formatStep10StatusSummaryValue(text, emptyText = '无') { +function formatStep10StatusSummaryValue(text, emptyText = 'không có') { return text ? `"${getInlineTextSnippet(text, 80)}"` : emptyText; } @@ -459,8 +459,8 @@ function isStep10BrowserSwitchRequiredConflict(diagnostics = {}) { function getStep10BrowserSwitchRequiredMessage(diagnostics = {}) { const callbackFailureText = normalizeStep9StatusText(diagnostics?.callbackFailureText || ''); return [ - '检测到 CPA 页面同时显示“认证成功”和“回调 URL 提交失败: 请更新CLI Proxy API或检查连接”。', - '这通常不是浏览器问题,而是 CPA 项目会清理多线程 OAuth 会话。CPA 项目无法使用多线程,请修改 CPA 服务器或改为单线程注册。', + 'Đã phát hiện trang CPA đồng thời hiển thị “Xác thực thành công” và “Gửi callback URL thất bại: vui lòng cập nhật CLI Proxy API hoặc kiểm tra kết nối”.', + 'Thường đây không phải vấn đề của trình duyệt, mà là dự án CPA sẽ dọn dẹp các phiên OAuth đa luồng. Dự án CPA không thể dùng đa luồng, hãy sửa máy chủ CPA hoặc chuyển sang đăng ký đơn luồng.', callbackFailureText ? `面板原文:${callbackFailureText}` : '', ].filter(Boolean).join(' '); } diff --git a/docs/plans/2026-06-07-vietnamese-i18n-refactor.md b/docs/plans/2026-06-07-vietnamese-i18n-refactor.md new file mode 100644 index 0000000..a1fbc8b --- /dev/null +++ b/docs/plans/2026-06-07-vietnamese-i18n-refactor.md @@ -0,0 +1,606 @@ +# Kế hoạch Việt hoá mã nguồn GuJumpgate + +> **Cho Hermes/dev:** triển khai theo hướng i18n tối thiểu, tránh sửa tay toàn bộ text một lần rồi khó merge upstream. + +**Mục tiêu:** chuyển UI và message runtime chính của GuJumpgate sang tiếng Việt, đồng thời tạo kiến trúc i18n đủ sạch để sau này giữ được đa ngôn ngữ `zh-CN / vi-VN / en-US`. + +**Kiến trúc:** thêm một lớp i18n mỏng dùng key-value dictionary, ưu tiên áp dụng trước ở `sidepanel`, sau đó mới lan sang `background` và `content scripts`. Không đổi logic nghiệp vụ; chỉ tách text khỏi code và thêm hàm `t(key, params?)` để render chuỗi. + +**Tech stack:** Chrome Extension Manifest V3, plain JavaScript, sidepanel HTML/CSS/JS, background service worker, content scripts. + +--- + +## 1. Kết quả mong muốn + +Sau khi hoàn tất pha đầu: + +1. Mở sidepanel thấy tiếng Việt ở các khu vực chính: + - header + - nút hành động + - menu cấu hình + - nhãn form phổ biến + - trạng thái cơ bản +2. Toast/log/status chính hiển thị tiếng Việt. +3. Code không còn hardcode text mới trong `sidepanel/sidepanel.html` và các vùng sidepanel chính của `sidepanel/sidepanel.js`. +4. Có hạ tầng i18n để tiếp tục migrate các file lớn như: + - `background.js` + - `background/steps/*.js` + - `content/signup-page.js` + - `content/plus-checkout.js` + - `content/vps-panel.js` + +--- + +## 2. Hiện trạng repo + +### Các file đang chứa nhiều text cứng + +- `sidepanel/sidepanel.html` +- `sidepanel/sidepanel.js` +- `background.js` +- `content/signup-page.js` +- `content/plus-checkout.js` +- `content/vps-panel.js` +- `background/phone-verification-flow.js` +- `background/steps/create-plus-checkout.js` +- `background/steps/fill-plus-checkout.js` +- nhiều manager file dưới `sidepanel/` + +### Dấu hiệu hiện trạng + +- UI hiện tại chủ yếu là tiếng Trung. +- Chuỗi hiển thị đang nằm lẫn trong: + - HTML text nodes + - `title` + - `aria-label` + - `placeholder` + - `textContent = ...` + - `innerHTML = ...` + - `throw new Error(...)` + - `message: '...'` + - `addLog(...)` +- Repo chưa có thư mục/kiến trúc i18n chuẩn. + +--- + +## 3. Nguyên tắc thiết kế lại + +### 3.1. Không sửa tay một đợt toàn repo + +Nếu sửa trực tiếp toàn bộ chuỗi tiếng Trung sang tiếng Việt trong các file lớn: +- diff sẽ cực lớn +- khó review +- khó merge upstream +- dễ sót text runtime + +=> Thay vào đó, phải **tạo lớp i18n trước**, rồi migrate dần. + +### 3.2. Chia làm 3 lớp text + +#### Lớp A — UI tĩnh +Ví dụ: +- nút +- tiêu đề +- nhãn form +- placeholder +- menu item + +#### Lớp B — UI động / trạng thái +Ví dụ: +- toast +- status badge +- auto-run status +- summary text +- confirmation text + +#### Lớp C — runtime / nghiệp vụ +Ví dụ: +- log text +- error message +- callback status +- verification message +- warning/fallback message + +### 3.3. Ưu tiên `sidepanel` trước + +Lý do: +- người dùng nhìn thấy ngay +- rủi ro thấp hơn content/background +- ít ảnh hưởng logic automation hơn + +### 3.4. Không phá backward compatibility + +Bản refactor i18n không được làm thay đổi: +- key cấu hình cũ trong storage +- message protocol giữa background và content +- step IDs / flow IDs / runtime state shape + +--- + +## 4. Kiến trúc i18n đề xuất + +## 4.1. File mới + +### Tạo: `shared/i18n.js` + +Trách nhiệm: +- giữ locale hiện tại +- nạp dictionary +- export hàm `t(key, params?)` +- export helper áp text vào DOM + +### Tạo: `locales/vi-VN.js` +### Tạo: `locales/zh-CN.js` + +Trách nhiệm: +- mỗi file export object dictionary phẳng hoặc nested + +Ví dụ dạng phẳng: + +```js +window.GuJumpgateLocaleViVN = { + 'app.title': 'GuJumpgate', + 'header.guide': 'Hướng dẫn', + 'header.auto': 'Tự động', + 'header.stop': 'Dừng', + 'header.config': 'Cấu hình', + 'config.export': 'Xuất cấu hình', + 'config.import': 'Nhập cấu hình', + 'status.waiting_callback': 'Đang chờ callback', + 'status.oauth_url_not_generated': 'Chưa tạo liên kết đăng nhập', +}; +``` + +### Sửa: `manifest.json` + +Thêm các file locale/i18n vào trước các script cần dùng, ví dụ với sidepanel: +- `locales/zh-CN.js` +- `locales/vi-VN.js` +- `shared/i18n.js` +- rồi mới tới `sidepanel/*.js` + +Nếu background/content cũng cần dùng chung, cần bảo đảm các script này được load trước chúng. + +--- + +## 4.2. API i18n tối thiểu + +Trong `shared/i18n.js` triển khai các API sau: + +```js +(function attachI18n(root) { + const dictionaries = { + 'zh-CN': root.GuJumpgateLocaleZhCN || {}, + 'vi-VN': root.GuJumpgateLocaleViVN || {}, + }; + + const DEFAULT_LOCALE = 'vi-VN'; + let currentLocale = DEFAULT_LOCALE; + + function interpolate(template, params = {}) { + return String(template).replace(/\{(\w+)\}/g, (_, key) => { + return Object.prototype.hasOwnProperty.call(params, key) ? String(params[key]) : `{${key}}`; + }); + } + + function t(key, params = {}, fallback = '') { + const dict = dictionaries[currentLocale] || {}; + const fallbackDict = dictionaries['zh-CN'] || {}; + const raw = dict[key] ?? fallbackDict[key] ?? fallback || key; + return interpolate(raw, params); + } + + function setLocale(locale) { + if (dictionaries[locale]) currentLocale = locale; + } + + function getLocale() { + return currentLocale; + } + + function applyI18n(rootEl = document) { + rootEl.querySelectorAll('[data-i18n]').forEach((el) => { + el.textContent = t(el.dataset.i18n); + }); + rootEl.querySelectorAll('[data-i18n-title]').forEach((el) => { + el.title = t(el.dataset.i18nTitle); + }); + rootEl.querySelectorAll('[data-i18n-placeholder]').forEach((el) => { + el.placeholder = t(el.dataset.i18nPlaceholder); + }); + rootEl.querySelectorAll('[data-i18n-aria-label]').forEach((el) => { + el.setAttribute('aria-label', t(el.dataset.i18nAriaLabel)); + }); + } + + root.GuJumpgateI18n = { t, setLocale, getLocale, applyI18n }; +})(typeof self !== 'undefined' ? self : globalThis); +``` + +--- + +## 4.3. Cách chọn locale + +Pha đầu dùng mặc định: +- `vi-VN` + +Pha sau có thể thêm setting storage, ví dụ: +- `uiLocale: 'vi-VN' | 'zh-CN' | 'en-US'` + +Không nên làm ngay ở bước đầu nếu chưa cần, để tránh scope creep. + +--- + +## 5. Chiến lược migrate theo pha + +## Pha 1 — Việt hoá sidepanel nhìn thấy ngay + +### Mục tiêu +Khi mở extension, người dùng nhìn thấy tiếng Việt ở phần lớn vùng giao diện chính. + +### File cần sửa +- Tạo: `shared/i18n.js` +- Tạo: `locales/vi-VN.js` +- Tạo: `locales/zh-CN.js` +- Sửa: `manifest.json` +- Sửa: `sidepanel/sidepanel.html` +- Sửa: `sidepanel/sidepanel.js` +- Có thể sửa thêm các manager sidepanel nếu cần: + - `sidepanel/hotmail-manager.js` + - `sidepanel/mail-2925-manager.js` + - `sidepanel/icloud-manager.js` + - `sidepanel/luckmail-manager.js` + - `sidepanel/custom-email-pool-manager.js` + - `sidepanel/hosted-sms-pool-manager.js` + - `sidepanel/paypal-manager.js` + +### Việc cụ thể + +#### Bước 1 +Tạo `locales/vi-VN.js` với các key dùng cho: +- header +- nút chính +- menu config +- section label phổ biến +- các chuỗi sidepanel rất hay gặp + +#### Bước 2 +Tạo `locales/zh-CN.js` làm bản đối chiếu/fallback. + +#### Bước 3 +Tạo `shared/i18n.js` và expose `window.GuJumpgateI18n`. + +#### Bước 4 +Sửa `sidepanel/sidepanel.html`: +- thay text cứng bằng `data-i18n` +- thay `title` bằng `data-i18n-title` +- thay `placeholder` bằng `data-i18n-placeholder` +- thay `aria-label` bằng `data-i18n-aria-label` + +Ví dụ: + +```html + +``` + +#### Bước 5 +Trong `sidepanel/sidepanel.js`, khi init panel: + +```js +const { t, applyI18n } = window.GuJumpgateI18n; +applyI18n(document); +``` + +#### Bước 6 +Thay các chuỗi runtime UI ở `sidepanel/sidepanel.js` bằng `t(...)`. + +Ví dụ: + +```js +btnAutoRun.innerHTML = ` ${t('header.auto')}`; +``` + +hoặc với text-only: + +```js +contributionModeSummary.textContent = t('contribution.waiting_to_start'); +``` + +#### Bước 7 +Các chuỗi có biến dùng param interpolation: + +```js +t('auto.status.running_with_count', { count: runCount }) +``` + +Dictionary: + +```js +'auto.status.running_with_count': 'Đang chạy ({count})' +``` + +--- + +## Pha 2 — Việt hoá manager sidepanel và message UI phụ + +### Mục tiêu +Dọn sạch các chuỗi tiếng Trung còn hiện ra trong panel phụ. + +### File ưu tiên +- `sidepanel/hotmail-manager.js` +- `sidepanel/mail-2925-manager.js` +- `sidepanel/icloud-manager.js` +- `sidepanel/luckmail-manager.js` +- `sidepanel/account-records-manager.js` +- `sidepanel/hosted-sms-pool-manager.js` +- `sidepanel/custom-email-pool-manager.js` +- `sidepanel/contribution-mode.js` + +### Việc cụ thể +- thay empty state text +- thay filter/no-match text +- thay action button labels +- thay overlay titles/stats labels + +### Lưu ý +Các đoạn `innerHTML` cần: +- giữ `escapeHtml` nếu đang dùng +- chỉ dùng `t(...)` cho phần text, không nhét HTML động nguy hiểm vào dictionary + +--- + +## Pha 3 — Việt hoá background và content script message + +### Mục tiêu +- lỗi/log/toast/message chính sang tiếng Việt +- vẫn giữ nguyên protocol và logic automation + +### File ưu tiên +- `background.js` +- `background/message-router.js` +- `background/verification-flow.js` +- `background/phone-verification-flow.js` +- `background/steps/create-plus-checkout.js` +- `background/steps/fill-plus-checkout.js` +- `background/steps/fetch-login-code.js` +- `content/signup-page.js` +- `content/plus-checkout.js` +- `content/vps-panel.js` +- `content/phone-auth.js` +- `content/paypal-flow.js` + +### Chiến lược +Không migrate mọi text một lượt. Chia theo nhóm: + +1. error message user-facing +2. status message +3. log/debug message +4. fallback text/diagnostic text + +### Quy tắc +- text người dùng thấy được → đưa vào i18n +- text debug nội bộ ít thấy → có thể giữ lại sau cùng +- message protocol key/enum → **không dịch** + +Ví dụ: +- `STEP_COMPLETE` giữ nguyên +- `error: 'email_invalid'` nếu là machine code thì giữ nguyên +- nhưng phần mô tả hiển thị cho người dùng thì dịch qua `t(...)` + +--- + +## 6. Quy ước key i18n + +Dùng namespace rõ ràng, ví dụ: + +- `app.*` +- `header.*` +- `config.*` +- `common.*` +- `form.*` +- `auto.*` +- `status.*` +- `error.*` +- `contribution.*` +- `paypal.*` +- `mail.*` +- `phone.*` +- `records.*` + +Ví dụ: + +```js +'common.save': 'Lưu' +'common.cancel': 'Huỷ' +'header.auto': 'Tự động' +'header.stop': 'Dừng' +'status.waiting_callback': 'Đang chờ callback' +'error.invalid_api_url': 'URL API không hợp lệ' +``` + +Không dùng key kiểu mơ hồ như: +- `text1` +- `msg2` +- `buttonA` + +--- + +## 7. Những gì không được dịch + +Không đổi các giá trị nghiệp vụ/enum/protocol như: +- `pending` +- `running` +- `completed` +- `failed` +- `stopped` +- `manual_completed` +- `skipped` +- message types giữa background/content +- storage keys hiện có +- flow IDs +- node IDs +- provider IDs + +Chỉ map chúng sang label hiển thị nếu cần. + +Ví dụ: + +```js +const STATUS_LABELS = { + pending: t('status.pending'), + running: t('status.running'), + completed: t('status.completed'), +}; +``` + +--- + +## 8. Checklist review cho mỗi PR i18n + +### Functional +- [ ] UI vẫn mở bình thường +- [ ] Không lỗi `GuJumpgateI18n is undefined` +- [ ] Không gãy thứ tự script load +- [ ] Không làm hỏng click handler / DOM selector + +### UX +- [ ] Không còn text tiếng Trung ở vùng đã migrate +- [ ] `title`, `placeholder`, `aria-label` đã được Việt hoá +- [ ] Text có dấu tiếng Việt hiển thị đúng + +### Safety +- [ ] Không đưa HTML động không escape vào dictionary +- [ ] Không đổi machine-readable error code +- [ ] Không đổi protocol message type + +### Maintainability +- [ ] Text mới không hardcode trực tiếp trong file đã migrate +- [ ] Key i18n có namespace rõ ràng +- [ ] Không copy-paste dictionary trùng lặp + +--- + +## 9. Thứ tự triển khai khuyến nghị + +### PR 1 +- thêm `locales/vi-VN.js` +- thêm `locales/zh-CN.js` +- thêm `shared/i18n.js` +- wiring vào `manifest.json` + +### PR 2 +- migrate `sidepanel/sidepanel.html` +- migrate phần header + config + action bar của `sidepanel/sidepanel.js` + +### PR 3 +- migrate form labels/chuỗi sidepanel chính +- migrate toast/status chính + +### PR 4 +- migrate `sidepanel/*manager.js` + +### PR 5+ +- migrate background/content theo domain: + - auth/phone + - plus-checkout + - CPA/SUB2API + - mail providers + +--- + +## 10. Gợi ý kiểm thử thủ công + +### Kiểm thử cơ bản +1. Load extension ở `chrome://extensions` +2. Mở sidepanel +3. Kiểm tra: + - header + - nút Auto/Stop/Reset/Config + - menu export/import + - section labels + - placeholder inputs +4. Trigger vài toast/status để xem text tiếng Việt hiển thị đúng. + +### Kiểm thử không hồi quy +1. Save config +2. Export/import config +3. Open account records overlay +4. Toggle Plus mode +5. Toggle contribution mode +6. Kiểm tra các manager list empty state/no-match state + +### Kiểm thử kỹ thuật +Nếu có Node trong máy: + +```bash +node --check sidepanel/sidepanel.js +node --check shared/i18n.js +node --check locales/vi-VN.js +node --check locales/zh-CN.js +``` + +Nếu sửa nhiều file JS: + +```bash +git ls-files '*.js' | xargs -n 1 node --check +``` + +--- + +## 11. Phạm vi bản đầu tiên nên chốt + +### Nên làm ngay +- sidepanel chính +- text nhìn thấy ngay +- toast/status cơ bản +- kiến trúc i18n nền + +### Chưa cần làm ngay +- toàn bộ log diagnostic sâu +- toàn bộ content script hiếm gặp +- setting chọn locale nhiều ngôn ngữ +- tự động extract text từ source + +--- + +## 12. Định nghĩa thành công + +Bản refactor được coi là thành công khi: + +1. sidepanel mở lên chủ yếu là tiếng Việt; +2. code mới không hardcode text trực tiếp ở vùng đã migrate; +3. không làm hỏng flow hiện tại; +4. có thể tiếp tục migrate background/content theo từng PR nhỏ. + +--- + +## 13. Handoff cho bước thực thi + +Nếu bắt đầu triển khai, nên đi theo đúng thứ tự sau: + +1. dựng `shared/i18n.js` +2. dựng `locales/vi-VN.js` và `locales/zh-CN.js` +3. nối script load trong `manifest.json` +4. migrate `sidepanel/sidepanel.html` +5. migrate `sidepanel/sidepanel.js` phần header/action/config +6. test thủ công sidepanel +7. tiếp tục các manager và runtime text còn lại + +--- + +## 14. Quyết định thiết kế chốt + +**Quyết định:** chọn hướng **i18n mỏng + migrate dần**, không chọn hướng “find/replace toàn repo sang tiếng Việt”. + +**Lý do:** +- ít rủi ro hơn +- review dễ hơn +- giữ khả năng merge upstream +- mở đường cho đa ngôn ngữ thật sự +- phù hợp với repo đang có nhiều file lớn và nhiều luồng automation diff --git a/docs/reverse-skill-contributor-quickstart.md b/docs/reverse-skill-contributor-quickstart.md new file mode 100644 index 0000000..6628d56 --- /dev/null +++ b/docs/reverse-skill-contributor-quickstart.md @@ -0,0 +1,72 @@ +# Hướng dẫn nhanh cho contributor: Reverse-Skill tiếng Việt + +Dành cho contributor mới vào repo này: nếu bạn muốn bật nhanh lớp route `reverse-skill` tiếng Việt trong workspace hiện tại, chỉ cần làm theo các bước dưới đây. + +## Mục tiêu + +- Cung cấp cho agent / Codex một router tiếng Việt có thể đọc ngay tại local +- Ưu tiên route các tác vụ CTF / reverse / web security vào đúng skill family hẹp nhất +- Giữ tích hợp ở dạng **local-only**, không sửa luồng chạy chính của extension + +## Điều kiện đầu vào + +Tốt nhất máy của bạn đã có sẵn các nguồn skill local sau: + +- `~/.hermes-shop/skills/reverse-skill` +- `~/.hermes-shop/skills/ctf-sandbox-orchestrator` + +Nếu thiếu một hoặc cả hai thư mục trên, script vẫn chạy bình thường; nó chỉ ghi các nguồn còn thiếu vào `MANIFEST.json`. + +## Một lệnh để bật + +```bash +python reverse_skill_proxy.py --bundle-dir .reverse-skill-proxy --workspace-root . +``` + +## Kết quả sẽ được tạo ra + +Sau khi chạy xong, bạn sẽ thấy: + +- `.reverse-skill-proxy/ROUTER.vi.md` +- `.reverse-skill-proxy/MANIFEST.json` +- `AGENTS.md` + +## Cách sử dụng + +1. Mở `AGENTS.md` +2. Khi gặp tác vụ như CTF / reverse / pwn / web API security / LLM security / Windows AD, đọc `.reverse-skill-proxy/ROUTER.vi.md` trước +3. Chọn skill family hẹp nhất theo router +4. Nếu chưa rõ challenge thuộc loại nào, bắt đầu bằng `ctf-sandbox-orchestrator` + +## Các trường hợp fallback thường gặp + +### `MANIFEST.json` có `missing_source_roots` + +Điều này có nghĩa là máy hiện tại chưa có đầy đủ local skill sources. Đây **không phải lỗi nghiêm trọng**. + +Bạn vẫn có thể tiếp tục theo một trong hai cách: + +- dùng router như lớp dẫn đường tối thiểu +- hoặc fallback về workflow mặc định của agent + +### Không muốn commit file sinh ra vào Git + +Repo đã ignore sẵn: + +- `/.reverse-skill-proxy/` + +Vì vậy bình thường bạn chỉ cần commit: + +- `reverse_skill_proxy.py` +- `AGENTS.md` +- các file tài liệu được cập nhật + +## Khuyến nghị sử dụng + +- Luôn bắt đầu bằng kiểm tra thụ động tối thiểu trước khi route +- Chỉ nạp skill family liên quan nhất, đừng đọc dồn toàn bộ +- Khi ghi nhận kết quả, ưu tiên format ngắn: + - kết quả + - bằng chứng + - xác minh + - bước tiếp theo diff --git a/docs/reverse-skill-vi-integration.md b/docs/reverse-skill-vi-integration.md new file mode 100644 index 0000000..372d67a --- /dev/null +++ b/docs/reverse-skill-vi-integration.md @@ -0,0 +1,79 @@ +# Reverse-Skill Proxy Integration (Tiếng Việt) + +Tài liệu này thêm một lớp **proxy tối thiểu** để repo này có thể mang theo bundle `reverse-skill` tiếng Việt / CTF mà không làm thay đổi luồng chạy chính của extension. + +## Mục tiêu + +- Không đụng vào luồng extension hiện có nếu không cần. +- Chỉ thêm một bundle phụ trợ để Codex / agent có thể đọc khi làm CTF, reverse, web security hoặc các bài nhiều bước. +- Khi thiếu skill local, hệ thống vẫn **fallback an toàn** về workflow mặc định, không chặn tác vụ chính. + +## Thành phần + +- `reverse_skill_proxy.py` + - đồng bộ local skill bundle từ: + - `~/.hermes-shop/skills/reverse-skill` + - `~/.hermes-shop/skills/ctf-sandbox-orchestrator` + - sinh ra: + - `ROUTER.vi.md` + - `MANIFEST.json` + - `AGENTS.md` tùy chọn cho workspace hiện tại + +- `AGENTS.md` + - contract nhẹ để Codex / agent biết khi nào nên đọc router tiếng Việt trước + +## Cách dùng + +### 1) Tạo bundle cục bộ trong repo hiện tại + +```bash +python reverse_skill_proxy.py --bundle-dir .reverse-skill-proxy --workspace-root . +``` + +Sau khi chạy xong sẽ có: + +- `.reverse-skill-proxy/ROUTER.vi.md` +- `.reverse-skill-proxy/MANIFEST.json` +- `./AGENTS.md` + +### 2) Dùng cùng agent/Codex + +Khi mở workspace có `AGENTS.md`, agent có thể đọc router tiếng Việt trước khi xử lý các bài như: + +- CTF tổng quát +- reverse / pwn +- web / API security +- prompt injection / LLM security +- mobile reverse +- Windows / AD pivot +- cloud / container drift + +### 3) Fallback-friendly + +Nếu máy không có local skills ở `~/.hermes-shop/skills/...`: + +- script vẫn tạo router + manifest +- `MANIFEST.json` sẽ ghi các nguồn bị thiếu +- workflow còn lại vẫn dùng được + +## Khi nào nên route sang skill nào + +- Chưa rõ đề bài: `ctf-sandbox-orchestrator` +- Web/API: `competition-web-runtime` +- Reverse/Pwn: `competition-reverse-pwn` +- Agent/LLM/Cloud: `competition-agent-cloud` +- Windows/AD: `competition-identity-windows` +- Reverse tổng quát/mobile: `reverse-engineering` +- API auth/boundary: `api-security` +- Nhiều bước / pivot chain: `attack-chain` +- LLM security / RAG poisoning: `llm-security` + +## Gợi ý vận hành + +- Luôn bắt đầu bằng **kiểm tra thụ động tối thiểu**. +- Chỉ route sang family hẹp nhất phù hợp. +- Ghi nhận ngắn gọn theo format: + - kết quả + - bằng chứng + - xác minh + - bước kế tiếp diff --git a/locales/vi-VN.js b/locales/vi-VN.js new file mode 100644 index 0000000..703c4c6 --- /dev/null +++ b/locales/vi-VN.js @@ -0,0 +1,100 @@ +window.GUJUMPGATE_LOCALES = window.GUJUMPGATE_LOCALES || {}; +window.GUJUMPGATE_LOCALES['vi-VN'] = { + document: { + title: 'GuJumpgate V0.1.9', + }, + header: { + repoHome: 'Mở kho GitHub', + releases: 'Mở trang GitHub Releases', + releaseLog: 'Nhật ký cập nhật', + guide: 'Hướng dẫn sử dụng', + runCount: 'Số lần chạy', + autoRun: 'Tự động', + stop: 'Dừng', + reset: 'Đặt lại toàn bộ bước', + theme: 'Chuyển giao diện', + config: 'Cấu hình', + exportSettings: 'Xuất cấu hình', + importSettings: 'Nhập cấu hình', + contributionHint: 'Hướng dẫn sử dụng đã có cập nhật, bấm nút phía trên để xem.', + dismissContributionHint: 'Đóng thông báo cập nhật', + }, + updates: { + label: 'Nội dung cập nhật', + ignore: 'Bỏ qua bản cập nhật này', + open: 'Xem bản cập nhật', + reminder: 'Hãy xuất cấu hình trước khi cập nhật', + }, + settings: { + register: 'Đăng ký', + exportTo: 'Xuất tới', + localCpaJson: 'CPA JSON cục bộ', + cpaPanel: 'Bảng CPA', + accountAccessStrategy: 'Chiến lược truy cập tài khoản', + accessSmsOauth: 'Đăng ký bằng số điện thoại trước rồi OAuth', + accessPhoneBindOauth: 'Liên kết số điện thoại sau rồi OAuth', + accessSessionJson: 'Nhập SESSION JSON', + pluginDir: 'Thư mục tiện ích', + pluginDirPlaceholder: 'Thư mục chứa extension sau khi giải nén', + advanced: 'Nâng cao', + pluginDirNote: 'Mặc định sẽ ghi vào .cli-proxy-api trong thư mục tiện ích', + expandAuthDir: 'Mở rộng thư mục xác thực', + authDir: 'Thư mục xác thực', + authDirPlaceholder: '.cli-proxy-api', + contributionMode: 'Chế độ đóng góp', + contributionCopy: 'Tài khoản hiện tại sẽ được dùng để hỗ trợ duy trì dự án. Extension sẽ tự xin địa chỉ đăng nhập đóng góp và theo dõi trạng thái ủy quyền; nếu phát hiện callback thì sẽ tự gửi, không cần sao chép thủ công.', + contributionNickname: 'Biệt danh đóng góp', + contributionNicknamePlaceholder: 'Có thể để trống, sẽ hiển thị là người đóng góp ẩn danh', + qqPlaceholder: 'Có thể để trống, chỉ nhập số', + oauthPending: 'Chưa tạo địa chỉ đăng nhập', + callback: 'Callback', + callbackWaiting: 'Đang chờ callback', + contributionWaiting: 'Đang chờ bắt đầu đóng góp', + startContribution: 'Bắt đầu đóng góp', + openContributionUpload: 'Đã có file xác thực? Tự xử lý', + exitContributionMode: 'Thoát chế độ đóng góp', + cpaAddressShow: 'Hiện địa chỉ CPA', + adminKey: 'Khóa quản trị', + adminKeyPlaceholder: 'Nhập khóa quản trị CPA', + showAdminKey: 'Hiện khóa quản trị', + callbackMode: 'Cách callback', + callbackSubmit: 'Triển khai máy chủ', + callbackBypass: 'Triển khai cục bộ', + account: 'Tài khoản', + password: 'Mật khẩu', + add: 'Thêm', + priority: 'Ưu tiên', + defaultProxyPlaceholder: 'Để trống nếu không dùng proxy; hoặc nhập tên / ID proxy', + }, + modals: { + sharedFormTitle: 'Thêm tài khoản', + close: 'Đóng', + cancel: 'Hủy', + confirm: 'Xác nhận', + autoStartTitle: 'Khởi động tự động', + autoStartMessage: 'Đã phát hiện tiến độ quy trình hiện có, hãy chọn tiếp tục hay bắt đầu lại.', + dontAskAgain: 'Không hỏi lại', + restart: 'Bắt đầu lại', + continueCurrent: 'Tiếp tục hiện tại', + resetFlowTitle: 'Đặt lại quy trình', + resetFlowMessage: 'Xác nhận đặt lại toàn bộ bước và dữ liệu?', + resetFlowConfirm: 'Xác nhận đặt lại', + newUserGuideTitle: 'Hướng dẫn cho người mới', + newUserGuideMessage: 'Nếu đây là lần đầu bạn dùng, hãy đọc hướng dẫn trong kho trước. Bấm “Xem hướng dẫn” sẽ mở trang mô tả dự án.', + newUserGuideAlert: 'Thông báo này chỉ xuất hiện một lần.', + viewGuide: 'Xem hướng dẫn', + }, + runtime: { + autoRunPlanned: 'Đã lên lịch{runLabel}', + autoRunWaiting: 'Đang chờ{runLabel}', + autoRunPaused: 'Đã tạm dừng{runLabel}', + autoRunRunning: 'Đang chạy{runLabel}', + autoRunRetrying: 'Đang thử lại{runLabel}', + autoRunScheduling: 'Đang lập lịch...', + autoRunStarting: 'Đang chạy...', + skipNode: 'Bỏ qua nút này', + skipNodeAria: 'Bỏ qua nút {nodeId}', + contributionUpdateTutorial: 'Thông báo / hướng dẫn sử dụng đã có cập nhật, bấm nút “Đóng góp / Hướng dẫn” phía trên để xem.', + contributionUpdateSurvey: 'Có khảo sát mới, mời mọi người cùng tham gia lựa chọn.', + }, +}; diff --git a/locales/zh-CN.js b/locales/zh-CN.js new file mode 100644 index 0000000..653ae22 --- /dev/null +++ b/locales/zh-CN.js @@ -0,0 +1,100 @@ +window.GUJUMPGATE_LOCALES = window.GUJUMPGATE_LOCALES || {}; +window.GUJUMPGATE_LOCALES['zh-CN'] = { + document: { + title: 'GuJumpgate V0.1.9', + }, + header: { + repoHome: '打开 GitHub 仓库', + releases: '打开 GitHub Releases 页面', + releaseLog: '更新日志', + guide: '使用说明', + runCount: '运行次数', + autoRun: '自动', + stop: '停止', + reset: '重置全部步骤', + theme: '切换主题', + config: '配置', + exportSettings: '导出配置', + importSettings: '导入配置', + contributionHint: '使用说明有更新了,可点上方“使用说明”查看。', + dismissContributionHint: '关闭更新提示', + }, + updates: { + label: '更新内容', + ignore: '忽略本次更新', + open: '前往更新', + reminder: '一定请先导出配置,再执行更新', + }, + settings: { + register: '注册', + exportTo: '导出至', + localCpaJson: '本地CPA JSON', + cpaPanel: 'CPA 面板', + accountAccessStrategy: '账号接入策略', + accessSmsOauth: '先手机号注册 Oauth', + accessPhoneBindOauth: '后手机号绑定 Oauth', + accessSessionJson: 'SESSION JSON导入', + pluginDir: '插件目录', + pluginDirPlaceholder: '解压后浏览器插件所在的目录', + advanced: '高级', + pluginDirNote: '默认会写入插件目录下的 .cli-proxy-api', + expandAuthDir: '展开认证目录', + authDir: '认证目录', + authDirPlaceholder: '.cli-proxy-api', + contributionMode: '贡献模式', + contributionCopy: '当前账号将用于支持项目维护。扩展会自动申请贡献登录地址并持续跟踪授权状态;如检测到回调地址,会自动提交,无需手动复制。', + contributionNickname: '贡献昵称', + contributionNicknamePlaceholder: '可留空,将显示为匿名贡献者', + qqPlaceholder: '可留空,只能填写数字', + oauthPending: '未生成登录地址', + callback: '回调', + callbackWaiting: '等待回调', + contributionWaiting: '等待开始贡献', + startContribution: '开始贡献', + openContributionUpload: '已有认证文件?自行处理', + exitContributionMode: '退出贡献模式', + cpaAddressShow: '显示 CPA 地址', + adminKey: '管理密钥', + adminKeyPlaceholder: '请输入 CPA 管理密钥', + showAdminKey: '显示管理密钥', + callbackMode: '回调方式', + callbackSubmit: '服务器部署', + callbackBypass: '本地部署', + account: '账号', + password: '密码', + add: '添加', + priority: '优先级', + defaultProxyPlaceholder: '留空则不使用代理;或填写代理名称 / ID', + }, + modals: { + sharedFormTitle: '添加账号', + close: '关闭', + cancel: '取消', + confirm: '确认', + autoStartTitle: '启动自动', + autoStartMessage: '检测到已有流程进度,选择继续当前还是重新开始。', + dontAskAgain: '不再提示', + restart: '重新开始', + continueCurrent: '继续当前', + resetFlowTitle: '重置流程', + resetFlowMessage: '确认重置全部步骤和数据吗?', + resetFlowConfirm: '确认重置', + newUserGuideTitle: '新手引导', + newUserGuideMessage: '如果你是第一次使用,可以先阅读仓库里的使用说明。点击“查看说明”会打开项目说明页。', + newUserGuideAlert: '本提示仅出现一次。', + viewGuide: '查看说明', + }, + runtime: { + autoRunPlanned: '已计划{runLabel}', + autoRunWaiting: '等待中{runLabel}', + autoRunPaused: '已暂停{runLabel}', + autoRunRunning: '运行中{runLabel}', + autoRunRetrying: '重试中{runLabel}', + autoRunScheduling: '计划中...', + autoRunStarting: '运行中...', + skipNode: '跳过此节点', + skipNodeAria: '跳过节点 {nodeId}', + contributionUpdateTutorial: '公告 / 使用教程有更新了,可点上方“贡献/使用”查看。', + contributionUpdateSurvey: '有新的征求意见,请佬友共同参与选择。', + }, +}; diff --git a/manifest.json b/manifest.json index e5c005a..efe4759 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,7 @@ "name": "GuJumpgate", "version": "0.1.9", "version_name": "GuJumpgate V0.1.9", - "description": "用于自动执行多步骤 OAuth 注册流程", + "description": "Dùng để tự động thực hiện quy trình đăng ký OAuth nhiều bước", "permissions": [ "sidePanel", "alarms", diff --git a/reverse_skill_proxy.py b/reverse_skill_proxy.py new file mode 100644 index 0000000..87d8a94 --- /dev/null +++ b/reverse_skill_proxy.py @@ -0,0 +1,221 @@ +from __future__ import annotations + +import argparse +import json +import os +import shutil +from dataclasses import asdict, dataclass +from pathlib import Path +from typing import Iterable + +DEFAULT_SOURCE_ROOTS = ( + Path.home() / ".hermes-shop" / "skills" / "reverse-skill", + Path.home() / ".hermes-shop" / "skills" / "ctf-sandbox-orchestrator", +) +DEFAULT_BUNDLE_DIRNAME = ".reverse-skill-proxy" +ROUTER_FILENAME = "ROUTER.vi.md" +MANIFEST_FILENAME = "MANIFEST.json" +WORKSPACE_AGENTS_FILENAME = "AGENTS.md" + +ROUTES = [ + { + "name": "ctf-sandbox-orchestrator", + "match": ["ctf", "challenge", "unknown target", "first look"], + "description_vi": "Dùng khi loại bài chưa rõ hoặc cần bộ điều phối ban đầu cho môi trường CTF.", + }, + { + "name": "competition-web-runtime", + "match": ["web", "http", "api", "graphql", "ssrf", "xss", "request smuggling"], + "description_vi": "Ưu tiên cho web/API, route, middleware, auth flow, SSRF/XSS và các biến thể runtime web.", + }, + { + "name": "competition-reverse-pwn", + "match": ["reverse", "pwn", "elf", "pe", "crackme", "rop", "gdb"], + "description_vi": "Ưu tiên khi phân tích nhị phân, khai thác bộ nhớ, reverse và pwn.", + }, + { + "name": "competition-agent-cloud", + "match": ["agent", "llm", "prompt injection", "mcp", "cloud", "container"], + "description_vi": "Dùng cho agent/LLM security, tool abuse, cloud/container drift và prompt injection.", + }, + { + "name": "competition-identity-windows", + "match": ["windows", "ad", "kerberos", "ntlm", "pivot", "domain"], + "description_vi": "Ưu tiên cho Windows/AD, identity abuse, credential pivot và relay/coercion chain.", + }, + { + "name": "reverse-engineering", + "match": ["decompile", "ida", "ghidra", "frida", "apk", "ios"], + "description_vi": "Dùng cho reverse engineering tổng quát, mobile reverse và hiểu luồng xác minh/chữ ký.", + }, + { + "name": "api-security", + "match": ["swagger", "openapi", "jwt", "oauth", "auth bypass"], + "description_vi": "Dùng cho API security, auth bypass, trust boundary và xác minh nguồn dữ liệu.", + }, + { + "name": "attack-chain", + "match": ["multi-step", "chain", "pivot", "lateral", "post-exploitation"], + "description_vi": "Dùng khi cần ghép chuỗi khai thác nhiều bước và theo dõi bằng chứng theo từng fact.", + }, + { + "name": "llm-security", + "match": ["model", "rag", "retrieval poisoning", "jailbreak", "tool misuse"], + "description_vi": "Dùng cho các bài LLM security, RAG poisoning, jailbreak và lạm dụng tool.", + }, +] + + +@dataclass +class SyncSummary: + bundle_dir: str + source_roots: list[str] + copied_skill_directories: int + missing_source_roots: list[str] + router_path: str + workspace_agents_path: str | None + + +def _iter_skill_dirs(source_root: Path) -> Iterable[Path]: + if not source_root.exists(): + return [] + return sorted( + path for path in source_root.iterdir() if path.is_dir() and (path / "SKILL.md").exists() + ) + + +def build_router_markdown(routes: list[dict], source_roots: list[Path]) -> str: + lines = [ + "# Reverse-Skill Router (VI)", + "", + "Router này là lớp proxy tối thiểu để agent/Codex đọc trước khi xử lý CTF, reverse hoặc bài security nhiều bước trong workspace hiện tại.", + "Nếu bundle skill local có sẵn, hãy ưu tiên route hẹp nhất. Nếu chưa rõ loại bài, bắt đầu từ `ctf-sandbox-orchestrator`.", + "", + "## Nguồn local dự kiến", + "", + ] + lines.extend(f"- `{root}`" for root in source_roots) + lines.extend( + [ + "", + "## Quy tắc route", + "", + "1. Kiểm tra yêu cầu và artefact ban đầu bằng phương pháp thụ động trước.", + "2. Chỉ route sang family hẹp nhất phù hợp với challenge hiện tại.", + "3. Nếu thiếu bundle local hoặc skill cụ thể, fallback về workflow mặc định mà không chặn phần còn lại của task.", + "4. Cập nhật theo dạng: kết quả -> bằng chứng -> xác minh -> bước kế tiếp.", + "", + "## Route map", + "", + ] + ) + for route in routes: + match_words = ", ".join(f"`{word}`" for word in route["match"]) + lines.extend( + [ + f"### {route['name']}", + f"- Khi gặp: {match_words}", + f"- Gợi ý: {route['description_vi']}", + "", + ] + ) + return "\n".join(lines).rstrip() + "\n" + + +def build_workspace_agents(router_hint: str = ".reverse-skill-proxy/ROUTER.vi.md") -> str: + return f"""# Reverse-Skill Proxy For GuJumpgate + +This workspace includes an optional Vietnamese reverse-skill proxy bundle for CTF / reverse / exploit tasks. + +## Startup contract + +- When the task is CTF, reverse engineering, binary exploitation, web/API security, prompt injection, mobile analysis, cloud/container drift, or Windows/AD pivoting, read `{router_hint}` first. +- Start with the narrowest matching route instead of loading unrelated skill families. +- If challenge type is still unclear, start with `ctf-sandbox-orchestrator` and let it route you. +- If the bundle or route is missing, fall back to the default workflow without blocking the rest of the task. + +## Local safety expectations + +- Prefer passive inspection first. +- Do not disrupt already-running local services, browser sessions, or extension state unless explicitly asked. +- Keep updates short: outcome -> evidence -> verification -> next step. +""" + + +def sync_reverse_skills( + bundle_dir: Path, + source_roots: Iterable[Path] = DEFAULT_SOURCE_ROOTS, + emit_workspace_agents: Path | None = None, +) -> SyncSummary: + bundle_dir = bundle_dir.expanduser().resolve() + roots = [Path(root).expanduser().resolve() for root in source_roots] + bundle_dir.mkdir(parents=True, exist_ok=True) + + copied = 0 + missing_roots: list[str] = [] + for root in roots: + if not root.exists(): + missing_roots.append(str(root)) + continue + dest_root = bundle_dir / root.name + dest_root.mkdir(parents=True, exist_ok=True) + for skill_dir in _iter_skill_dirs(root): + target_dir = dest_root / skill_dir.name + if target_dir.exists(): + shutil.rmtree(target_dir) + shutil.copytree(skill_dir, target_dir) + copied += 1 + + router_path = bundle_dir / ROUTER_FILENAME + router_path.write_text(build_router_markdown(ROUTES, roots), encoding="utf-8") + + agents_path: str | None = None + if emit_workspace_agents is not None: + emit_workspace_agents = emit_workspace_agents.expanduser().resolve() + emit_workspace_agents.mkdir(parents=True, exist_ok=True) + workspace_agents_path = emit_workspace_agents / WORKSPACE_AGENTS_FILENAME + workspace_agents_path.write_text(build_workspace_agents(), encoding="utf-8") + agents_path = str(workspace_agents_path) + + summary = SyncSummary( + bundle_dir=str(bundle_dir), + source_roots=[str(root) for root in roots], + copied_skill_directories=copied, + missing_source_roots=missing_roots, + router_path=str(router_path), + workspace_agents_path=agents_path, + ) + manifest_path = bundle_dir / MANIFEST_FILENAME + manifest_path.write_text(json.dumps(asdict(summary), ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + return summary + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Sync local reverse-skill bundles and generate a Vietnamese router proxy." + ) + parser.add_argument( + "--bundle-dir", + default=os.path.join(os.getcwd(), DEFAULT_BUNDLE_DIRNAME), + help="Directory where the proxy bundle should be written.", + ) + parser.add_argument( + "--workspace-root", + default=None, + help="Optional workspace root where an AGENTS.md proxy file should be emitted.", + ) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + summary = sync_reverse_skills( + bundle_dir=Path(args.bundle_dir), + emit_workspace_agents=Path(args.workspace_root) if args.workspace_root else None, + ) + print(json.dumps(asdict(summary), ensure_ascii=False, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/shared/i18n.js b/shared/i18n.js new file mode 100644 index 0000000..9b180db --- /dev/null +++ b/shared/i18n.js @@ -0,0 +1,161 @@ +(function attachGuJumpgateI18n(globalScope) { + const FALLBACK_LOCALE = 'vi-VN'; + const STORAGE_KEY = 'gujumpgate:locale'; + const locales = globalScope.GUJUMPGATE_LOCALES || (globalScope.GUJUMPGATE_LOCALES = {}); + + function isObject(value) { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); + } + + function resolvePath(source, key) { + if (!isObject(source) || !key) { + return undefined; + } + return String(key) + .split('.') + .reduce((current, part) => (isObject(current) || Array.isArray(current) ? current[part] : undefined), source); + } + + function interpolate(template, params = {}) { + return String(template).replace(/\{(\w+)\}/g, (_, name) => { + return Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : `{${name}}`; + }); + } + + function getStoredLocale() { + try { + return String(globalScope.localStorage?.getItem(STORAGE_KEY) || '').trim(); + } catch { + return ''; + } + } + + function persistLocale(locale) { + try { + globalScope.localStorage?.setItem(STORAGE_KEY, locale); + } catch { + // Ignore storage errors in restricted contexts. + } + } + + function determineInitialLocale() { + const candidates = [ + getStoredLocale(), + String(globalScope.GUJUMPGATE_LOCALE || '').trim(), + String(globalScope.navigator?.language || '').trim(), + FALLBACK_LOCALE, + ].filter(Boolean); + + for (const candidate of candidates) { + if (locales[candidate]) { + return candidate; + } + } + return FALLBACK_LOCALE; + } + + let currentLocale = determineInitialLocale(); + + function registerLocale(locale, messages) { + if (!locale || !isObject(messages)) { + return; + } + locales[String(locale)] = messages; + if (!currentLocale) { + currentLocale = String(locale); + } + } + + function getLocaleMessages(locale) { + return locales[String(locale)] || {}; + } + + function getDocument() { + if (globalScope.document) { + return globalScope.document; + } + if (typeof document !== 'undefined') { + return document; + } + return null; + } + + function setDocumentLang(locale) { + const documentRef = getDocument(); + if (documentRef?.documentElement && locale) { + documentRef.documentElement.lang = locale; + } + } + + function t(key, params = {}, fallback = '') { + const direct = resolvePath(getLocaleMessages(currentLocale), key); + const fallbackValue = resolvePath(getLocaleMessages(FALLBACK_LOCALE), key); + const message = direct ?? fallbackValue ?? fallback; + if (message === undefined || message === null || message === '') { + return key; + } + return interpolate(message, params); + } + + function applyTranslations(root = getDocument()) { + if (!root?.querySelectorAll) { + setDocumentLang(currentLocale); + return; + } + + root.querySelectorAll('[data-i18n]').forEach((node) => { + const key = node.dataset?.i18n; + if (!key) return; + node.textContent = t(key); + }); + + root.querySelectorAll('[data-i18n-placeholder]').forEach((node) => { + const key = node.dataset?.i18nPlaceholder; + if (!key) return; + node.placeholder = t(key); + }); + + root.querySelectorAll('[data-i18n-title]').forEach((node) => { + const key = node.dataset?.i18nTitle; + if (!key) return; + const value = t(key); + node.title = value; + node.setAttribute?.('title', value); + }); + + root.querySelectorAll('[data-i18n-aria-label]').forEach((node) => { + const key = node.dataset?.i18nAriaLabel; + if (!key) return; + node.setAttribute?.('aria-label', t(key)); + }); + + setDocumentLang(currentLocale); + } + + function setLocale(locale, options = {}) { + const normalized = String(locale || '').trim(); + if (!normalized || !locales[normalized]) { + return currentLocale; + } + currentLocale = normalized; + if (options.persist !== false) { + persistLocale(currentLocale); + } + applyTranslations(options.root || globalScope.document); + return currentLocale; + } + + const api = { + FALLBACK_LOCALE, + STORAGE_KEY, + getLocale: () => currentLocale, + getLocales: () => ({ ...locales }), + registerLocale, + t, + applyTranslations, + setLocale, + }; + + globalScope.GuJumpgateI18n = api; + setDocumentLang(currentLocale); +})(typeof window !== 'undefined' ? window : globalThis); diff --git a/sidepanel/account-records-manager.js b/sidepanel/account-records-manager.js index 33d1d92..c827a56 100644 --- a/sidepanel/account-records-manager.js +++ b/sidepanel/account-records-manager.js @@ -13,40 +13,40 @@ const FILTER_CONFIG = { all: { - label: '总', + label: 'Tổng', className: '', matches: () => true, - metaLabel: '全部', + metaLabel: 'Tất cả', }, success: { - label: '成', + label: 'Thành công', className: 'is-success', matches: (record) => getRecordDisplayStatus(record) === 'success', - metaLabel: '成功', + metaLabel: 'Thành công', }, running: { - label: '运行', + label: 'Chạy', className: 'is-running', matches: (record) => getRecordDisplayStatus(record) === 'running', - metaLabel: '运行中', + metaLabel: 'Chạy中', }, failed: { - label: '失', + label: 'Thất bại', className: 'is-failed', matches: (record) => getRecordDisplayStatus(record) === 'failed', - metaLabel: '失败', + metaLabel: 'Thất bại', }, stopped: { - label: '停', + label: 'Dừng', className: 'is-stopped', matches: (record) => getRecordDisplayStatus(record) === 'stopped', - metaLabel: '停止', + metaLabel: 'Dừng', }, retry: { - label: '重试', + label: 'Thử lại', className: 'is-retry', matches: (record) => normalizeRetryCount(record.retryCount) > 0, - metaLabel: '重试', + metaLabel: 'Thử lại', }, }; @@ -150,7 +150,7 @@ return { ...record, displayStatus: 'running', - displaySummary: '正在运行', + displaySummary: 'Đang chạy', }; } @@ -214,7 +214,7 @@ } function getRecordTitle(record = {}) { - const primaryIdentifier = getRecordPrimaryIdentifier(record) || '(空账号)'; + const primaryIdentifier = getRecordPrimaryIdentifier(record) || '(Tài khoản trống)'; const secondaryIdentifier = getRecordSecondaryIdentifier(record); return secondaryIdentifier ? `${primaryIdentifier} / ${secondaryIdentifier}` @@ -292,15 +292,15 @@ function getStatusMeta(record = {}) { const status = getRecordDisplayStatus(record); if (status === 'success') { - return { kind: 'success', label: '成功' }; + return { kind: 'success', label: 'Thành công' }; } if (status === 'running') { - return { kind: 'running', label: '正在运行' }; + return { kind: 'running', label: 'Đang chạy' }; } if (status === 'stopped') { - return { kind: 'stopped', label: '停止' }; + return { kind: 'stopped', label: 'Dừng' }; } - return { kind: 'failed', label: '失败' }; + return { kind: 'failed', label: 'Thất bại' }; } function getRecordSummaryText(record = {}) { @@ -309,15 +309,15 @@ return String(record.displaySummary || '').trim(); } if (status === 'success') { - return '流程完成'; + return '流程完Thành công'; } if (status === 'running') { - return '正在运行'; + return 'Đang chạy'; } return String(record.failureDetail || record.reason || '').trim() || String(record.failureLabel || '').trim() - || '流程失败'; + || '流程Thất bại'; } function getRecordTooltipText(record = {}, summaryText = '') { @@ -438,7 +438,7 @@ } if (!allRecords.length) { - dom.accountRecordsMeta.textContent = '暂无账号记录'; + dom.accountRecordsMeta.textContent = 'Chưa có bản ghi tài khoản'; return; } @@ -479,14 +479,14 @@ setNodeHidden(dom.btnClearAccountRecords, selectionMode); toggleNodeClass(dom.btnToggleAccountRecordsSelection, 'is-active', selectionMode); setNodeAttr(dom.btnToggleAccountRecordsSelection, 'aria-pressed', selectionMode ? 'true' : 'false'); - setNodeText(dom.btnToggleAccountRecordsSelection, selectionMode ? '取消多选' : '多选'); + setNodeText(dom.btnToggleAccountRecordsSelection, selectionMode ? 'Huỷ chọn nhiều' : 'Chọn nhiều'); const selectedCount = selectedRecordIds.size; setNodeHidden(dom.btnDeleteSelectedAccountRecords, !selectionMode); setNodeDisabled(dom.btnDeleteSelectedAccountRecords, selectedCount === 0); setNodeText( dom.btnDeleteSelectedAccountRecords, - selectedCount > 0 ? `删除选中(${selectedCount})` : '删除选中' + selectedCount > 0 ? `Xoá选中(${selectedCount})` : 'Xoá选中' ); } @@ -514,7 +514,7 @@ const message = allRecords.length ? `当前筛选“${getFilterConfig(activeFilter).metaLabel}”下暂无记录` - : '暂无账号记录'; + : 'Chưa có bản ghi tài khoản'; dom.accountRecordsList.innerHTML = `
${escapeHtml(message)}
`; } @@ -534,7 +534,7 @@ dom.accountRecordsList.innerHTML = visibleRecords.map((record) => { const recordId = buildRecordId(record); - const primaryIdentifier = getRecordPrimaryIdentifier(record) || '(空账号)'; + const primaryIdentifier = getRecordPrimaryIdentifier(record) || '(Tài khoản trống)'; const secondaryIdentifier = getRecordSecondaryIdentifier(record); const statusMeta = getStatusMeta(record); const summaryText = getRecordSummaryText(record); @@ -580,7 +580,7 @@
${escapeHtml(summaryText)}
- 重试 ${escapeHtml(String(retryCount))} + Thử lại ${escapeHtml(String(retryCount))}
`; @@ -653,14 +653,14 @@ async function clearRecords() { const records = getAccountRunRecords(); if (!records.length) { - helpers.showToast?.('没有可清理的账号记录。', 'warn', 1800); + helpers.showToast?.('Không có bản ghi tài khoản nào để dọn dẹp.', 'warn', 1800); return; } const confirmed = await helpers.openConfirmModal({ - title: '清理账号记录', - message: '确认清理当前全部账号记录吗?该操作会同时清空面板记录与本地同步快照。', - confirmLabel: '确认清理', + title: 'Dọn dẹp bản ghi tài khoản', + message: 'Xác nhận dọn dẹp toàn bộ bản ghi tài khoản hiện tại? Thao tác này sẽ xoá cả bản ghi trong panel lẫn snapshot đồng bộ cục bộ.', + confirmLabel: 'Xác nhận dọn dẹp', confirmVariant: 'btn-danger', }); if (!confirmed) { @@ -680,20 +680,20 @@ selectionMode = false; resetSelection(); state.syncLatestState({ accountRunHistory: [] }); - helpers.showToast?.(`已清理 ${Math.max(0, Number(response?.clearedCount) || 0)} 条账号记录。`, 'success', 2200); + helpers.showToast?.(`已清理 ${Math.max(0, Number(response?.clearedCount) || 0)} 条Tài khoản记录。`, 'success', 2200); } async function deleteSelectedRecords() { const recordIds = Array.from(selectedRecordIds).filter(Boolean); if (!recordIds.length) { - helpers.showToast?.('请先勾选要删除的账号记录。', 'warn', 1800); + helpers.showToast?.('请先勾选要Xoá的Tài khoản记录。', 'warn', 1800); return; } const confirmed = await helpers.openConfirmModal({ - title: '删除选中记录', - message: `确认删除选中的 ${recordIds.length} 条账号记录吗?该操作会同步更新本地 helper 快照。`, - confirmLabel: '确认删除', + title: 'Xoá选中记录', + message: `Xác nhận xoá选中的 ${recordIds.length} 条Tài khoản记录吗?该操作会同步更新本地 helper 快照。`, + confirmLabel: 'Xác nhận xoá', confirmVariant: 'btn-danger', }); if (!confirmed) { @@ -717,7 +717,7 @@ resetSelection(); state.syncLatestState({ accountRunHistory: nextRecords }); - helpers.showToast?.(`已删除 ${Math.max(0, Number(response?.deletedCount) || 0)} 条账号记录。`, 'success', 2200); + helpers.showToast?.(`已Xoá ${Math.max(0, Number(response?.deletedCount) || 0)} 条Tài khoản记录。`, 'success', 2200); } function handleStatsClick(event) { @@ -802,14 +802,14 @@ try { await deleteSelectedRecords(); } catch (error) { - helpers.showToast?.(`删除账号记录失败:${error.message}`, 'error'); + helpers.showToast?.(`XoáTài khoản记录Thất bại:${error.message}`, 'error'); } }); dom.btnClearAccountRecords?.addEventListener('click', async () => { try { await clearRecords(); } catch (error) { - helpers.showToast?.(`清理账号记录失败:${error.message}`, 'error'); + helpers.showToast?.(`Dọn dẹp bản ghi tài khoảnThất bại:${error.message}`, 'error'); } }); } diff --git a/sidepanel/contribution-mode.js b/sidepanel/contribution-mode.js index dab7258..3c37765 100644 --- a/sidepanel/contribution-mode.js +++ b/sidepanel/contribution-mode.js @@ -1,7 +1,10 @@ (function attachSidepanelContributionMode(globalScope) { const ACTIVE_STATUSES = new Set(['started', 'waiting', 'processing']); const FINAL_STATUSES = new Set(['auto_approved', 'auto_rejected', 'expired', 'error']); - const DEFAULT_COPY = '当前账号将用于支持项目维护。扩展会自动申请贡献登录地址并持续跟踪授权状态;如检测到回调地址,会自动提交,并继续等待服务端确认。'; + const translate = globalScope.GuJumpgateI18n?.t + ? (key, params = {}, fallback = '') => globalScope.GuJumpgateI18n.t(key, params, fallback) + : (_key, _params = {}, fallback = '') => fallback || ''; + const DEFAULT_COPY = translate('settings.contributionCopy', {}, 'Tài khoản hiện tại sẽ được dùng để hỗ trợ duy trì dự án. Extension sẽ tự xin địa chỉ đăng nhập đóng góp và theo dõi trạng thái ủy quyền; nếu phát hiện callback thì sẽ tự gửi và tiếp tục chờ xác nhận từ máy chủ.'); const CONTRIBUTION_SOURCE_CPA = 'cpa'; const CONTRIBUTION_SOURCE_SUB2API = 'sub2api'; const CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME = 'codex号池'; @@ -116,7 +119,7 @@ dom.btnContributionMode.classList.toggle('is-active', enabled); dom.btnContributionMode.setAttribute('aria-pressed', String(enabled)); dom.btnContributionMode.disabled = false; - dom.btnContributionMode.title = '打开项目仓库说明页'; + dom.btnContributionMode.title = translate('header.guide', {}, 'Hướng dẫn sử dụng'); } function stopPolling() { @@ -152,40 +155,40 @@ const status = normalizeStatus(currentState.contributionStatus); const hasAuthUrl = Boolean(normalizeString(currentState.contributionAuthUrl)); if (!normalizeString(currentState.contributionSessionId) || !hasAuthUrl) { - return '未生成登录地址'; + return translate('settings.oauthPending', {}, 'Chưa tạo địa chỉ đăng nhập'); } if (status === 'waiting') { - return '等待提交回调'; + return 'Đang chờ gửi callback'; } if (status === 'processing' || status === 'auto_approved' || status === 'auto_rejected') { - return status === 'processing' ? '已提交回调' : '授权已结束'; + return status === 'processing' ? 'Đã gửi callback' : 'Đã kết thúc ủy quyền'; } if (status === 'expired' || status === 'error') { - return '授权失败'; + return 'Ủy quyền thất bại'; } if (Number(currentState.contributionAuthOpenedAt) > 0) { - return '已打开授权页'; + return 'Đã mở trang ủy quyền'; } - return '登录地址已生成'; + return 'Đã tạo địa chỉ đăng nhập'; } function getCallbackStatusText(currentState = getLatestState()) { const status = normalizeCallbackStatus(currentState.contributionCallbackStatus); switch (status) { case 'captured': - return '已捕获回调地址'; + return 'Đã bắt được địa chỉ callback'; case 'submitting': - return '正在提交回调'; + return 'Đang gửi callback'; case 'submitted': - return '已提交回调'; + return 'Đã gửi callback'; case 'failed': - return '回调提交失败'; + return 'Gửi callback thất bại'; case 'waiting': case 'idle': default: return normalizeString(currentState.contributionCallbackUrl) - ? '已捕获回调地址' - : '等待回调'; + ? 'Đã bắt được địa chỉ callback' + : translate('settings.callbackWaiting', {}, 'Đang chờ callback'); } } @@ -196,7 +199,7 @@ } if (getContributionSource(currentState) === CONTRIBUTION_SOURCE_SUB2API) { const groupName = normalizeString(currentState.contributionTargetGroupName) || CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME; - return `当前账号将用于支持项目维护。贡献会通过 SUB2API 完成,并固定写入 ${groupName} 分组;如检测到回调地址,扩展会自动提交并等待服务端确认。`; + return `Tài khoản hiện tại sẽ được dùng để hỗ trợ duy trì dự án. Đóng góp sẽ được hoàn tất qua SUB2API và luôn ghi vào nhóm ${groupName}; nếu phát hiện callback, extension sẽ tự gửi và chờ xác nhận từ máy chủ.`; } return DEFAULT_COPY; } @@ -254,7 +257,7 @@ throw new Error(response.error); } if (!response?.state) { - throw new Error('贡献模式切换后未返回最新状态。'); + throw new Error('Không nhận được trạng thái mới sau khi chuyển chế độ đóng góp.'); } helpers.applySettingsState?.(response.state); @@ -300,7 +303,7 @@ async function startContributionFlow() { if (typeof helpers.startContributionAutoRun !== 'function') { - throw new Error('贡献模式尚未接入主自动流程启动能力。'); + throw new Error('Chế độ đóng góp hiện chưa nối vào khả năng khởi động luồng tự động chính.'); } const profile = helpers.getContributionProfile?.() || {}; @@ -315,19 +318,19 @@ return; } - helpers.showToast?.('贡献自动流程已启动。', 'info', 1800); + helpers.showToast?.('Luồng tự động đóng góp đã khởi chạy.', 'info', 1800); render(); } async function enterContributionMode() { await requestContributionMode(true); - helpers.showToast?.('已进入贡献模式。', 'success', 1800); + helpers.showToast?.('Đã vào chế độ đóng góp.', 'success', 1800); } async function exitContributionMode() { stopPolling(); await requestContributionMode(false); - helpers.showToast?.('已退出贡献模式。', 'info', 1800); + helpers.showToast?.('Đã thoát chế độ đóng góp.', 'info', 1800); } function render() { @@ -395,7 +398,7 @@ if (dom.btnExitContributionMode) { dom.btnExitContributionMode.disabled = actionInFlight || blocked; - dom.btnExitContributionMode.title = blocked ? '当前流程运行中,暂时不能退出贡献模式' : '退出贡献模式'; + dom.btnExitContributionMode.title = blocked ? 'Quy trình hiện tại đang chạy, tạm thời chưa thể thoát chế độ đóng góp' : 'Thoát chế độ đóng góp'; } if (dom.btnOpenAccountRecords) { @@ -418,7 +421,7 @@ try { helpers.openExternalUrl?.(guideRepositoryUrl); } catch (error) { - helpers.showToast?.(`打开说明页失败:${error.message}`, 'error'); + helpers.showToast?.(`Mở trang hướng dẫn thất bại: ${error.message}`, 'error'); } }); @@ -468,7 +471,7 @@ try { openContributionUploadPage(); } catch (error) { - helpers.showToast?.(`打开上传页面失败:${error.message}`, 'error'); + helpers.showToast?.(`Mở trang tải lên thất bại: ${error.message}`, 'error'); } }); diff --git a/sidepanel/custom-email-pool-manager.js b/sidepanel/custom-email-pool-manager.js index 82a3e1d..3d61839 100644 --- a/sidepanel/custom-email-pool-manager.js +++ b/sidepanel/custom-email-pool-manager.js @@ -97,9 +97,9 @@ const haystack = [ entry.email, entry.note, - entry.enabled ? 'enabled 启用' : 'disabled 停用', - entry.used ? 'used 已用' : 'unused 未用', - entry.current ? 'current 当前' : '', + entry.enabled ? 'enabled Bật' : 'disabled ngừng dùng', + entry.used ? 'used đã dùng' : 'unused chưa dùng', + entry.current ? 'current hiện tại' : '', ].join(' ').toLowerCase(); return haystack.includes(normalizedSearchTerm); @@ -159,8 +159,8 @@ if (!renderedEntries.length) { selectedEntryIds.clear(); - dom.customEmailPoolList.innerHTML = '
还没有自定义邮箱,先导入一批邮箱再开始。
'; - dom.customEmailPoolSummary.textContent = '导入你提前准备好的注册邮箱,每行一个邮箱地址。'; + dom.customEmailPoolList.innerHTML = '
Chưa có email tuỳ chỉnh, hãy nhập một lô email trước khi bắt đầu.
'; + dom.customEmailPoolSummary.textContent = 'Nhập các email đăng ký bạn đã chuẩn bị sẵn, mỗi dòng một địa chỉ email.'; if (dom.btnCustomEmailPoolClearUsed) dom.btnCustomEmailPoolClearUsed.disabled = true; if (dom.btnCustomEmailPoolDeleteAll) dom.btnCustomEmailPoolDeleteAll.disabled = true; updateBulkUi([]); @@ -170,13 +170,13 @@ const entriesWithCurrent = withCurrentFlag(renderedEntries); const usedCount = entriesWithCurrent.filter((entry) => entry.used).length; const enabledCount = entriesWithCurrent.filter((entry) => entry.enabled).length; - dom.customEmailPoolSummary.textContent = `已加载 ${entriesWithCurrent.length} 个邮箱,其中 ${enabledCount} 个启用,${usedCount} 个已标记为已用。`; + dom.customEmailPoolSummary.textContent = `已加载 ${entriesWithCurrent.length} 个邮箱,其中 ${enabledCount} 个Bật,${usedCount} 个已Đánh dấu là đã dùng。`; if (dom.btnCustomEmailPoolClearUsed) dom.btnCustomEmailPoolClearUsed.disabled = loading || usedCount === 0; if (dom.btnCustomEmailPoolDeleteAll) dom.btnCustomEmailPoolDeleteAll.disabled = loading || entriesWithCurrent.length === 0; const visibleEntries = getFilteredEntries(entriesWithCurrent); if (!visibleEntries.length) { - dom.customEmailPoolList.innerHTML = '
没有匹配当前筛选条件的邮箱。
'; + dom.customEmailPoolList.innerHTML = '
Không có email nào khớp bộ lọc hiện tại.
'; updateBulkUi([]); return; } @@ -189,27 +189,27 @@
- +
- ${entry.current ? '当前' : ''} - ${entry.used ? '已用' : '未用'} - ${entry.enabled ? '启用' : '停用'} + ${entry.current ? 'Hiện tại' : ''} + ${entry.used ? 'Đã dùng' : 'Chưa dùng'} + ${entry.enabled ? 'Bật' : 'Ngừng dùng'} ${entry.note ? `${helpers.escapeHtml(entry.note)}` : ''}
- - - + + +
`; @@ -224,17 +224,17 @@ item.querySelector('[data-action="copy-email"]').addEventListener('click', async () => { await helpers.copyTextToClipboard(entry.email || ''); - helpers.showToast('邮箱已复制', 'success', 1600); + helpers.showToast('Đã sao chép email', 'success', 1600); }); item.querySelector('[data-action="use"]').addEventListener('click', async () => { try { - setLoadingState(true, '正在切换当前邮箱...'); + setLoadingState(true, 'Đang chuyển email hiện tại...'); await actions.setRuntimeEmail?.(entry.email); helpers.showToast(`已切换到 ${entry.email}`, 'success', 1800); queueCustomEmailPoolRefresh(); } catch (error) { - helpers.showToast(`切换邮箱失败:${error.message}`, 'error'); + helpers.showToast(`切换邮箱Thất bại:${error.message}`, 'error'); } finally { setLoadingState(false); } @@ -263,7 +263,7 @@ item.querySelector('[data-action="delete"]').addEventListener('click', async () => { await deleteEntries({ ids: [entry.id], - }, `确认删除 ${entry.email} 吗?此操作不可撤销。`); + }, `Xác nhận xoá ${entry.email} 吗?此操作不可撤销。`); }); dom.customEmailPoolList.appendChild(item); @@ -276,7 +276,7 @@ const previousEntries = normalizeEntries(state.getEntries?.() || []); const nextEntries = normalizeEntries(mutator(previousEntries.map((entry) => ({ ...entry })))); - setLoadingState(true, '正在更新自定义邮箱池...'); + setLoadingState(true, 'Đang cập nhật pool email tuỳ chỉnh...'); state.setEntries?.(nextEntries); renderCustomEmailPoolEntries(nextEntries); @@ -285,7 +285,7 @@ } catch (error) { state.setEntries?.(previousEntries); renderCustomEmailPoolEntries(previousEntries); - helpers.showToast(`更新自定义邮箱池失败:${error.message}`, 'error'); + helpers.showToast(`更新自定义邮箱池Thất bại:${error.message}`, 'error'); } finally { setLoadingState(false); } @@ -293,9 +293,9 @@ async function deleteEntries(payload = {}, confirmMessage = '') { const confirmed = await helpers.openConfirmModal({ - title: '删除邮箱', - message: confirmMessage || '确认删除选中的邮箱吗?此操作不可撤销。', - confirmLabel: '确认删除', + title: 'Xoá邮箱', + message: confirmMessage || 'Xác nhận xoá选中的邮箱吗?此操作不可撤销。', + confirmLabel: 'Xác nhận xoá', confirmVariant: 'btn-danger', }); if (!confirmed) { @@ -327,7 +327,7 @@ async function importEntriesFromTextarea() { const text = String(dom.inputCustomEmailPoolImport?.value || ''); if (!text.trim()) { - helpers.showToast('请先粘贴邮箱列表,每行一个邮箱。', 'warn'); + helpers.showToast('Hãy dán trước danh sách email, mỗi dòng một email.', 'warn'); return; } @@ -358,12 +358,12 @@ } if (!importedEntries.length && skippedCount > 0) { - helpers.showToast('没有可导入的新邮箱(可能都重复或无效)。', 'warn'); + helpers.showToast('Không có email mới nào để nhập (có thể đều bị trùng hoặc không hợp lệ).', 'warn'); return; } const nextEntries = normalizeEntries([...previousEntries, ...importedEntries]); - setLoadingState(true, '正在导入邮箱...'); + setLoadingState(true, 'Đang nhập email...'); state.setEntries?.(nextEntries); renderCustomEmailPoolEntries(nextEntries); @@ -382,7 +382,7 @@ } catch (error) { state.setEntries?.(previousEntries); renderCustomEmailPoolEntries(previousEntries); - helpers.showToast(`导入邮箱失败:${error.message}`, 'error'); + helpers.showToast(`导入邮箱Thất bại:${error.message}`, 'error'); } finally { setLoadingState(false); } @@ -395,7 +395,7 @@ } if (!silent) { - setLoadingState(true, '正在刷新自定义邮箱池...'); + setLoadingState(true, 'Đang làm mới pool email tuỳ chỉnh...'); } renderCustomEmailPoolEntries(state.getEntries?.()); if (!silent) { @@ -422,7 +422,7 @@ dom.customEmailPoolList.innerHTML = ''; } if (dom.customEmailPoolSummary) { - dom.customEmailPoolSummary.textContent = '导入你提前准备好的注册邮箱,每行一个邮箱地址。'; + dom.customEmailPoolSummary.textContent = 'Nhập các email đăng ký bạn đã chuẩn bị sẵn, mỗi dòng một địa chỉ email.'; } updateBulkUi([]); } @@ -503,19 +503,19 @@ dom.btnCustomEmailPoolBulkDelete?.addEventListener('click', async () => { await deleteEntries({ ids: [...selectedEntryIds], - }, `确认删除当前选中的 ${selectedEntryIds.size} 个邮箱吗?此操作不可撤销。`); + }, `Xác nhận xoá当前选中的 ${selectedEntryIds.size} 个邮箱吗?此操作不可撤销。`); }); dom.btnCustomEmailPoolClearUsed?.addEventListener('click', async () => { await deleteEntries({ mode: 'used', - }, '确认删除当前所有已用邮箱吗?此操作不可撤销。'); + }, 'Xác nhận xoá当前所有Đã dùng邮箱吗?此操作不可撤销。'); }); dom.btnCustomEmailPoolDeleteAll?.addEventListener('click', async () => { await deleteEntries({ mode: 'all', - }, '确认删除当前全部邮箱吗?此操作不可撤销。'); + }, 'Xác nhận xoá toàn bộ email hiện tại? Thao tác này không thể hoàn tác.'); }); } diff --git a/sidepanel/hosted-sms-pool-manager.js b/sidepanel/hosted-sms-pool-manager.js index f860285..f1214b9 100644 --- a/sidepanel/hosted-sms-pool-manager.js +++ b/sidepanel/hosted-sms-pool-manager.js @@ -13,41 +13,41 @@ } = context; const copyIcon = constants.copyIcon || ''; - const poolLabel = String(labels.poolLabel || 'PayPal 接码池').trim() || 'PayPal 接码池'; - const importSubject = String(labels.importSubject || `${poolLabel}号码`).trim() || `${poolLabel}号码`; - const numberNoun = String(labels.numberNoun || '号码').trim() || '号码'; - const localPhonePrefix = String(labels.localPhonePrefix || 'PayPal 填').trim(); + const poolLabel = String(labels.poolLabel || 'Kho số nhận mã PayPal').trim() || 'Kho số nhận mã PayPal'; + const importSubject = String(labels.importSubject || `${poolLabel}Số`).trim() || `${poolLabel}Số`; + const numberNoun = String(labels.numberNoun || 'Số').trim() || 'Số'; + const localPhonePrefix = String(labels.localPhonePrefix || 'Điền PayPal').trim(); const emptySummary = String( - labels.emptySummary || `导入 ${importSubject},每行一个号码和验证码接口。` - ).trim() || `导入 ${importSubject},每行一个号码和验证码接口。`; + labels.emptySummary || `导入 ${importSubject},每行一个Số和验证码接口。` + ).trim() || `导入 ${importSubject},每行一个Số和验证码接口。`; const emptyListText = String( - labels.emptyListText || `还没有 ${poolLabel}号码,先导入一批号码再开始。` - ).trim() || `还没有 ${poolLabel}号码,先导入一批号码再开始。`; + labels.emptyListText || `还没有 ${poolLabel}Số,先导入一批Số再开始。` + ).trim() || `还没有 ${poolLabel}Số,先导入一批Số再开始。`; const noMatchText = String( labels.noMatchText || `没有匹配当前筛选条件的${numberNoun}。` ).trim() || `没有匹配当前筛选条件的${numberNoun}。`; const refreshLoadingText = String( - labels.refreshLoadingText || `正在刷新${poolLabel}...` + labels.refreshLoadingText || `Đang làm mới ${poolLabel}...` ).trim() || `正在刷新${poolLabel}...`; const updateLoadingText = String( - labels.updateLoadingText || `正在更新${poolLabel}...` + labels.updateLoadingText || `Đang cập nhật ${poolLabel}...` ).trim() || `正在更新${poolLabel}...`; const updateFailedPrefix = String( - labels.updateFailedPrefix || `更新${poolLabel}失败` - ).trim() || `更新${poolLabel}失败`; - const copySuccessText = String(labels.copySuccessText || '号码已复制').trim() || '号码已复制'; + labels.updateFailedPrefix || `更新${poolLabel}Thất bại` + ).trim() || `更新${poolLabel}Thất bại`; + const copySuccessText = String(labels.copySuccessText || 'Số已复制').trim() || 'Số已复制'; const importEmptyWarning = String( - labels.importEmptyWarning || `请先粘贴${importSubject},每行一个号码和验证码接口。` - ).trim() || `请先粘贴${importSubject},每行一个号码和验证码接口。`; - const deleteTitle = String(labels.deleteTitle || `删除${poolLabel}号码`).trim() || `删除${poolLabel}号码`; - const clearUsageTitle = String(labels.clearUsageTitle || '清空使用次数').trim() || '清空使用次数'; + labels.importEmptyWarning || `请先粘贴${importSubject},每行一个Số和验证码接口。` + ).trim() || `请先粘贴${importSubject},每行一个Số和验证码接口。`; + const deleteTitle = String(labels.deleteTitle || `Xoá${poolLabel}Số`).trim() || `Xoá${poolLabel}Số`; + const clearUsageTitle = String(labels.clearUsageTitle || 'Xoá số lần sử dụng').trim() || 'Xoá số lần sử dụng'; const clearUsageMessage = String( - labels.clearUsageMessage || `确认清空${poolLabel}的使用次数吗?号码本身会保留。` - ).trim() || `确认清空${poolLabel}的使用次数吗?号码本身会保留。`; - const deleteAllTitle = String(labels.deleteAllTitle || `删除${poolLabel}`).trim() || `删除${poolLabel}`; + labels.clearUsageMessage || `Xác nhận xoá số lần sử dụng của ${poolLabel}? Bản thân số vẫn sẽ được giữ lại.` + ).trim() || `确认清空${poolLabel}的使用次数吗?Số本身会Giữ lại。`; + const deleteAllTitle = String(labels.deleteAllTitle || `Xoá${poolLabel}`).trim() || `Xoá${poolLabel}`; const deleteAllMessage = String( - labels.deleteAllMessage || `确认删除当前全部${importSubject}吗?此操作不可撤销。` - ).trim() || `确认删除当前全部${importSubject}吗?此操作不可撤销。`; + labels.deleteAllMessage || `Xác nhận xoá toàn bộ ${importSubject} hiện tại? Thao tác này không thể hoàn tác.` + ).trim() || `Xác nhận xoá当前Tất cả${importSubject}吗?此操作不可撤销。`; const normalizePoolPhoneValue = typeof normalizers.normalizePhone === 'function' ? normalizers.normalizePhone : normalizeUsHostedPhoneDigits; @@ -224,12 +224,12 @@ return [ entry.phone, entry.verificationUrl, - entry.enabled ? 'enabled 启用' : 'disabled 禁用', - entry.current ? 'current 当前' : '', - entry.used ? 'used 已用' : 'unused 未用', - entry.exhausted ? 'exhausted 已达上限' : '', + entry.enabled ? 'enabled Bật' : 'disabled Tắt', + entry.current ? 'current hiện tại' : '', + entry.used ? 'used đã dùng' : 'unused chưa dùng', + entry.exhausted ? 'exhausted đã đạt giới hạn' : '', entry.disabledReason ? `disabledReason ${entry.disabledReason}` : '', - entry.lastError ? `error 异常 ${entry.lastError}` : '', + entry.lastError ? `error Lỗi ${entry.lastError}` : '', ].join(' ').toLowerCase().includes(normalizedSearch); }); } @@ -286,8 +286,8 @@ ? entriesWithState.filter((entry) => entry.exhausted).length : 0; dom.hostedSmsPoolSummary.textContent = usageLimit > 0 - ? `已加载 ${entriesWithState.length} 个号码,${usedCount} 个有使用记录,${exhaustedCount} 个已达上限,${disabledCount} 个已禁用,累计使用 ${totalUseCount} 次。` - : `已加载 ${entriesWithState.length} 个号码,${usedCount} 个有使用记录,${disabledCount} 个已禁用,累计使用 ${totalUseCount} 次。`; + ? `已加载 ${entriesWithState.length} 个Số,${usedCount} 个有使用记录,${exhaustedCount} 个已达上限,${disabledCount} 个已Tắt,累计使用 ${totalUseCount} 次。` + : `已加载 ${entriesWithState.length} 个Số,${usedCount} 个有使用记录,${disabledCount} 个已Tắt,累计使用 ${totalUseCount} 次。`; const visibleEntries = getFilteredEntries(renderedEntries); if (!visibleEntries.length) { @@ -309,32 +309,32 @@
${helpers.escapeHtml?.(entry.phone) || entry.phone} - ${entry.current ? '当前' : ''} + ${entry.current ? 'Hiện tại' : ''} ${localPhonePrefix && localPhone && localPhone !== entry.phone ? `${helpers.escapeHtml?.(localPhonePrefix) || localPhonePrefix} ${helpers.escapeHtml?.(localPhone) || localPhone}` : ''}
${helpers.escapeHtml?.(entry.verificationUrl) || entry.verificationUrl}
- ${entry.current ? '当前' : ''} - ${entry.enabled ? '启用中' : '已禁用'} + ${entry.current ? 'Hiện tại' : ''} + ${entry.enabled ? 'Bật中' : '已Tắt'} ${helpers.escapeHtml?.(usageText) || usageText} - ${entry.exhausted ? '已达上限' : ''} - ${entry.failureCount > 0 ? `失败 ${Math.max(0, Number(entry.failureCount) || 0)} 次` : ''} + ${entry.exhausted ? 'Đã đạt giới hạn' : ''} + ${entry.failureCount > 0 ? `Thất bại ${Math.max(0, Number(entry.failureCount) || 0)} 次` : ''}
${entry.disabledReason ? `
${helpers.escapeHtml?.(entry.disabledReason) || entry.disabledReason}
` : ''}
- + - +
`; @@ -353,7 +353,7 @@ lastAttemptAt: Math.max(0, Number(nextUsage[entry.key]?.lastAttemptAt) || 0), lastError: normalizeText(nextUsage[entry.key]?.lastError), enabled: false, - disabledReason: '手动禁用', + disabledReason: '手动Tắt', disabledAt: Date.now(), failureCount: Math.max(0, Math.floor(Number(nextUsage[entry.key]?.failureCount) || 0)), }; @@ -420,8 +420,8 @@ item.querySelector('[data-action="delete"]')?.addEventListener('click', async () => { const confirmed = await helpers.openConfirmModal?.({ title: deleteTitle, - message: `确认删除 ${entry.phone} 吗?此操作不可撤销。`, - confirmLabel: '确认删除', + message: `Xác nhận xoá ${entry.phone} 吗?此操作不可撤销。`, + confirmLabel: 'Xác nhận xoá', confirmVariant: 'btn-danger', }); if (!confirmed) return; @@ -499,7 +499,7 @@ imported.push(entry); } if (!imported.length) { - helpers.showToast?.(skippedCount > 0 ? '没有可导入的新号码(可能都重复或格式无效)。' : '没有识别到有效号码。', 'warn'); + helpers.showToast?.(skippedCount > 0 ? '没有可导入的新Số(可能都重复或格式无效)。' : '没有识别到有效Số。', 'warn'); return; } @@ -515,8 +515,8 @@ } helpers.showToast?.( skippedCount > 0 - ? `已导入 ${imported.length} 个号码,跳过 ${skippedCount} 条重复数据。` - : `已导入 ${imported.length} 个号码。`, + ? `已导入 ${imported.length} 个Số,跳过 ${skippedCount} 条重复数据。` + : `已导入 ${imported.length} 个Số。`, 'success', 2200 ); @@ -526,7 +526,7 @@ const confirmed = await helpers.openConfirmModal?.({ title: clearUsageTitle, message: clearUsageMessage, - confirmLabel: '清空次数', + confirmLabel: 'Xoá số lần', }); if (!confirmed) return; await patchPool(({ entries, usage }) => ({ @@ -548,7 +548,7 @@ const confirmed = await helpers.openConfirmModal?.({ title: deleteAllTitle, message: deleteAllMessage, - confirmLabel: '确认删除', + confirmLabel: 'Xác nhận xoá', confirmVariant: 'btn-danger', }); if (!confirmed) return; diff --git a/sidepanel/hotmail-manager.js b/sidepanel/hotmail-manager.js index a8a0ab6..b0981cd 100644 --- a/sidepanel/hotmail-manager.js +++ b/sidepanel/hotmail-manager.js @@ -35,7 +35,7 @@ return hotmailUtils.getHotmailBulkActionLabel(mode, count); } const normalizedCount = Number.isFinite(Number(count)) ? Math.max(0, Number(count)) : 0; - const prefix = mode === 'used' ? '清空已用' : '全部删除'; + const prefix = mode === 'used' ? 'Xoá sạch đã dùng' : 'Xoá tất cả'; const suffix = normalizedCount > 0 ? `(${normalizedCount})` : ''; return `${prefix}${suffix}`; } @@ -46,7 +46,7 @@ } const normalizedCount = Number.isFinite(Number(count)) ? Math.max(0, Number(count)) : 0; const suffix = normalizedCount > 0 ? `(${normalizedCount})` : ''; - return `${expanded ? '收起列表' : '展开列表'}${suffix}`; + return `${expanded ? 'Thu gọn danh sách' : 'Mở rộng danh sách'}${suffix}`; } function updateHotmailListViewport() { @@ -157,7 +157,7 @@ function formatDateTime(timestamp) { const value = Number(timestamp); if (!Number.isFinite(value) || value <= 0) { - return '未使用'; + return 'Chưa dùng'; } return new Date(value).toLocaleString('zh-CN', { hour12: false, @@ -166,20 +166,20 @@ } function getHotmailAvailabilityLabel(account) { - if (account.used) return '已用'; - return '可分配'; + if (account.used) return 'Đã dùng'; + return 'Có thể phân bổ'; } function getHotmailStatusLabel(account) { - if (account.used) return '已用'; + if (account.used) return 'Đã dùng'; switch (account.status) { case 'authorized': - return '可用'; + return 'Có thể dùng'; case 'error': - return '异常'; + return 'Lỗi'; default: - return '待校验'; + return 'Chờ kiểm tra'; } } @@ -214,7 +214,7 @@ account.status, getHotmailAvailabilityLabel(account), getHotmailStatusLabel(account), - isCurrent ? 'current 当前' : '', + isCurrent ? 'current hiện tại' : '', ].join(' ').toLowerCase(); return haystack.includes(normalizedSearchTerm); @@ -232,8 +232,8 @@ ? createAccountPoolFormController({ formShell: dom.hotmailFormShell, toggleButton: dom.btnToggleHotmailForm, - hiddenLabel: '添加账号', - visibleLabel: '取消添加', + hiddenLabel: 'Thêm tài khoản', + visibleLabel: 'Huỷ thêm', onClear: () => { clearHotmailForm(); }, @@ -254,14 +254,14 @@ const currentId = latestState?.currentHotmailAccountId || ''; if (!accounts.length) { - dom.hotmailAccountsList.innerHTML = '
还没有 Hotmail 账号,先添加一条再校验。
'; + dom.hotmailAccountsList.innerHTML = '
Chưa có tài khoản Hotmail, hãy thêm một tài khoản trước khi kiểm tra.
'; updateHotmailListViewport(); return; } const visibleAccounts = getFilteredHotmailAccounts(accounts, currentId); if (!visibleAccounts.length) { - dom.hotmailAccountsList.innerHTML = '
没有匹配当前筛选条件的 Hotmail 账号。
'; + dom.hotmailAccountsList.innerHTML = '
Không có tài khoản Hotmail nào khớp bộ lọc hiện tại.
'; updateHotmailListViewport(); return; } @@ -270,32 +270,32 @@
${helpers.escapeHtml(getHotmailStatusLabel(account))}
- 客户端 ID:${helpers.escapeHtml(account.clientId ? `${account.clientId.slice(0, 10)}...` : '未填写')} - 刷新令牌:${account.refreshToken ? '已保存' : '未保存'} + 客户端 ID:${helpers.escapeHtml(account.clientId ? `${account.clientId.slice(0, 10)}...` : 'Chưa điền')} + Refresh token: ${account.refreshToken ? 'Đã lưu' : 'Chưa lưu'} 分配状态: ${helpers.escapeHtml(getHotmailAvailabilityLabel(account))} 上次校验: ${helpers.escapeHtml(formatDateTime(account.lastAuthAt))} 上次使用: ${helpers.escapeHtml(formatDateTime(account.lastUsedAt))}
${account.lastError ? `
${helpers.escapeHtml(account.lastError)}
` : ''}
- - + + - +
`).join(''); @@ -306,16 +306,16 @@ const isUsedMode = mode === 'used'; const targetAccounts = getHotmailAccountsByUsage(isUsedMode ? 'used' : 'all'); if (!targetAccounts.length) { - helpers.showToast(isUsedMode ? '没有已用账号可清空。' : '没有可删除的 Hotmail 账号。', 'warn'); + helpers.showToast(isUsedMode ? '没有Đã dùngTài khoản可清空。' : '没有可Xoá的 Hotmail Tài khoản。', 'warn'); return; } const confirmed = await helpers.openConfirmModal({ - title: isUsedMode ? '清空已用账号' : '全部删除账号', + title: isUsedMode ? '清空Đã dùngTài khoản' : 'Tất cảXoáTài khoản', message: isUsedMode - ? `确认删除当前 ${targetAccounts.length} 个已用 Hotmail 账号吗?` - : `确认删除全部 ${targetAccounts.length} 个 Hotmail 账号吗?`, - confirmLabel: isUsedMode ? '确认清空已用' : '确认全部删除', + ? `Xác nhận xoá当前 ${targetAccounts.length} 个Đã dùng Hotmail Tài khoản吗?` + : `Xác nhận xoáTất cả ${targetAccounts.length} 个 Hotmail Tài khoản吗?`, + confirmLabel: isUsedMode ? '确认清空Đã dùng' : 'Xác nhận xoá tất cả', confirmVariant: isUsedMode ? 'btn-outline' : 'btn-danger', }); if (!confirmed) { @@ -350,8 +350,8 @@ helpers.showToast( isUsedMode - ? `已清空 ${response.deletedCount || 0} 个已用 Hotmail 账号` - : `已删除全部 ${response.deletedCount || 0} 个 Hotmail 账号`, + ? `已清空 ${response.deletedCount || 0} 个Đã dùng Hotmail Tài khoản` + : `已XoáTất cả ${response.deletedCount || 0} 个 Hotmail Tài khoản`, 'success', 2200 ); @@ -364,15 +364,15 @@ const clientId = dom.inputHotmailClientId.value.trim(); const refreshToken = dom.inputHotmailRefreshToken.value.trim(); if (!email) { - helpers.showToast('请先填写 Hotmail 邮箱。', 'warn'); + helpers.showToast('Hãy nhập trước email Hotmail.', 'warn'); return; } if (!clientId) { - helpers.showToast('请先填写微软应用客户端 ID。', 'warn'); + helpers.showToast('Hãy nhập trước client ID ứng dụng Microsoft.', 'warn'); return; } if (!refreshToken) { - helpers.showToast('请先填写刷新令牌(refresh token)。', 'warn'); + helpers.showToast('Hãy nhập trước refresh token.', 'warn'); return; } @@ -396,10 +396,10 @@ } await syncHotmailStateFromBackground(); - helpers.showToast(`已保存 Hotmail 账号 ${email}`, 'success', 1800); + helpers.showToast(`Đã lưu Hotmail Tài khoản ${email}`, 'success', 1800); formController.setVisible(false, { clearForm: true }); } catch (err) { - helpers.showToast(`保存 Hotmail 账号失败:${err.message}`, 'error'); + helpers.showToast(`保存 Hotmail Tài khoảnThất bại:${err.message}`, 'error'); } finally { actionInFlight = false; dom.btnAddHotmailAccount.disabled = false; @@ -409,19 +409,19 @@ async function handleImportHotmailAccounts() { if (actionInFlight) return; if (typeof hotmailUtils.parseHotmailImportText !== 'function') { - helpers.showToast('导入解析器未加载,请刷新扩展后重试。', 'error'); + helpers.showToast('Trình phân tích nhập chưa được tải, hãy làm mới extension rồi thử lại.', 'error'); return; } const rawText = dom.inputHotmailImport.value.trim(); if (!rawText) { - helpers.showToast('请先粘贴账号导入内容。', 'warn'); + helpers.showToast('Hãy dán trước nội dung nhập tài khoản.', 'warn'); return; } const parsedAccounts = hotmailUtils.parseHotmailImportText(rawText); if (!parsedAccounts.length) { - helpers.showToast('没有解析到有效账号,请检查格式是否为 账号----密码----ID----Token。', 'error'); + helpers.showToast('Không phân tích được tài khoản hợp lệ, hãy kiểm tra định dạng có phải là tài khoản----mật khẩu----ID----Token hay không.', 'error'); return; } @@ -445,9 +445,9 @@ await syncHotmailStateFromBackground(); dom.inputHotmailImport.value = ''; - helpers.showToast(`已导入 ${parsedAccounts.length} 条 Hotmail 账号`, 'success', 2200); + helpers.showToast(`已导入 ${parsedAccounts.length} 条 Hotmail Tài khoản`, 'success', 2200); } catch (err) { - helpers.showToast(`批量导入失败:${err.message}`, 'error'); + helpers.showToast(`批量导入Thất bại:${err.message}`, 'error'); } finally { actionInFlight = false; dom.btnImportHotmailAccounts.disabled = false; @@ -473,7 +473,7 @@ try { if (action === 'copy-email') { - if (!targetAccount?.email) throw new Error('未找到可复制的邮箱地址。'); + if (!targetAccount?.email) throw new Error('Không tìm thấy địa chỉ email có thể sao chép.'); await helpers.copyTextToClipboard(targetAccount.email); helpers.showToast(`已复制 ${targetAccount.email}`, 'success', 1800); } else if (action === 'select') { @@ -486,9 +486,9 @@ await syncHotmailStateFromBackground(); state.syncLatestState({ currentHotmailAccountId: response.account.id }); applyHotmailAccountMutation(response.account, { preserveCurrentSelection: true }); - helpers.showToast(`已切换当前 Hotmail 账号为 ${response.account.email}`, 'success', 1800); + helpers.showToast(`已切换当前 Hotmail Tài khoản为 ${response.account.email}`, 'success', 1800); } else if (action === 'toggle-used') { - if (!targetAccount) throw new Error('未找到目标 Hotmail 账号。'); + if (!targetAccount) throw new Error('Không tìm thấy tài khoản Hotmail mục tiêu.'); const response = await runtime.sendMessage({ type: 'PATCH_HOTMAIL_ACCOUNT', source: 'sidepanel', @@ -500,7 +500,7 @@ if (response?.error) throw new Error(response.error); await syncHotmailStateFromBackground(); applyHotmailAccountMutation(response.account); - helpers.showToast(`账号 ${response.account.email} 已${response.account.used ? '标记为已用' : '恢复为未用'}`, 'success', 2200); + helpers.showToast(`Tài khoản ${response.account.email} 已${response.account.used ? 'Đánh dấu là đã dùng' : 'Khôi phục về chưa dùng'}`, 'success', 2200); } else if (action === 'verify') { const response = await runtime.sendMessage({ type: 'VERIFY_HOTMAIL_ACCOUNT', @@ -510,7 +510,7 @@ if (response?.error) throw new Error(response.error); await syncHotmailStateFromBackground(); applyHotmailAccountMutation(response.account, { preserveCurrentSelection: true }); - helpers.showToast(`账号 ${response.account.email} 校验通过`, 'success', 2200); + helpers.showToast(`Tài khoản ${response.account.email} 校验通过`, 'success', 2200); } else if (action === 'test') { const response = await runtime.sendMessage({ type: 'TEST_HOTMAIL_ACCOUNT', @@ -528,13 +528,13 @@ const mailbox = response.latestMailbox ? `(${response.latestMailbox})` : ''; helpers.showToast(`最新邮件${mailbox}没有验证码:${response.latestSubject}`, 'warn', 3200); } else { - helpers.showToast('当前没有可读取的最新邮件。', 'warn', 2600); + helpers.showToast('Hiện không có email mới nhất nào có thể đọc được.', 'warn', 2600); } } else if (action === 'delete') { const confirmed = await helpers.openConfirmModal({ - title: '删除账号', - message: '确认删除这个 Hotmail 账号吗?对应 token 也会一起移除。', - confirmLabel: '确认删除', + title: 'XoáTài khoản', + message: 'Xác nhận xoá这个 Hotmail Tài khoản吗?对应 token 也会一起移除。', + confirmLabel: 'Xác nhận xoá', confirmVariant: 'btn-danger', }); if (!confirmed) { @@ -547,7 +547,7 @@ }); if (response?.error) throw new Error(response.error); await syncHotmailStateFromBackground(); - helpers.showToast('Hotmail 账号已删除', 'success', 1800); + helpers.showToast('Đã xoá tài khoản Hotmail', 'success', 1800); } } catch (err) { helpers.showToast(err.message, 'error'); @@ -572,9 +572,9 @@ dom.btnHotmailUsageGuide?.addEventListener('click', async () => { await helpers.openConfirmModal({ - title: '使用教程', - message: 'API对接模式会直接调用微软邮箱接口取件;本地助手模式仍走本地服务。两种模式继续共用同一套 Hotmail 账号池与导入格式。', - confirmLabel: '确定', + title: 'Hướng dẫn sử dụng', + message: 'Chế độ tích hợp API sẽ gọi trực tiếp giao diện email Microsoft để lấy thư; chế độ trợ lý cục bộ vẫn dùng dịch vụ local. Cả hai chế độ tiếp tục dùng chung cùng một pool tài khoản Hotmail và định dạng nhập.', + confirmLabel: 'Xác nhận', confirmVariant: 'btn-primary', }); }); diff --git a/sidepanel/icloud-manager.js b/sidepanel/icloud-manager.js index a7ebe18..7ee1786 100644 --- a/sidepanel/icloud-manager.js +++ b/sidepanel/icloud-manager.js @@ -36,9 +36,9 @@ alias.email, alias.label, alias.note, - alias.used ? '已用 used' : '未用 unused', - alias.active ? '可用 active' : '不可用 inactive', - alias.preserved ? '保留 preserved' : '', + alias.used ? 'Đã dùng used' : 'chưa dùng unused', + alias.active ? 'Có thể dùng active' : '不Có thể dùng inactive', + alias.preserved ? 'Giữ lại preserved' : '', ].join(' ').toLowerCase(); return haystack.includes(normalizedSearchTerm); @@ -98,8 +98,8 @@ host = loginUrl; } } - if (dom.icloudLoginHelpTitle) dom.icloudLoginHelpTitle.textContent = '需要登录 iCloud'; - if (dom.icloudLoginHelpText) dom.icloudLoginHelpText.textContent = `我已经为你打开 ${host}。请在那个页面完成登录,然后回到这里点击“我已登录”。`; + if (dom.icloudLoginHelpTitle) dom.icloudLoginHelpTitle.textContent = 'Cần đăng nhập iCloud'; + if (dom.icloudLoginHelpText) dom.icloudLoginHelpText.textContent = `我已经为你打开 ${host}。请在那个页面完Thành công登录,然后回到这里点击“我已登录”。`; dom.icloudLoginHelp.style.display = 'flex'; } @@ -118,8 +118,8 @@ if (!aliases.length) { selectedEmails.clear(); - dom.icloudList.innerHTML = '
未找到 iCloud Hide My Email 别名。
'; - dom.icloudSummary.textContent = '加载你的 iCloud Hide My Email 别名以便在这里管理。'; + dom.icloudList.innerHTML = '
Không tìm thấy alias iCloud Hide My Email.
'; + dom.icloudSummary.textContent = 'Tải alias iCloud Hide My Email của bạn để quản lý tại đây.'; if (dom.btnIcloudDeleteUsed) dom.btnIcloudDeleteUsed.disabled = true; updateIcloudBulkUI([]); return; @@ -127,12 +127,12 @@ const usedCount = aliases.filter((alias) => alias.used).length; const deletableUsedCount = aliases.filter((alias) => alias.used && !alias.preserved).length; - dom.icloudSummary.textContent = `已加载 ${aliases.length} 个别名,其中 ${usedCount} 个已标记为已用。`; + dom.icloudSummary.textContent = `已加载 ${aliases.length} 个别名,其中 ${usedCount} 个已Đánh dấu là đã dùng。`; if (dom.btnIcloudDeleteUsed) dom.btnIcloudDeleteUsed.disabled = deletableUsedCount === 0; const visibleAliases = getFilteredIcloudAliases(aliases); if (!visibleAliases.length) { - dom.icloudList.innerHTML = '
没有匹配当前筛选条件的别名。
'; + dom.icloudList.innerHTML = '
Không có alias nào khớp bộ lọc hiện tại.
'; updateIcloudBulkUI([]); return; } @@ -145,17 +145,17 @@
${helpers.escapeHtml(alias.email)}
- ${alias.used ? '已用' : ''} - ${!alias.used && alias.active ? '可用' : ''} - ${alias.preserved ? '保留' : ''} + ${alias.used ? 'Đã dùng' : ''} + ${!alias.used && alias.active ? 'Có thể dùng' : ''} + ${alias.preserved ? 'Giữ lại' : ''} ${alias.label ? `${helpers.escapeHtml(alias.label)}` : ''} ${alias.note ? `${helpers.escapeHtml(alias.note)}` : ''}
- - - + + +
`; @@ -188,7 +188,7 @@ return; } - if (!silent) setIcloudLoadingState(true, '正在加载 iCloud 别名...'); + if (!silent) setIcloudLoadingState(true, 'Đang tải alias iCloud...'); try { const response = await runtime.sendMessage({ type: 'LIST_ICLOUD_ALIASES', @@ -201,13 +201,13 @@ } catch (err) { selectedEmails.clear(); if (dom.icloudList) { - dom.icloudList.innerHTML = '
无法加载 iCloud 别名。
'; + dom.icloudList.innerHTML = '
Không thể tải alias iCloud.
'; } if (dom.icloudSummary) { dom.icloudSummary.textContent = err.message; } updateIcloudBulkUI([]); - if (!silent) helpers.showToast(`iCloud 别名加载失败:${err.message}`, 'error'); + if (!silent) helpers.showToast(`iCloud 别名加载Thất bại:${err.message}`, 'error'); } finally { setIcloudLoadingState(false); } @@ -224,16 +224,16 @@ async function deleteSingleIcloudAlias(alias) { const confirmed = await helpers.openConfirmModal({ - title: '删除 iCloud 别名', - message: `确认删除 ${alias.email} 吗?此操作不可撤销。`, - confirmLabel: '确认删除', + title: 'Xoá iCloud 别名', + message: `Xác nhận xoá ${alias.email} 吗?此操作不可撤销。`, + confirmLabel: 'Xác nhận xoá', confirmVariant: 'btn-danger', }); if (!confirmed) { return; } - setIcloudLoadingState(true, `正在删除 ${alias.email} ...`); + setIcloudLoadingState(true, `正在Xoá ${alias.email} ...`); try { const response = await runtime.sendMessage({ type: 'DELETE_ICLOUD_ALIAS', @@ -241,11 +241,11 @@ payload: { email: alias.email, anonymousId: alias.anonymousId }, }); if (response?.error) throw new Error(response.error); - helpers.showToast(`已删除 ${alias.email}`, 'success', 2200); + helpers.showToast(`已Xoá ${alias.email}`, 'success', 2200); await refreshIcloudAliases({ silent: true }); } catch (err) { if (dom.icloudSummary) dom.icloudSummary.textContent = err.message; - helpers.showToast(`删除 iCloud 别名失败:${err.message}`, 'error'); + helpers.showToast(`Xoá iCloud 别名Thất bại:${err.message}`, 'error'); } finally { setIcloudLoadingState(false); } @@ -260,18 +260,18 @@ payload: { email: alias.email, used }, }); if (response?.error) throw new Error(response.error); - helpers.showToast(`${alias.email} 已${used ? '标记为已用' : '恢复为未用'}`, 'success', 2200); + helpers.showToast(`${alias.email} 已${used ? 'Đánh dấu là đã dùng' : 'Khôi phục về chưa dùng'}`, 'success', 2200); await refreshIcloudAliases({ silent: true }); } catch (err) { if (dom.icloudSummary) dom.icloudSummary.textContent = err.message; - helpers.showToast(`更新 iCloud 使用状态失败:${err.message}`, 'error'); + helpers.showToast(`更新 iCloud 使用状态Thất bại:${err.message}`, 'error'); } finally { setIcloudLoadingState(false); } } async function setSingleIcloudAliasPreservedState(alias, preserved) { - setIcloudLoadingState(true, `正在更新 ${alias.email} 的保留状态...`); + setIcloudLoadingState(true, `正在更新 ${alias.email} 的Giữ lại状态...`); try { const response = await runtime.sendMessage({ type: 'SET_ICLOUD_ALIAS_PRESERVED_STATE', @@ -279,11 +279,11 @@ payload: { email: alias.email, preserved }, }); if (response?.error) throw new Error(response.error); - helpers.showToast(`${alias.email} 已${preserved ? '设为保留' : '取消保留'}`, 'success', 2200); + helpers.showToast(`${alias.email} 已${preserved ? '设为Giữ lại' : 'Bỏ giữ lại'}`, 'success', 2200); await refreshIcloudAliases({ silent: true }); } catch (err) { if (dom.icloudSummary) dom.icloudSummary.textContent = err.message; - helpers.showToast(`更新 iCloud 保留状态失败:${err.message}`, 'error'); + helpers.showToast(`更新 iCloud Giữ lại状态Thất bại:${err.message}`, 'error'); } finally { setIcloudLoadingState(false); } @@ -298,9 +298,9 @@ if (action === 'delete') { const confirmed = await helpers.openConfirmModal({ - title: '批量删除 iCloud 别名', - message: `确认删除选中的 ${selectedAliases.length} 个 iCloud 别名吗?此操作不可撤销。`, - confirmLabel: '确认删除', + title: '批量Xoá iCloud 别名', + message: `Xác nhận xoá选中的 ${selectedAliases.length} 个 iCloud 别名吗?此操作不可撤销。`, + confirmLabel: 'Xác nhận xoá', confirmVariant: 'btn-danger', }); if (!confirmed) { @@ -309,13 +309,13 @@ } const actionLabelMap = { - used: '标记已用', - unused: '标记未用', - preserve: '保留', - unpreserve: '取消保留', - delete: '删除', + used: 'Đánh dấu đã dùng', + unused: 'Đánh dấu chưa dùng', + preserve: 'Giữ lại', + unpreserve: 'Bỏ giữ lại', + delete: 'Xoá', }; - setIcloudLoadingState(true, `正在批量${actionLabelMap[action] || '处理'} iCloud 别名...`); + setIcloudLoadingState(true, `正在批量${actionLabelMap[action] || 'Xử lý'} iCloud 别名...`); try { for (const alias of selectedAliases) { @@ -346,11 +346,11 @@ } } - helpers.showToast(`已批量${actionLabelMap[action] || '处理'} ${selectedAliases.length} 个 iCloud 别名`, 'success', 2400); + helpers.showToast(`已批量${actionLabelMap[action] || 'Xử lý'} ${selectedAliases.length} 个 iCloud 别名`, 'success', 2400); await refreshIcloudAliases({ silent: true }); } catch (err) { if (dom.icloudSummary) dom.icloudSummary.textContent = err.message; - helpers.showToast(`批量处理 iCloud 别名失败:${err.message}`, 'error'); + helpers.showToast(`批量Xử lý iCloud 别名Thất bại:${err.message}`, 'error'); } finally { setIcloudLoadingState(false); updateIcloudBulkUI(); @@ -359,16 +359,16 @@ async function deleteUsedIcloudAliases() { const confirmed = await helpers.openConfirmModal({ - title: '删除已用 iCloud 别名', - message: '确认删除所有未保留的已用 iCloud 别名吗?此操作不可撤销。', - confirmLabel: '确认删除', + title: 'XoáĐã dùng iCloud 别名', + message: 'Xác nhận xoá所有未Giữ lại的Đã dùng iCloud 别名吗?此操作不可撤销。', + confirmLabel: 'Xác nhận xoá', confirmVariant: 'btn-danger', }); if (!confirmed) { return; } - setIcloudLoadingState(true, '正在删除已用 iCloud 别名...'); + setIcloudLoadingState(true, '正在XoáĐã dùng iCloud 别名...'); try { const response = await runtime.sendMessage({ type: 'DELETE_USED_ICLOUD_ALIASES', @@ -378,11 +378,11 @@ if (response?.error) throw new Error(response.error); const deleted = response?.deleted || []; const skipped = response?.skipped || []; - helpers.showToast(`已删除 ${deleted.length} 个已用别名,跳过 ${skipped.length} 个`, skipped.length ? 'warn' : 'success', 2800); + helpers.showToast(`已Xoá ${deleted.length} 个Đã dùng别名,跳过 ${skipped.length} 个`, skipped.length ? 'warn' : 'success', 2800); await refreshIcloudAliases({ silent: true }); } catch (err) { if (dom.icloudSummary) dom.icloudSummary.textContent = err.message; - helpers.showToast(`删除已用 iCloud 别名失败:${err.message}`, 'error'); + helpers.showToast(`XoáĐã dùng iCloud 别名Thất bại:${err.message}`, 'error'); } finally { setIcloudLoadingState(false); } @@ -390,10 +390,10 @@ function isLikelyIcloudLoginRequiredMessage(message = '') { const lower = String(message || '').toLowerCase(); - return lower.includes('请先在新打开的 icloud 页面中完成登录') - || lower.includes('请先在当前浏览器登录') - || lower.includes('需要先登录') - || lower.includes('请先登录') + return lower.includes('请先在新打开的 icloud 页面中完Thành công登录') + || lower.includes('Hãy đăng nhập trước trên trình duyệt hiện tại') + || lower.includes('Cần đăng nhập trước') + || lower.includes('Hãy đăng nhập trước') || lower.includes('please sign in') || lower.includes('sign in required') || lower.includes('not logged in') @@ -415,17 +415,17 @@ throw new Error(response.error); } hideIcloudLoginHelp(); - helpers.showToast('iCloud 会话已恢复,别名列表已刷新。', 'success', 2600); + helpers.showToast('Phiên iCloud đã được khôi phục, danh sách alias đã được làm mới.', 'success', 2600); await refreshIcloudAliases({ silent: true }); } catch (err) { - const errorMessage = String(err?.message || '未知错误'); + const errorMessage = String(err?.message || 'Lỗi không xác định'); if (isLikelyIcloudLoginRequiredMessage(errorMessage)) { - helpers.showToast(`看起来还没有登录完成:${errorMessage}`, 'warn', 4200); + helpers.showToast(`看起来还没有登录完Thành công:${errorMessage}`, 'warn', 4200); return; } await refreshIcloudAliases({ silent: true }).catch(() => { }); - helpers.showToast(`iCloud 会话校验失败(非登录态):${errorMessage}`, 'warn', 4200); + helpers.showToast(`iCloud 会话校验Thất bại(非登录态):${errorMessage}`, 'warn', 4200); } finally { if (dom.btnIcloudLoginDone) { dom.btnIcloudLoginDone.disabled = false; @@ -442,7 +442,7 @@ if (dom.inputIcloudSearch) dom.inputIcloudSearch.value = ''; if (dom.selectIcloudFilter) dom.selectIcloudFilter.value = 'all'; if (dom.icloudList) dom.icloudList.innerHTML = ''; - if (dom.icloudSummary) dom.icloudSummary.textContent = '加载你的 iCloud Hide My Email 别名以便在这里管理。'; + if (dom.icloudSummary) dom.icloudSummary.textContent = 'Tải alias iCloud Hide My Email của bạn để quản lý tại đây.'; updateIcloudBulkUI([]); hideIcloudLoginHelp(); } diff --git a/sidepanel/luckmail-manager.js b/sidepanel/luckmail-manager.js index 29520a9..db57042 100644 --- a/sidepanel/luckmail-manager.js +++ b/sidepanel/luckmail-manager.js @@ -40,10 +40,10 @@ purchase.email_address, purchase.project_name, purchase.tag_name, - purchase.used ? '已用 used' : '未用 unused', - purchase.preserved ? '保留 preserved' : '', - purchase.disabled ? '已禁用 disabled' : '', - purchase.reusable ? '可复用 reusable' : '', + purchase.used ? 'Đã dùng used' : 'chưa dùng unused', + purchase.preserved ? 'Giữ lại preserved' : '', + purchase.disabled ? '已Tắt disabled' : '', + purchase.reusable ? 'có thể tái dùng reusable' : '', ].join(' ').toLowerCase(); return haystack.includes(normalizedSearchTerm); @@ -104,8 +104,8 @@ if (!renderedPurchases.length) { selectedPurchaseIds.clear(); - dom.luckmailList.innerHTML = '
未找到 openai 项目的 LuckMail 邮箱。
'; - dom.luckmailSummary.textContent = '加载已购邮箱后可在这里管理 openai 项目的 LuckMail 邮箱。'; + dom.luckmailList.innerHTML = '
Không tìm thấy email LuckMail của dự án OpenAI.
'; + dom.luckmailSummary.textContent = 'Sau khi tải email đã mua, bạn có thể quản lý email LuckMail của dự án OpenAI tại đây.'; if (dom.btnLuckmailDisableUsed) dom.btnLuckmailDisableUsed.disabled = true; updateLuckmailBulkUI([]); return; @@ -114,15 +114,15 @@ const usedCount = renderedPurchases.filter((purchase) => purchase.used).length; const reusableCount = renderedPurchases.filter((purchase) => purchase.reusable).length; const disableUsedCount = renderedPurchases.filter((purchase) => purchase.used && !purchase.preserved && !purchase.disabled).length; - dom.luckmailSummary.textContent = `已加载 ${renderedPurchases.length} 个 openai 邮箱,其中 ${reusableCount} 个可复用,${usedCount} 个已本地标记为已用。`; + dom.luckmailSummary.textContent = `已加载 ${renderedPurchases.length} 个 openai 邮箱,其中 ${reusableCount} 个可复用,${usedCount} 个已本地Đánh dấu là đã dùng。`; if (dom.btnLuckmailDisableUsed) { - dom.btnLuckmailDisableUsed.textContent = `禁用已用${disableUsedCount > 0 ? `(${disableUsedCount})` : ''}`; + dom.btnLuckmailDisableUsed.textContent = `TắtĐã dùng${disableUsedCount > 0 ? `(${disableUsedCount})` : ''}`; dom.btnLuckmailDisableUsed.disabled = disableUsedCount === 0; } const visiblePurchases = getFilteredLuckmailPurchases(renderedPurchases); if (!visiblePurchases.length) { - dom.luckmailList.innerHTML = '
没有匹配当前筛选条件的 LuckMail 邮箱。
'; + dom.luckmailList.innerHTML = '
Không có email LuckMail nào khớp bộ lọc hiện tại.
'; updateLuckmailBulkUI([]); return; } @@ -135,22 +135,22 @@
- +
${helpers.escapeHtml(helpers.normalizeLuckmailProjectName(purchase.project_name) || 'openai')} - ${purchase.reusable ? '可复用' : ''} - ${purchase.current ? '当前' : ''} - ${purchase.used ? '已用' : ''} - ${purchase.preserved ? '保留' : ''} - ${purchase.disabled ? '已禁用' : ''} + ${purchase.reusable ? 'Có thể tái dùng' : ''} + ${purchase.current ? 'Hiện tại' : ''} + ${purchase.used ? 'Đã dùng' : ''} + ${purchase.preserved ? 'Giữ lại' : ''} + ${purchase.disabled ? '已Tắt' : ''} ${purchase.tag_name && normalizeLuckmailSearchText(purchase.tag_name) !== normalizeLuckmailSearchText(helpers.getLuckmailPreserveTagName()) ? `${helpers.escapeHtml(purchase.tag_name)}` : ''} @@ -162,9 +162,9 @@
- - - + + +
`; @@ -178,7 +178,7 @@ }); item.querySelector('[data-action="copy-email"]').addEventListener('click', async () => { await helpers.copyTextToClipboard(purchase.email_address || ''); - helpers.showToast('邮箱已复制', 'success', 1600); + helpers.showToast('Đã sao chép email', 'success', 1600); }); item.querySelector('[data-action="use"]').addEventListener('click', async () => { await selectSingleLuckmailPurchase(purchase); @@ -204,7 +204,7 @@ return; } - if (!silent) setLuckmailLoadingState(true, '正在加载 LuckMail openai 邮箱...'); + if (!silent) setLuckmailLoadingState(true, 'Đang tải email LuckMail OpenAI...'); try { const response = await runtime.sendMessage({ type: 'LIST_LUCKMAIL_PURCHASES', @@ -216,14 +216,14 @@ } catch (err) { selectedPurchaseIds.clear(); if (dom.luckmailList) { - dom.luckmailList.innerHTML = '
无法加载 LuckMail 邮箱列表。
'; + dom.luckmailList.innerHTML = '
Không thể tải danh sách email LuckMail.
'; } if (dom.luckmailSummary) { dom.luckmailSummary.textContent = err.message; } updateLuckmailBulkUI([]); if (!silent) { - helpers.showToast(`LuckMail 邮箱列表加载失败:${err.message}`, 'error'); + helpers.showToast(`LuckMail 邮箱列表加载Thất bại:${err.message}`, 'error'); } } finally { setLuckmailLoadingState(false); @@ -253,14 +253,14 @@ await refreshLuckmailPurchases({ silent: true }); } catch (err) { if (dom.luckmailSummary) dom.luckmailSummary.textContent = err.message; - helpers.showToast(`切换 LuckMail 邮箱失败:${err.message}`, 'error'); + helpers.showToast(`切换 LuckMail 邮箱Thất bại:${err.message}`, 'error'); } finally { setLuckmailLoadingState(false); } } async function setSingleLuckmailPurchaseUsedState(purchase, used) { - setLuckmailLoadingState(true, `正在更新 ${purchase.email_address} 的已用状态...`); + setLuckmailLoadingState(true, `正在更新 ${purchase.email_address} 的Đã dùng状态...`); try { const response = await runtime.sendMessage({ type: 'SET_LUCKMAIL_PURCHASE_USED_STATE', @@ -268,18 +268,18 @@ payload: { purchaseId: purchase.id, used }, }); if (response?.error) throw new Error(response.error); - helpers.showToast(`${purchase.email_address} 已${used ? '标记为已用' : '恢复为未用'}`, 'success', 2200); + helpers.showToast(`${purchase.email_address} 已${used ? 'Đánh dấu là đã dùng' : 'Khôi phục về chưa dùng'}`, 'success', 2200); await refreshLuckmailPurchases({ silent: true }); } catch (err) { if (dom.luckmailSummary) dom.luckmailSummary.textContent = err.message; - helpers.showToast(`更新 LuckMail 已用状态失败:${err.message}`, 'error'); + helpers.showToast(`更新 LuckMail Đã dùng状态Thất bại:${err.message}`, 'error'); } finally { setLuckmailLoadingState(false); } } async function setSingleLuckmailPurchasePreservedState(purchase, preserved) { - setLuckmailLoadingState(true, `正在更新 ${purchase.email_address} 的保留状态...`); + setLuckmailLoadingState(true, `正在更新 ${purchase.email_address} 的Giữ lại状态...`); try { const response = await runtime.sendMessage({ type: 'SET_LUCKMAIL_PURCHASE_PRESERVED_STATE', @@ -287,18 +287,18 @@ payload: { purchaseId: purchase.id, preserved }, }); if (response?.error) throw new Error(response.error); - helpers.showToast(`${purchase.email_address} 已${preserved ? '设为保留' : '取消保留'}`, 'success', 2200); + helpers.showToast(`${purchase.email_address} 已${preserved ? '设为Giữ lại' : 'Bỏ giữ lại'}`, 'success', 2200); await refreshLuckmailPurchases({ silent: true }); } catch (err) { if (dom.luckmailSummary) dom.luckmailSummary.textContent = err.message; - helpers.showToast(`更新 LuckMail 保留状态失败:${err.message}`, 'error'); + helpers.showToast(`更新 LuckMail Giữ lại状态Thất bại:${err.message}`, 'error'); } finally { setLuckmailLoadingState(false); } } async function setSingleLuckmailPurchaseDisabledState(purchase, disabled) { - setLuckmailLoadingState(true, `正在${disabled ? '禁用' : '启用'} ${purchase.email_address} ...`); + setLuckmailLoadingState(true, `正在${disabled ? 'Tắt' : 'Bật'} ${purchase.email_address} ...`); try { const response = await runtime.sendMessage({ type: 'SET_LUCKMAIL_PURCHASE_DISABLED_STATE', @@ -306,11 +306,11 @@ payload: { purchaseId: purchase.id, disabled }, }); if (response?.error) throw new Error(response.error); - helpers.showToast(`${purchase.email_address} 已${disabled ? '禁用' : '启用'}`, 'success', 2200); + helpers.showToast(`${purchase.email_address} 已${disabled ? 'Tắt' : 'Bật'}`, 'success', 2200); await refreshLuckmailPurchases({ silent: true }); } catch (err) { if (dom.luckmailSummary) dom.luckmailSummary.textContent = err.message; - helpers.showToast(`更新 LuckMail 禁用状态失败:${err.message}`, 'error'); + helpers.showToast(`更新 LuckMail Tắt状态Thất bại:${err.message}`, 'error'); } finally { setLuckmailLoadingState(false); } @@ -326,15 +326,15 @@ } const actionLabelMap = { - used: '标记已用', - unused: '标记未用', - preserve: '保留', - unpreserve: '取消保留', - disable: '禁用', - enable: '启用', + used: 'Đánh dấu đã dùng', + unused: 'Đánh dấu chưa dùng', + preserve: 'Giữ lại', + unpreserve: 'Bỏ giữ lại', + disable: 'Tắt', + enable: 'Bật', }; - setLuckmailLoadingState(true, `正在批量${actionLabelMap[action] || '处理'} LuckMail 邮箱...`); + setLuckmailLoadingState(true, `正在批量${actionLabelMap[action] || 'Xử lý'} LuckMail 邮箱...`); try { const response = await runtime.sendMessage({ type: 'BATCH_UPDATE_LUCKMAIL_PURCHASES', @@ -342,11 +342,11 @@ payload: { action, ids: selectedIds }, }); if (response?.error) throw new Error(response.error); - helpers.showToast(`已批量${actionLabelMap[action] || '处理'} ${selectedIds.length} 个 LuckMail 邮箱`, 'success', 2400); + helpers.showToast(`已批量${actionLabelMap[action] || 'Xử lý'} ${selectedIds.length} 个 LuckMail 邮箱`, 'success', 2400); await refreshLuckmailPurchases({ silent: true }); } catch (err) { if (dom.luckmailSummary) dom.luckmailSummary.textContent = err.message; - helpers.showToast(`批量处理 LuckMail 邮箱失败:${err.message}`, 'error'); + helpers.showToast(`批量Xử lý LuckMail 邮箱Thất bại:${err.message}`, 'error'); } finally { setLuckmailLoadingState(false); updateLuckmailBulkUI(); @@ -355,16 +355,16 @@ async function disableUsedLuckmailPurchases() { const confirmed = await helpers.openConfirmModal({ - title: '禁用已用 LuckMail 邮箱', - message: '确认禁用所有本地已用且未保留的 openai LuckMail 邮箱吗?', - confirmLabel: '确认禁用', + title: 'TắtĐã dùng LuckMail 邮箱', + message: '确认Tắt所有本地Đã dùng且未Giữ lại的 openai LuckMail 邮箱吗?', + confirmLabel: '确认Tắt', confirmVariant: 'btn-danger', }); if (!confirmed) { return; } - setLuckmailLoadingState(true, '正在禁用已用 LuckMail 邮箱...'); + setLuckmailLoadingState(true, '正在TắtĐã dùng LuckMail 邮箱...'); try { const response = await runtime.sendMessage({ type: 'DISABLE_USED_LUCKMAIL_PURCHASES', @@ -373,11 +373,11 @@ }); if (response?.error) throw new Error(response.error); const disabledCount = Array.isArray(response?.disabledIds) ? response.disabledIds.length : 0; - helpers.showToast(`已禁用 ${disabledCount} 个 LuckMail 邮箱`, disabledCount > 0 ? 'success' : 'info', 2400); + helpers.showToast(`已Tắt ${disabledCount} 个 LuckMail 邮箱`, disabledCount > 0 ? 'success' : 'info', 2400); await refreshLuckmailPurchases({ silent: true }); } catch (err) { if (dom.luckmailSummary) dom.luckmailSummary.textContent = err.message; - helpers.showToast(`禁用已用 LuckMail 邮箱失败:${err.message}`, 'error'); + helpers.showToast(`TắtĐã dùng LuckMail 邮箱Thất bại:${err.message}`, 'error'); } finally { setLuckmailLoadingState(false); } @@ -392,7 +392,7 @@ if (dom.inputLuckmailSearch) dom.inputLuckmailSearch.value = ''; if (dom.selectLuckmailFilter) dom.selectLuckmailFilter.value = 'all'; if (dom.luckmailList) dom.luckmailList.innerHTML = ''; - if (dom.luckmailSummary) dom.luckmailSummary.textContent = '加载已购邮箱后可在这里管理 openai 项目的 LuckMail 邮箱。'; + if (dom.luckmailSummary) dom.luckmailSummary.textContent = 'Sau khi tải email đã mua, bạn có thể quản lý email LuckMail của dự án OpenAI tại đây.'; if (dom.btnLuckmailDisableUsed) dom.btnLuckmailDisableUsed.disabled = true; updateLuckmailBulkUI([]); } diff --git a/sidepanel/mail-2925-manager.js b/sidepanel/mail-2925-manager.js index 26a2afb..f46cdfa 100644 --- a/sidepanel/mail-2925-manager.js +++ b/sidepanel/mail-2925-manager.js @@ -31,13 +31,13 @@ function updateMail2925ListViewport() { const count = getMail2925Accounts().length; if (dom.btnDeleteAllMail2925Accounts) { - dom.btnDeleteAllMail2925Accounts.textContent = `全部删除${count > 0 ? `(${count})` : ''}`; + dom.btnDeleteAllMail2925Accounts.textContent = `Xoá tất cả${count > 0 ? `(${count})` : ''}`; dom.btnDeleteAllMail2925Accounts.disabled = count === 0; } if (dom.btnToggleMail2925List) { const label = typeof mail2925Utils.getMail2925ListToggleLabel === 'function' ? mail2925Utils.getMail2925ListToggleLabel(listExpanded, count) - : `${listExpanded ? '收起列表' : '展开列表'}${count > 0 ? `(${count})` : ''}`; + : `${listExpanded ? 'Thu gọn danh sách' : 'Mở rộng danh sách'}${count > 0 ? `(${count})` : ''}`; dom.btnToggleMail2925List.textContent = label; dom.btnToggleMail2925List.setAttribute('aria-expanded', String(listExpanded)); dom.btnToggleMail2925List.disabled = count === 0; @@ -65,7 +65,7 @@ function formatDateTime(timestamp) { const value = Number(timestamp); if (!Number.isFinite(value) || value <= 0) { - return '未记录'; + return 'Chưa ghi nhận'; } return new Date(value).toLocaleString('zh-CN', { hour12: false, @@ -79,15 +79,15 @@ : 'ready'; switch (status) { case 'cooldown': - return { label: '冷却中', className: 'status-used' }; + return { label: 'Đang hồi cooldown', className: 'status-used' }; case 'disabled': - return { label: '已禁用', className: 'status-disabled' }; + return { label: '已Tắt', className: 'status-disabled' }; case 'error': - return { label: '异常', className: 'status-error' }; + return { label: 'Lỗi', className: 'status-error' }; case 'pending': - return { label: '待完善', className: 'status-pending' }; + return { label: 'Cần hoàn thiện', className: 'status-pending' }; default: - return { label: '可用', className: 'status-authorized' }; + return { label: 'Có thể dùng', className: 'status-authorized' }; } } @@ -125,7 +125,7 @@ account.email, statusKey, status.label, - isCurrent ? 'current 当前' : '', + isCurrent ? 'current hiện tại' : '', ].join(' ').toLowerCase(); return haystack.includes(normalizedSearchTerm); @@ -166,8 +166,8 @@ ? createAccountPoolFormController({ formShell: dom.mail2925FormShell, toggleButton: dom.btnToggleMail2925Form, - hiddenLabel: '添加账号', - visibleLabel: '取消添加', + hiddenLabel: 'Thêm tài khoản', + visibleLabel: 'Huỷ thêm', onClear: () => { stopEditingAccount({ clearForm: true }); }, @@ -183,7 +183,7 @@ function syncEditUi() { if (dom.btnAddMail2925Account) { - dom.btnAddMail2925Account.textContent = editingAccountId ? '保存修改' : '添加账号'; + dom.btnAddMail2925Account.textContent = editingAccountId ? 'Lưu thay đổi' : 'Thêm tài khoản'; } } @@ -213,39 +213,39 @@ const currentId = getCurrentMail2925AccountId(latestState); if (!accounts.length) { - dom.mail2925AccountsList.innerHTML = '
还没有 2925 账号,先添加一条再使用。
'; + dom.mail2925AccountsList.innerHTML = '
Chưa có tài khoản 2925, hãy thêm một tài khoản trước khi dùng.
'; updateMail2925ListViewport(); return; } const visibleAccounts = getFilteredMail2925Accounts(accounts, currentId); if (!visibleAccounts.length) { - dom.mail2925AccountsList.innerHTML = '
没有匹配当前筛选条件的 2925 账号。
'; + dom.mail2925AccountsList.innerHTML = '
Không có tài khoản 2925 nào khớp bộ lọc hiện tại.
'; updateMail2925ListViewport(); return; } dom.mail2925AccountsList.innerHTML = visibleAccounts.map((account) => { const status = getStatusSnapshot(account); - const coolingDown = status.label === '冷却中'; + const coolingDown = status.label === 'Đang hồi cooldown'; return `
${account.lastError ? `` : ''}
`; @@ -273,11 +273,11 @@ const email = String(dom.inputMail2925Email?.value || '').trim(); const password = String(dom.inputMail2925Password?.value || ''); if (!email) { - helpers.showToast('请先填写 2925 邮箱。', 'warn'); + helpers.showToast('Hãy nhập trước email 2925.', 'warn'); return; } if (!password) { - helpers.showToast('请先填写 2925 密码。', 'warn'); + helpers.showToast('Hãy nhập trước mật khẩu 2925.', 'warn'); return; } @@ -305,13 +305,13 @@ formController.setVisible(false, { clearForm: true }); helpers.showToast( updatingExisting - ? `已更新 2925 账号 ${email}` - : `已保存 2925 账号 ${email}`, + ? `已更新 2925 Tài khoản ${email}` + : `Đã lưu 2925 Tài khoản ${email}`, 'success', 1800 ); } catch (err) { - helpers.showToast(`保存 2925 账号失败:${err.message}`, 'error'); + helpers.showToast(`保存 2925 Tài khoảnThất bại:${err.message}`, 'error'); } finally { actionInFlight = false; if (dom.btnAddMail2925Account) { @@ -323,19 +323,19 @@ async function handleImportMail2925Accounts() { if (actionInFlight) return; if (typeof mail2925Utils.parseMail2925ImportText !== 'function') { - helpers.showToast('2925 导入解析器未加载,请刷新扩展后重试。', 'error'); + helpers.showToast('Trình phân tích nhập 2925 chưa được tải, hãy làm mới extension rồi thử lại.', 'error'); return; } const rawText = String(dom.inputMail2925Import?.value || '').trim(); if (!rawText) { - helpers.showToast('请先粘贴 2925 账号导入内容。', 'warn'); + helpers.showToast('Hãy dán trước nội dung nhập tài khoản 2925.', 'warn'); return; } const parsedAccounts = mail2925Utils.parseMail2925ImportText(rawText); if (!parsedAccounts.length) { - helpers.showToast('没有解析到有效账号,请检查格式是否为 邮箱----密码。', 'error'); + helpers.showToast('Không phân tích được tài khoản hợp lệ, hãy kiểm tra định dạng có phải là email----mật khẩu hay không.', 'error'); return; } @@ -359,9 +359,9 @@ if (dom.inputMail2925Import) { dom.inputMail2925Import.value = ''; } - helpers.showToast(`已导入 ${parsedAccounts.length} 条 2925 账号`, 'success', 2200); + helpers.showToast(`已导入 ${parsedAccounts.length} 条 2925 Tài khoản`, 'success', 2200); } catch (err) { - helpers.showToast(`批量导入 2925 账号失败:${err.message}`, 'error'); + helpers.showToast(`批量导入 2925 Tài khoảnThất bại:${err.message}`, 'error'); } finally { actionInFlight = false; if (dom.btnImportMail2925Accounts) { @@ -373,14 +373,14 @@ async function deleteAllMail2925Accounts() { const accounts = getMail2925Accounts(); if (!accounts.length) { - helpers.showToast('没有可删除的 2925 账号。', 'warn'); + helpers.showToast('没有可Xoá的 2925 Tài khoản。', 'warn'); return; } const confirmed = await helpers.openConfirmModal({ - title: '全部删除 2925 账号', - message: `确认删除当前全部 ${accounts.length} 个 2925 账号吗?`, - confirmLabel: '确认全部删除', + title: 'Tất cảXoá 2925 Tài khoản', + message: `Xác nhận xoá当前Tất cả ${accounts.length} 个 2925 Tài khoản吗?`, + confirmLabel: 'Xác nhận xoá tất cả', confirmVariant: 'btn-danger', }); if (!confirmed) { @@ -403,7 +403,7 @@ formController.setVisible(false, { clearForm: true }); refreshManagedAliasBaseEmail(); renderMail2925Accounts(); - helpers.showToast(`已删除全部 ${response.deletedCount || 0} 个 2925 账号`, 'success', 2200); + helpers.showToast(`已XoáTất cả ${response.deletedCount || 0} 个 2925 Tài khoản`, 'success', 2200); } async function handleAccountListClick(event) { @@ -424,7 +424,7 @@ try { if (action === 'copy-email') { - if (!targetAccount?.email) throw new Error('未找到可复制的 2925 邮箱。'); + if (!targetAccount?.email) throw new Error('Không tìm thấy email 2925 có thể sao chép.'); await helpers.copyTextToClipboard(targetAccount.email); helpers.showToast(`已复制 ${targetAccount.email}`, 'success', 1800); return; @@ -440,7 +440,7 @@ state.syncLatestState({ currentMail2925AccountId: response.account.id }); refreshManagedAliasBaseEmail(); renderMail2925Accounts(); - helpers.showToast(`已切换当前 2925 账号为 ${response.account.email}`, 'success', 2000); + helpers.showToast(`已切换当前 2925 Tài khoản为 ${response.account.email}`, 'success', 2000); return; } @@ -462,14 +462,14 @@ } if (action === 'edit') { - if (!targetAccount) throw new Error('未找到目标 2925 账号。'); + if (!targetAccount) throw new Error('Không tìm thấy tài khoản 2925 mục tiêu.'); startEditingAccount(targetAccount); - helpers.showToast(`已载入 ${targetAccount.email},修改后点“保存修改”即可`, 'info', 1800); + helpers.showToast(`已载入 ${targetAccount.email},修改后点“Lưu thay đổi”即可`, 'info', 1800); return; } if (action === 'toggle-enabled') { - if (!targetAccount) throw new Error('未找到目标 2925 账号。'); + if (!targetAccount) throw new Error('Không tìm thấy tài khoản 2925 mục tiêu.'); const response = await runtime.sendMessage({ type: 'PATCH_MAIL2925_ACCOUNT', source: 'sidepanel', @@ -482,7 +482,7 @@ }); if (response?.error) throw new Error(response.error); applyMail2925AccountMutation(response.account); - helpers.showToast(`2925 账号 ${response.account.email} 已${response.account.enabled === false ? '禁用' : '启用'}`, 'success', 2200); + helpers.showToast(`2925 Tài khoản ${response.account.email} 已${response.account.enabled === false ? 'Tắt' : 'Bật'}`, 'success', 2200); return; } @@ -500,15 +500,15 @@ }); if (response?.error) throw new Error(response.error); applyMail2925AccountMutation(response.account); - helpers.showToast(`2925 账号 ${response.account.email} 已清除冷却`, 'success', 2200); + helpers.showToast(`2925 Tài khoản ${response.account.email} 已清除冷却`, 'success', 2200); return; } if (action === 'delete') { const confirmed = await helpers.openConfirmModal({ - title: '删除 2925 账号', - message: '确认删除这个 2925 账号吗?', - confirmLabel: '确认删除', + title: 'Xoá 2925 Tài khoản', + message: 'Xác nhận xoá这个 2925 Tài khoản吗?', + confirmLabel: 'Xác nhận xoá', confirmVariant: 'btn-danger', }); if (!confirmed) { @@ -532,7 +532,7 @@ } refreshManagedAliasBaseEmail(); renderMail2925Accounts(); - helpers.showToast('2925 账号已删除', 'success', 1800); + helpers.showToast('Đã xoá tài khoản 2925', 'success', 1800); } } catch (err) { helpers.showToast(err.message, 'error'); diff --git a/sidepanel/paypal-manager.js b/sidepanel/paypal-manager.js index 15ccc72..052f77d 100644 --- a/sidepanel/paypal-manager.js +++ b/sidepanel/paypal-manager.js @@ -24,7 +24,7 @@ return ''; } return accounts.map((account) => ( - `` + `` )).join(''); } @@ -33,7 +33,7 @@ } function getPayPalAccountLabel(account = {}) { - return String(account?.email || '(未命名账号)'); + return String(account?.email || '(Tài khoản chưa đặt tên)'); } function normalizePickerPayPalAccounts(accounts = []) { @@ -66,7 +66,7 @@ current: dom.payPalAccountCurrent, menu: dom.payPalAccountMenu, emptyLabel: '', - itemLabel: '账号', + itemLabel: 'Tài khoản', normalizeItems: normalizePickerPayPalAccounts, normalizeValue: (value) => String(value || '').trim(), getItemValue: getPayPalAccountValue, @@ -138,7 +138,7 @@ }); renderPayPalAccounts(); if (!silent) { - helpers.showToast(`已切换当前 PayPal 账号为 ${response.account?.email || accountId}`, 'success', 1800); + helpers.showToast(`已切换当前 Tài khoản PayPal为 ${response.account?.email || accountId}`, 'success', 1800); } return response.account || null; } @@ -190,7 +190,7 @@ currentPayPalAccountId: payload.currentPayPalAccountId || null, }); renderPayPalAccounts(); - helpers.showToast(`已删除 PayPal 账号:${targetAccount.email || targetId}`, 'success', 1600); + helpers.showToast(`已Xoá Tài khoản PayPal:${targetAccount.email || targetId}`, 'success', 1600); } finally { actionInFlight = false; if (dom.btnAddPayPalAccount) { @@ -201,37 +201,37 @@ async function openPayPalAccountDialog() { if (typeof helpers.openFormDialog !== 'function') { - throw new Error('表单弹窗能力未加载,请刷新扩展后重试。'); + throw new Error('Khả năng popup biểu mẫu chưa được tải, hãy làm mới extension rồi thử lại.'); } return helpers.openFormDialog({ - title: '添加 PayPal 账号', - confirmLabel: '保存账号', + title: '添加 Tài khoản PayPal', + confirmLabel: 'Lưu tài khoản', confirmVariant: 'btn-primary', fields: [ { key: 'email', - label: 'PayPal 账号', + label: 'Tài khoản PayPal', type: 'text', - placeholder: '请输入 PayPal 登录邮箱', + placeholder: 'Hãy nhập email đăng nhập PayPal', autocomplete: 'username', required: true, - requiredMessage: '请先填写 PayPal 账号。', + requiredMessage: '请先填写 Tài khoản PayPal。', validate: (value) => { const normalized = String(value || '').trim(); if (!normalized.includes('@')) { - return 'PayPal 账号需填写邮箱格式。'; + return 'Tài khoản PayPal需填写邮箱格式。'; } return ''; }, }, { key: 'password', - label: 'PayPal 密码', + label: 'Mật khẩu PayPal', type: 'password', - placeholder: '请输入 PayPal 登录密码', + placeholder: 'Hãy nhập mật khẩu đăng nhập PayPal', autocomplete: 'current-password', required: true, - requiredMessage: '请先填写 PayPal 密码。', + requiredMessage: '请先填写 Mật khẩu PayPal。', }, ], }); @@ -270,9 +270,9 @@ dom.selectPayPalAccount.value = response.account.id; await syncSelectedPayPalAccount({ silent: true }); } - helpers.showToast(`已保存 PayPal 账号 ${response.account?.email || ''}`, 'success', 2200); + helpers.showToast(`Đã lưu Tài khoản PayPal ${response.account?.email || ''}`, 'success', 2200); } catch (error) { - helpers.showToast(`保存 PayPal 账号失败:${error.message}`, 'error'); + helpers.showToast(`保存 Tài khoản PayPalThất bại:${error.message}`, 'error'); throw error; } finally { actionInFlight = false; diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 26c5b2b..808c3df 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -1,10 +1,10 @@  - + - GuJumpgate V0.1.9 + GuJumpgate V0.1.9
- - + aria-label="Mở trang GitHub Releases" title="Mở trang GitHub Releases" data-i18n-aria-label="header.releases" data-i18n-title="header.releases">GuJumpgate V0.1.9 +
@@ -34,24 +34,24 @@
- - + + - +
- -
+ aria-expanded="false" data-i18n="header.config">Cấu hình
@@ -82,11 +82,9 @@ @@ -94,16 +92,16 @@
- +

- - + +
-

一定请先导出配置,再执行更新

+

Hãy xuất cấu hình trước khi cập nhật

@@ -111,81 +109,80 @@
- 注册 + Đăng ký
- 导出至 + Xuất tới
- 账号接入策略 + Chiến lược truy cập tài khoản
@@ -193,24 +190,24 @@
- +
- 管理密钥 + Khóa quản trị
- + placeholder="Nhập khóa quản trị CPA" data-i18n-placeholder="settings.adminKeyPlaceholder" /> +
- 回调方式 + Cách callback
- - + +
- + 余额未获取 @@ -604,7 +601,7 @@
- 邮箱服务 + Dịch vụ email
- 注册邮箱 + Email đăng ký
- 操作间延迟 + Độ trễ giữa thao tác
- 页面输入、选择、点击、提交、继续、授权后等待 2 秒 + Đợi 2 giây sau thao tác nhập/chọn/bấm/gửi/tiếp tục/uỷ quyền trên trang
@@ -781,7 +778,7 @@
- 无试用套餐自动重试 + Tự động thử lại khi không có gói dùng thử
- PAYPAL回调自动重试 + Tự động thử lại callback PayPal
- 拒卡重试 + Thử lại khi thẻ bị từ chối
- 授权总超时 + Tổng timeout uỷ quyền
- 登录验证码 + Mã xác minh đăng nhập 未获取
- 回调 + Callback
等待中... @@ -1713,7 +1710,7 @@
- 多选最多 10 个,按点击顺序生效。 + Chọn tối đa 10 mục, thứ tự hiệu lực theo thứ tự bạn bấm chọn.
- 多选最多 10 个,按点击顺序生效。 + Chọn tối đa 10 mục, thứ tự hiệu lực theo thứ tự bạn bấm chọn.