diff --git a/apps/vscode-extension/package.json b/apps/vscode-extension/package.json index 486fd36..cb51b0d 100644 --- a/apps/vscode-extension/package.json +++ b/apps/vscode-extension/package.json @@ -24,7 +24,7 @@ "commands": [ { "command": "extension.showWebview", - "title": "AtCoder Helper(编辑器)" + "title": "AtCoder Helper (Editor)" }, { "command": "extension.setDeeplApiKey", diff --git a/apps/vscode-extension/package.nls.zh-cn.json b/apps/vscode-extension/package.nls.zh-cn.json new file mode 100644 index 0000000..367f62e --- /dev/null +++ b/apps/vscode-extension/package.nls.zh-cn.json @@ -0,0 +1,3 @@ +{ + "extension.showWebview": "AtCoder Helper(编辑器)" +} diff --git a/apps/vscode-extension/src/extension.ts b/apps/vscode-extension/src/extension.ts index e7bdbe2..01809c9 100644 --- a/apps/vscode-extension/src/extension.ts +++ b/apps/vscode-extension/src/extension.ts @@ -5,6 +5,7 @@ import { runCommand } from "./tools/command"; import { IncomingMessage } from "./tools/types"; import { getWebviewContent } from "./tools/webview"; import { AtCoderViewProvider } from "./viewProvider"; +import { init, t } from "./tools/i18n"; let sidebarViewProvider: AtCoderViewProvider | undefined; @@ -39,27 +40,27 @@ export async function pullSubmitStatu(contest: string, taskName: string, send: ( send({ type: "statusUpdate", statuses: Object.fromEntries(statusMap) }); const status = statusMap.get(taskName); if (status && judgeStatus.has(status)) { - send({ type: "update", text: `评测结果: ${status}` }); + send({ type: "update", text: t("ext.judgeResult", { status }) }); return; } } catch { //单次轮询失败,直接下一次 } } - send({ type: "update", text: "评测超时,请稍后手动刷新查看结果" }); + send({ type: "update", text: t("ext.judgeTimeout") }); } function registerSetDeeplApiKey(context: vscode.ExtensionContext) { return vscode.commands.registerCommand("extension.setDeeplApiKey", async () => { const key = await vscode.window.showInputBox({ - prompt: "请输入 DeepL API Key", + prompt: t("ext.promptDeeplKey"), password: true, - placeHolder: "例如 xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:fx", + placeHolder: t("ext.placeholderDeeplKey"), ignoreFocusOut: true, }); if (key?.trim()) { await context.secrets.store("deeplApiKey", key.trim()); - vscode.window.showInformationMessage("DeepL API Key 已保存"); + vscode.window.showInformationMessage(t("ext.deeplKeySaved")); } }); } @@ -67,9 +68,9 @@ function registerSetDeeplApiKey(context: vscode.ExtensionContext) { function registerSetAtCoderCookie(context: vscode.ExtensionContext) { return vscode.commands.registerCommand("extension.setAtCoderCookie", async () => { const cookie = await vscode.window.showInputBox({ - prompt: "粘贴 AtCoder 的 Cookie(仅需 REVEL_SESSION)", + prompt: t("ext.promptCookie"), password: true, - placeHolder: "REVEL_SESSION=abcdef1234567890abcdef1234567890", + placeHolder: t("ext.placeholderCookie"), ignoreFocusOut: true, }); if (!cookie?.trim()) return; @@ -77,23 +78,23 @@ function registerSetAtCoderCookie(context: vscode.ExtensionContext) { if (!trimmed.startsWith("REVEL_SESSION=")) { const fix = `REVEL_SESSION=${trimmed}`; const choice = await vscode.window.showWarningMessage( - `Cookie 格式似乎不正确,是否添加 REVEL_SESSION= 前缀?`, + t("ext.cookieFormatWarn"), { modal: false }, - "自动修复", - "取消" + t("ext.autoFix"), + t("ext.cancel") ); - if (choice === "自动修复") { + if (choice === t("ext.autoFix")) { await context.secrets.store("atcoderCookie", fix); setSessionCookie(fix); notifyCookieChanged(true); - vscode.window.showInformationMessage("AtCoder Cookie 已保存并自动修复格式"); + vscode.window.showInformationMessage(t("ext.cookieSavedFixed")); } return; } await context.secrets.store("atcoderCookie", trimmed); setSessionCookie(trimmed); notifyCookieChanged(true); - vscode.window.showInformationMessage("AtCoder Cookie 已保存"); + vscode.window.showInformationMessage(t("ext.cookieSaved")); }); } @@ -168,7 +169,7 @@ export function openContestPanel(context: vscode.ExtensionContext, contest: stri export function openSubmissionPanel(context: vscode.ExtensionContext, contest: string, id: string) { const panel = vscode.window.createWebviewPanel( "atcoderSubmission", - `提交 ${id} - ${contest}`, + t("ext.submissionPanelTitle", { id, contest }), vscode.ViewColumn.One, { enableScripts: true, @@ -201,6 +202,7 @@ export function openSubmissionPanel(context: vscode.ExtensionContext, contest: s export async function activate(context: vscode.ExtensionContext) { log.info("Extension is now active!"); + init(vscode.env.language); setStaleCookieHandler(() => { // vscode.window.showWarningMessage( diff --git a/apps/vscode-extension/src/tools/SignUpContest.ts b/apps/vscode-extension/src/tools/SignUpContest.ts index 5d702d3..bee85ff 100644 --- a/apps/vscode-extension/src/tools/SignUpContest.ts +++ b/apps/vscode-extension/src/tools/SignUpContest.ts @@ -1,4 +1,5 @@ import { fetchText, fetchTextPost, CfError, LoginRequiredError, ProxyError } from "./fetch"; +import { t } from "./i18n"; export interface ContestPage { contest: string; @@ -113,20 +114,20 @@ function buildFormBody(fields: FormField[]): string { function parseRegistrationResult(html: string): RegistrationResult { const isSigned = /Unregister|registered/i.test(html); - if (isSigned) return { success: true, message: "报名成功!" }; + if (isSigned) return { success: true, message: t("register.success") }; const successMatch = html.match( /]*class="[^"]*alert-success[^"]*"[^>]*>([\s\S]*?)<\/div>/i ); if (successMatch) { const msg = successMatch[1].replace(/<[^>]+>/g, "").trim(); - return { success: true, message: msg || "报名成功!" }; + return { success: true, message: msg || t("register.success") }; } const errMatch = html.match( /]*class="[^"]*(?:alert-danger|alert-error)[^"]*"[^>]*>([\s\S]*?)<\/div>/i ); if (errMatch) { const msg = errMatch[1].replace(/<[^>]+>/g, "").trim(); - return { success: false, message: msg || "报名失败" }; + return { success: false, message: msg || t("register.failed") }; } return { success: false, message: "" }; } @@ -151,7 +152,7 @@ async function completeRatedRegistration( `]*action="[^"]*${contest}\\/rated_register"[^>]*>([\\s\\S]*?)<\\/form>`, "i" ); - const fallback: RegistrationResult = { success: false, message: "报名失败,请检查 Cookie 是否有效" }; + const fallback: RegistrationResult = { success: false, message: t("register.failedBadCookie") }; const step2Form = stepHtml.match(now)?.[1]; if (!step2Form) { const result = parseRegistrationResult(stepHtml); @@ -185,7 +186,7 @@ async function registerFormBased(contest: string, formHtml: string, rated: boole if (result.message) return result; return { success: false, - message: "报名未成功:注册页返回校验结果,请确认表单必填信息(如姓名、邮箱、居住地等)填写完整", + message: t("register.formIncomplete"), }; } return await completeRatedRegistration(contest, responseHtml, rated); @@ -196,11 +197,11 @@ export async function signedUpContest(contest: string, csrfToken: string, rated? try { const registerHtml = await fetchText(registerUrl); if (/Unregister|already registered/i.test(registerHtml)) { - return { success: true, message: "已报名" }; + return { success: true, message: t("register.alreadyDone") }; } const formHtml = extractFormHtml(registerHtml, contest); if (!formHtml) { - return { success: false, message: "报名已截止或无法获取报名信息" }; + return { success: false, message: t("register.closed") }; } const freshCsrfMatch = formHtml.match(/name="csrf_token"[^>]*value="([^"]*)"/i); const freshCsrf = freshCsrfMatch ? freshCsrfMatch[1] : csrfToken; @@ -212,6 +213,6 @@ export async function signedUpContest(contest: string, csrfToken: string, rated? if (error instanceof CfError || error instanceof LoginRequiredError || error instanceof ProxyError) { throw error; } - return { success: false, message: error instanceof Error ? error.message : "报名请求失败" }; + return { success: false, message: error instanceof Error ? error.message : t("register.requestFailed") }; } } \ No newline at end of file diff --git a/apps/vscode-extension/src/tools/command.ts b/apps/vscode-extension/src/tools/command.ts index 6f52a07..511533c 100644 --- a/apps/vscode-extension/src/tools/command.ts +++ b/apps/vscode-extension/src/tools/command.ts @@ -2,6 +2,7 @@ import * as vscode from "vscode"; import { copyMarkdown } from "./copy"; import { IncomingMessage } from "./types"; import { openContestPanel, openSubmissionPanel } from "../extension" +import { t } from "./i18n"; import { handleContestLoad, handleProblemLoad, @@ -32,7 +33,7 @@ export async function runCommand(message: IncomingMessage, context: vscode.Exten else if (problemCommands.has(message.command!)) await runProblem(message, context, sendToWebview); else if (submitCommands.has(message.command!)) await runSubmit(message, context, sendToWebview); else if (contestCommands.has(message.command!)) await runContest(message, context, sendToWebview); - else throw new Error("unknown command"); + else throw new Error(t("cmd.unknown")); } async function runContest(command: IncomingMessage, context: vscode.ExtensionContext, sendToWebview: (payload: Record) => void,): Promise { @@ -89,7 +90,7 @@ async function runProblem(command: IncomingMessage, context: vscode.ExtensionCon case "copyMarkdown": if (command.problem) { await vscode.env.clipboard.writeText(copyMarkdown(command.problem)); - sendToWebview({ type: "update", text: "已复制到剪贴板" }); + sendToWebview({ type: "update", text: t("cmd.copied") }); } return true; case "alert": @@ -127,7 +128,7 @@ async function runDeepL(command: IncomingMessage, context: vscode.ExtensionConte case "setApiKey": if (command.text?.trim()) { await context.secrets.store("deeplApiKey", command.text.trim()); - vscode.window.showInformationMessage("DeepL API Key 已保存"); + vscode.window.showInformationMessage(t("ext.deeplKeySaved")); } return true; default: diff --git a/apps/vscode-extension/src/tools/copy.ts b/apps/vscode-extension/src/tools/copy.ts index 492b36c..48e1d2e 100644 --- a/apps/vscode-extension/src/tools/copy.ts +++ b/apps/vscode-extension/src/tools/copy.ts @@ -1,4 +1,5 @@ import { AtCoderProblem } from "../atcoder"; +import { t } from "./i18n"; function decodeEntities(text: string): string { return text @@ -57,36 +58,36 @@ export function copyMarkdown(problem: AtCoderProblem): string { parts.push(""); if (problem.statement) { - parts.push("### 题目描述"); + parts.push(t("md.statement")); parts.push(htmlToText(problem.statement)); parts.push(""); } if (problem.constraints) { - parts.push("### 约束"); + parts.push(t("md.constraints")); parts.push(htmlToText(problem.constraints)); parts.push(""); } if (problem.inputFormat) { - parts.push("### 输入格式"); + parts.push(t("md.inputFormat")); parts.push(htmlToText(problem.inputFormat)); parts.push(""); } if (problem.outputFormat) { - parts.push("### 输出格式"); + parts.push(t("md.outputFormat")); parts.push(htmlToText(problem.outputFormat)); parts.push(""); } if (problem.samples && problem.samples.length > 0) { for (const sample of problem.samples) { - parts.push(`### 样例 ${sample.index}`); - parts.push("输入"); + parts.push(t("md.sample", { index: sample.index })); + parts.push(t("md.inputLabel")); parts.push("```\n" + sample.input + "\n```"); parts.push(""); - parts.push("输出"); + parts.push(t("md.outputLabel")); parts.push("```\n" + sample.output + "\n```"); parts.push(""); } diff --git a/apps/vscode-extension/src/tools/cph.ts b/apps/vscode-extension/src/tools/cph.ts index 930fc32..7a8eefc 100644 --- a/apps/vscode-extension/src/tools/cph.ts +++ b/apps/vscode-extension/src/tools/cph.ts @@ -1,5 +1,6 @@ import * as http from "http"; import { AtCoderProblem } from "../atcoder"; +import { t } from "./i18n"; export interface CphTestCase { input: string; @@ -21,10 +22,7 @@ export interface CphProblem { export class CphNotRunningError extends Error { constructor() { - super( - "未检测到 CPH 插件(localhost:27121 无响应)。\n" + - "请安装并启用 Competitive Programming Helper 扩展后重试。" - ); + super(t("cph.notRunning")); this.name = "CphNotRunningError"; } } @@ -66,7 +64,7 @@ export function sendToCph(problem: CphProblem): Promise { if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) { resolve(); } else { - reject(new Error(`CPH 返回状态码 ${res.statusCode}`)); + reject(new Error(t("cph.httpError", { status: res.statusCode }))); } }); } @@ -75,7 +73,7 @@ export function sendToCph(problem: CphProblem): Promise { if ((err as NodeJS.ErrnoException).code === "ECONNREFUSED") { reject(new CphNotRunningError()); } else { - reject(new Error(`连接 CPH 失败: ${err.message}`)); + reject(new Error(t("cph.connectionFailed", { msg: err.message }))); } }); req.write(body); diff --git a/apps/vscode-extension/src/tools/deepl.ts b/apps/vscode-extension/src/tools/deepl.ts index 5f80550..6762ffb 100644 --- a/apps/vscode-extension/src/tools/deepl.ts +++ b/apps/vscode-extension/src/tools/deepl.ts @@ -1,4 +1,5 @@ import * as https from "https"; +import { t } from "./i18n"; let freeDeeplID = 1; @@ -56,7 +57,7 @@ export async function translateTextFree(text: string, lang: string): Promise { settle(() => { if (res.statusCode && res.statusCode >= 400) { - reject(new Error(res.statusCode === 429 ? "翻译请求过于频繁,请稍后再试" : `翻译接口错误 (${res.statusCode})`)); + reject(new Error(res.statusCode === 429 ? t("deepl.tooFrequent") : t("deepl.httpError", { status: res.statusCode }))); return; } try { @@ -64,19 +65,19 @@ export async function translateTextFree(text: string, lang: string): Promise req.destroy(new Error("翻译请求超时"))); + req.setTimeout(20000, () => req.destroy(new Error(t("deepl.timeout")))); req.on("error", (err: Error) => - settle(() => reject(new Error(err.message === "翻译请求超时" ? "翻译请求超时" : `翻译请求失败: ${err.message}`))) + settle(() => reject(new Error(err.message === t("deepl.timeout") ? t("deepl.timeout") : t("deepl.failed", { msg: err.message })))) ); req.write(postData); req.end(); @@ -104,17 +105,17 @@ export function translateTextRaw(text: string, targetLang: string, apiKey: strin try { const json = JSON.parse(data); if (res.statusCode && res.statusCode >= 400) { - reject(new Error(json.message || `翻译接口错误 (${res.statusCode})`)); + reject(new Error(json.message || t("deepl.httpError", { status: res.statusCode }))); return; } resolve(json.translations?.[0]?.text ?? text); } catch { - reject(new Error("翻译接口返回异常")); + reject(new Error(t("deepl.badResponse"))); } }); } ); - req.on("error", () => reject(new Error("翻译请求失败"))); + req.on("error", () => reject(new Error(t("deepl.failedSimple")))); req.write(params.toString()); req.end(); }); diff --git a/apps/vscode-extension/src/tools/fetch.ts b/apps/vscode-extension/src/tools/fetch.ts index b3b914e..db9a9df 100644 --- a/apps/vscode-extension/src/tools/fetch.ts +++ b/apps/vscode-extension/src/tools/fetch.ts @@ -5,12 +5,13 @@ import * as zlib from "zlib"; import * as net from "net"; import * as tls from "tls"; import { SubRecord } from "./types" +import { t } from "./i18n" export class CfError extends Error { url: string; constructor(url: string) { - super(`AtCoder 触发了 Cloudflare 验证,插件无法绕过。请在浏览器中直接访问 AtCoder。\nURL: ${url}`); + super(t("err.cf", { url })); this.name = "CfError"; this.url = url; } @@ -18,14 +19,7 @@ export class CfError extends Error { export class ProxyError extends Error { constructor() { - super( - `网络代理连接失败,无法访问 AtCoder。\n` + - `可能原因:在 WSL 2 中,代理地址 127.0.0.1 指向 WSL 而非 Windows 宿主机。\n` + - `解决方案:\n` + - ` 1. 在 WSL 中执行: export NO_PROXY=.atcoder.jp\n` + - ` 2. 或设置正确的宿主机 IP: export HTTPS_PROXY=http://$(hostname).local:7897\n` + - ` 3. 或连接 Windows 宿主机的 WSL 网关 IP(查看 /etc/resolv.conf)` - ); + super(t("err.proxy")); this.name = "ProxyError"; } } @@ -33,14 +27,7 @@ export class ProxyError extends Error { export class LoginRequiredError extends Error { url: string; constructor(url: string) { - super( - `访问需要登录,请设置 AtCoder Cookie。\n` + - `获取方法:\n` + - ` 1. 在浏览器中登录 https://atcoder.jp\n` + - ` 2. 按 F12 打开开发者工具 → Application → Cookies\n` + - ` 3. 找到 atcoder.jp 下的 REVEL_SESSION,复制其 Value\n` + - ` 4. 在插件设置中输入: REVEL_SESSION=复制的值` - ); + super(t("err.login")); this.name = "LoginRequiredError"; this.url = url; } @@ -278,7 +265,7 @@ function handleResponse( reject(new CfError(url)); } else { console.log(`${logPrefix} 403 但非 CF,可能 Cookie 无效`); - reject(new Error(`访问被拒绝 (403)。Cookie 可能无效或已过期,请重新登录 AtCoder 获取新的 REVEL_SESSION`)); + reject(new Error(t("err.http403"))); } return; } @@ -291,12 +278,12 @@ function handleResponse( if (res.statusCode === 404) { if (!sessionCookie) { console.log(`${logPrefix} 404 且无 Cookie,需要登录`); - reject(new Error(`访问失败 (404)。题目不存在或需要登录,请先设置 AtCoder Cookie。`)); + reject(new Error(t("err.http404NoCookie"))); return; } const rejectCookieInvalid = () => { console.log(`${logPrefix} 404 但有 Cookie,可能 Cookie 无效`); - reject(new Error(`访问失败 (404)。Cookie 可能无效或已过期,请重新登录 AtCoder 获取新的 REVEL_SESSION`)); + reject(new Error(t("err.http404BadCookie"))); }; const contest = extractContestFromUrl(url); if (!contest) { @@ -306,7 +293,7 @@ function handleResponse( void isContestStarted(contest).then((started) => { if (started === false) { console.log(`${logPrefix} 404 且比赛未开始,题目未公开: ${url}`); - reject(new Error(`访问失败 (404)。比赛「${contest}」尚未开始,题目还未公开,请等待开赛后再试。`)); + reject(new Error(t("err.http404NotStarted", { contest }))); } else { rejectCookieInvalid(); } @@ -314,7 +301,7 @@ function handleResponse( return; } console.log(`${logPrefix} 非 200 状态码:`, res.statusCode); - reject(new Error(`Request failed with status ${res.statusCode}`)); + reject(new Error(t("err.httpStatus", { status: res.statusCode }))); return; } @@ -371,7 +358,7 @@ function fetchTextOnce(url: string, withCookie: boolean): Promise { reject(new ProxyError()); return; } - reject(new Error(`网络错误: ${err.message}`)); + reject(new Error(t("err.network", { msg: err.message }))); }); }); } @@ -413,7 +400,7 @@ export function fetchTextPost(url: string, body: string): Promise { reject(new ProxyError()); return; } - reject(new Error(`网络错误: ${err.message}`)); + reject(new Error(t("err.network", { msg: err.message }))); }); }); } diff --git a/apps/vscode-extension/src/tools/handle.ts b/apps/vscode-extension/src/tools/handle.ts index 36d040f..756240b 100644 --- a/apps/vscode-extension/src/tools/handle.ts +++ b/apps/vscode-extension/src/tools/handle.ts @@ -9,19 +9,20 @@ import { fetchStandings } from "./standings"; import { fetchHomepageContests } from "./homepage"; import { fetchSubmissionDetail } from "./submission"; import { pullSubmitStatu, notifyCookieChanged } from "../extension"; +import { t } from "./i18n"; export function handleErrorWithCfAndLogin(error: unknown, send: (payload: Record) => void): boolean { if (error instanceof CfError) { - vscode.window.showErrorMessage(error.message, "在浏览器中打开").then((choice) => { - if (choice === "在浏览器中打开") vscode.env.openExternal(vscode.Uri.parse(error.url)); + vscode.window.showErrorMessage(error.message, t("err.openInBrowser")).then((choice) => { + if (choice === t("err.openInBrowser")) vscode.env.openExternal(vscode.Uri.parse(error.url)); }); send({ type: "cf_challenge", url: error.url }); return true; } if (error instanceof ProxyError) { - const fixNoProxy = "设置 NO_PROXY"; - const fixWsl = "查看 WSL 代理说明"; - vscode.window.showErrorMessage("代理连接失败,无法访问 AtCoder", fixNoProxy, fixWsl).then((choice) => { + const fixNoProxy = t("err.setNoProxy"); + const fixWsl = t("err.viewWslDoc"); + vscode.window.showErrorMessage(t("err.proxyFailedTitle"), fixNoProxy, fixWsl).then((choice) => { if (choice === fixNoProxy) vscode.env.openExternal(vscode.Uri.parse("https://github.com/anomalyco/opencode/issues")); if (choice === fixWsl) vscode.env.openExternal(vscode.Uri.parse("https://learn.microsoft.com/zh-cn/windows/wsl/networking")); }); @@ -36,7 +37,7 @@ export function handleErrorWithCfAndLogin(error: unknown, send: (payload: Record } export async function handleContestLoad(contest: string, send: (payload: Record) => void) { - send({ type: "loading", text: `正在抓取 ${contest} 的题目列表...` }); + send({ type: "loading", text: t("load.contestTasks", { contest }) }); try { const tasks = await fetchAtCoderTasks(contest); try { @@ -48,7 +49,7 @@ export async function handleContestLoad(contest: string, send: (payload: Record< } } catch (error) { if (!handleErrorWithCfAndLogin(error, send)) { - send({ type: "error", text: error instanceof Error ? error.message : "抓取题目失败" }); + send({ type: "error", text: error instanceof Error ? error.message : t("err.contestTasks") }); } return; } @@ -76,40 +77,40 @@ export async function handleTranslate( if (translationMode === "free") { for (const [key, value] of Object.entries(texts)) { if (typeof value === "string" && value.trim()) { - send({ type: "loading", text: `正在翻译 ${key}...` }); + send({ type: "loading", text: t("load.translateItem", { name: key }) }); translated[key] = await translateTextFree(value, lang); } } } else { const apiKey = await context.secrets.get("deeplApiKey"); if (!apiKey) { - const set = "设置 API Key"; - const choice = await vscode.window.showErrorMessage("请先设置 DeepL API Key", set); + const set = t("deepl.setKey"); + const choice = await vscode.window.showErrorMessage(t("deepl.setKeyFirst"), set); if (choice === set) vscode.commands.executeCommand("extension.setDeeplApiKey"); - send({ type: "error", text: "未设置 DeepL API Key" }); + send({ type: "error", text: t("deepl.noKey") }); return; } for (const [key, value] of Object.entries(texts)) { if (typeof value === "string" && value.trim()) { - send({ type: "loading", text: `正在翻译 ${key}...` }); + send({ type: "loading", text: t("load.translateItem", { name: key }) }); translated[key] = await translateTextRaw(value, lang, apiKey); } } } send({ type: "translation", translated }); } catch (error) { - send({ type: "error", text: error instanceof Error ? error.message : "翻译失败" }); + send({ type: "error", text: error instanceof Error ? error.message : t("err.translate") }); } } export async function handleProblemLoad(contest: string, task: string, send: (payload: Record) => void) { - send({ type: "loading", text: `正在抓取 ${contest}/${task} 的题面...` }); + send({ type: "loading", text: t("load.problem", { contest, task }) }); try { const problem = await fetchAtCoderProblem(contest, task); send({ type: "problem", problem }); } catch (error) { if (!handleErrorWithCfAndLogin(error, send)) { - send({ type: "error", text: error instanceof Error ? error.message : "抓取题面失败" }); + send({ type: "error", text: error instanceof Error ? error.message : t("err.problem") }); } } } @@ -121,7 +122,7 @@ export async function handleGetCookie(context: vscode.ExtensionContext, send: (p type: "cookieStatus", hasCookie: !!storedCookie, masked, - statusMessage: storedCookie ? "✅ Cookie 已加载,可访问需要登录的题目" : "未设置 Cookie", + statusMessage: storedCookie ? t("cookie.loaded") : t("cookie.notSet"), }); } @@ -132,59 +133,59 @@ export async function handleSetCookie( ) { if (cookie) { if (!cookie.startsWith("REVEL_SESSION=")) { - send({ type: "cookieStatus", hasCookie: false, statusMessage: "❌ Cookie 格式错误,请以 REVEL_SESSION= 开头" }); + send({ type: "cookieStatus", hasCookie: false, statusMessage: t("cookie.formatError") }); return; } if (cookie.length < 20) { - send({ type: "cookieStatus", hasCookie: false, statusMessage: "❌ Cookie 值过短,请确认已完整复制 REVEL_SESSION 的值" }); + send({ type: "cookieStatus", hasCookie: false, statusMessage: t("cookie.tooShort") }); return; } await context.secrets.store("atcoderCookie", cookie); setSessionCookie(cookie); - vscode.window.showInformationMessage("AtCoder Cookie 已保存"); - send({ type: "cookieStatus", hasCookie: true, statusMessage: "✅ Cookie 保存成功" }); + vscode.window.showInformationMessage(t("cookie.saved")); + send({ type: "cookieStatus", hasCookie: true, statusMessage: t("cookie.saveSuccess") }); notifyCookieChanged(true); } else { await context.secrets.delete("atcoderCookie"); setSessionCookie(""); - send({ type: "cookieStatus", hasCookie: false, statusMessage: "Cookie 已清除" }); + send({ type: "cookieStatus", hasCookie: false, statusMessage: t("cookie.cleared") }); } } export async function handleRegistration(contest: string, rated: boolean | undefined, send: (payload: Record) => void) { - send({ type: "loading", text: `正在报名 ${contest} ...` }); + send({ type: "loading", text: t("load.register", { contest }) }); try { const page = await fetchContest(contest); if (page.signed) { - send({ type: "registrationStatus", signed: true, registrationMessage: "已报名,无需重复操作" }); + send({ type: "registrationStatus", signed: true, registrationMessage: t("register.already") }); return; } const result = await signedUpContest(contest, page.csrfToken, rated); send({ type: "registrationStatus", signed: result.success, registrationMessage: result.message }); } catch (error) { if (!handleErrorWithCfAndLogin(error, send)) { - send({ type: "registrationStatus", signed: false, registrationMessage: error instanceof Error ? error.message : "报名失败" }); + send({ type: "registrationStatus", signed: false, registrationMessage: error instanceof Error ? error.message : t("register.failed") }); } } } export async function handleFetchSubmitPage(contest: string, send: (payload: Record) => void) { - send({ type: "loading", text: `正在获取 ${contest} 提交页面信息...` }); + send({ type: "loading", text: t("load.submitPage", { contest }) }); try { const pageData = await fetchSubmitPage(contest); send({ type: "submitPage", submitTasks: pageData.tasks, languages: pageData.languages, csrfToken: pageData.csrfToken }); - send({ type: "update", text: "已获取提交页面信息" }); + send({ type: "update", text: t("submit.pageReady") }); } catch (error) { if (error instanceof CfError) { - send({ type: "submitPageError", message: "该比赛提交需要 Cloudflare 验证,插件无法自动完成。请在浏览器中打开提交页完成验证后提交。", url: error.url }); + send({ type: "submitPageError", message: t("submit.cfNeedBrowser"), url: error.url }); return; } if (error instanceof LoginRequiredError) { send({ type: "submitPageError", message: getSessionCookie() - ? "提交需要登录,请检查 AtCoder Cookie 是否有效或已过期;若提交页触发 Cloudflare 验证,请用浏览器打开完成验证。" - : "提交需要登录,请先设置 AtCoder Cookie 后再试。", + ? t("submit.loginRequired") + : t("submit.loginRequiredNoCookie"), url: `https://atcoder.jp/contests/${contest}/submit`, }); return; @@ -192,7 +193,7 @@ export async function handleFetchSubmitPage(contest: string, send: (payload: Rec if (!handleErrorWithCfAndLogin(error, send)) { send({ type: "submitPageError", - message: error instanceof Error ? error.message : "获取提交页面失败", + message: error instanceof Error ? error.message : t("err.submitPage"), url: `https://atcoder.jp/contests/${contest}/submit`, }); } @@ -207,15 +208,15 @@ export async function handleSubmitCode( send: (payload: Record) => void, ) { if (!taskScreenName || !languageId || !sourceCode) { - send({ type: "submitResult", submitResult: { success: false, message: "提交参数不完整" } }); + send({ type: "submitResult", submitResult: { success: false, message: t("submit.paramsIncomplete") } }); return; } - send({ type: "loading", text: "正在提交代码..." }); + send({ type: "loading", text: t("load.submitting") }); try { const result = await submitCodeWithRedirect(contest, taskScreenName, languageId, sourceCode); send({ type: "submitResult", submitResult: result }); if (result.success) { - send({ type: "update", text: "代码提交成功,正在获取评测结果..." }); + send({ type: "update", text: t("submit.successWaiting") }); try { await pullSubmitStatu(contest, taskScreenName!, send); } catch { @@ -229,7 +230,7 @@ export async function handleSubmitCode( } else send({ type: "error", text: result.message }); } catch (error) { if (error instanceof CfError) { - send({ type: "submitResult", submitResult: { success: false, message: "该比赛提交需要 Cloudflare 验证,插件无法自动完成。请在浏览器中打开提交页完成验证后提交。" } }); + send({ type: "submitResult", submitResult: { success: false, message: t("submit.cfNeedBrowser") } }); return; } if (error instanceof LoginRequiredError) { @@ -238,62 +239,62 @@ export async function handleSubmitCode( submitResult: { success: false, message: getSessionCookie() - ? "提交需要登录,请检查 AtCoder Cookie 是否有效或已过期;若提交页触发 Cloudflare 验证,请用浏览器打开完成验证。" - : "提交需要登录,请先设置 AtCoder Cookie 后再试。", + ? t("submit.loginRequired") + : t("submit.loginRequiredNoCookie"), }, }); return; } if (!handleErrorWithCfAndLogin(error, send)) { - send({ type: "submitResult", submitResult: { success: false, message: error instanceof Error ? error.message : "提交失败" } }); + send({ type: "submitResult", submitResult: { success: false, message: error instanceof Error ? error.message : t("err.submit") } }); } } } export async function handleFetchSubHistory(contest: string, send: (payload: Record) => void) { - send({ type: "loading", text: `正在获取 ${contest} 提交记录...` }); + send({ type: "loading", text: t("load.subHistory", { contest }) }); try { const submissions = await fetchSubmitHistory(contest); send({ type: "submissionHistory", submissions }); } catch (error) { if (!handleErrorWithCfAndLogin(error, send)) { - send({ type: "error", text: error instanceof Error ? error.message : "获取提交记录失败" }); + send({ type: "error", text: error instanceof Error ? error.message : t("err.subHistory") }); } } } export async function handleFetchSubmissionDetail(contest: string, id: string, send: (payload: Record) => void) { - send({ type: "loading", text: `正在获取提交 ${id} 的详细信息...` }); + send({ type: "loading", text: t("load.subDetail", { id }) }); try { const detail = await fetchSubmissionDetail(contest, id); send({ type: "submissionDetail", submissionDetail: detail }); } catch (error) { if (!handleErrorWithCfAndLogin(error, send)) { - send({ type: "error", text: error instanceof Error ? error.message : "获取提交详情失败" }); + send({ type: "error", text: error instanceof Error ? error.message : t("err.subDetail") }); } } } export async function handleFetchStandings(contest: string, send: (payload: Record) => void) { - send({ type: "loading", text: `正在获取 ${contest} 排行榜...` }); + send({ type: "loading", text: t("load.standings", { contest }) }); try { const standings = await fetchStandings(contest); send({ type: "standings", contest, standings }); } catch (error) { if (!handleErrorWithCfAndLogin(error, send)) { - send({ type: "error", text: error instanceof Error ? error.message : "获取排行榜失败" }); + send({ type: "error", text: error instanceof Error ? error.message : t("err.standings") }); } } } export async function handleGetContests(send: (payload: Record) => void) { - send({ type: "loading", text: "正在抓取 AtCoder 首页比赛列表..." }); + send({ type: "loading", text: t("load.homepage") }); try { const contests = await fetchHomepageContests(); send({ type: "contestList", contests }); } catch (error) { if (!handleErrorWithCfAndLogin(error, send)) { - send({ type: "error", text: error instanceof Error ? error.message : "获取比赛列表失败" }); + send({ type: "error", text: error instanceof Error ? error.message : t("err.homepage") }); } } } @@ -302,9 +303,9 @@ export async function handleExportToCph(problem: AtCoderProblem, send: (payload: try { const payload = buildCphProblem(problem); await sendToCph(payload); - send({ type: "cphExportResult", success: true, message: "success send to cph" }); + send({ type: "cphExportResult", success: true, message: t("cph.exportSuccess") }); } catch (error) { - const message = error instanceof Error ? error.message : "fail to send cph"; + const message = error instanceof Error ? error.message : t("cph.exportFailed"); send({ type: "cphExportResult", success: false, message: message }); } } \ No newline at end of file diff --git a/apps/vscode-extension/src/tools/i18n/en.ts b/apps/vscode-extension/src/tools/i18n/en.ts new file mode 100644 index 0000000..43c203b --- /dev/null +++ b/apps/vscode-extension/src/tools/i18n/en.ts @@ -0,0 +1,120 @@ +import type { ZhDict } from "./zh"; + +export const en: Record = { + "ext.judgeResult": "Judge result: {status}", + "ext.judgeTimeout": "Judging timed out, please refresh later to check the result", + "ext.promptDeeplKey": "Enter DeepL API Key", + "ext.placeholderDeeplKey": "e.g. xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:fx", + "ext.deeplKeySaved": "DeepL API Key saved", + "ext.promptCookie": "Paste your AtCoder Cookie (REVEL_SESSION only)", + "ext.placeholderCookie": "REVEL_SESSION=abcdef1234567890abcdef1234567890", + "ext.cookieFormatWarn": "The Cookie format looks incorrect. Add the REVEL_SESSION= prefix?", + "ext.autoFix": "Auto fix", + "ext.cancel": "Cancel", + "ext.cookieSavedFixed": "AtCoder Cookie saved with auto-fixed format", + "ext.cookieSaved": "AtCoder Cookie saved", + "ext.submissionPanelTitle": "Submission {id} - {contest}", + + "cmd.copied": "Copied to clipboard", + "cmd.unknown": "Unknown command", + + "err.openInBrowser": "Open in Browser", + "err.proxyFailedTitle": "Proxy connection failed, cannot reach AtCoder", + "err.setNoProxy": "Set NO_PROXY", + "err.viewWslDoc": "View WSL proxy docs", + "load.contestTasks": "Fetching task list for {contest}...", + "err.contestTasks": "Failed to fetch task list", + "err.standingsParse": "Failed to parse standings data: {reason}", + "load.translateItem": "Translating {name}...", + "deepl.setKeyFirst": "Please set a DeepL API Key first", + "deepl.setKey": "Set API Key", + "deepl.noKey": "DeepL API Key is not set", + "err.translate": "Translation failed", + "load.problem": "Fetching statement for {contest}/{task}...", + "err.problem": "Failed to fetch statement", + "cookie.loaded": "✅ Cookie loaded, can access login-required problems", + "cookie.notSet": "Cookie not set", + "cookie.formatError": "❌ Invalid Cookie format, it must start with REVEL_SESSION=", + "cookie.tooShort": "❌ Cookie value is too short, please make sure you copied the full REVEL_SESSION value", + "cookie.saved": "AtCoder Cookie saved", + "cookie.saveSuccess": "✅ Cookie saved", + "cookie.cleared": "Cookie cleared", + "load.register": "Registering for {contest} ...", + "register.already": "Already registered, no need to register again", + "register.failed": "Registration failed", + "load.submitPage": "Fetching submit page for {contest}...", + "submit.pageReady": "Submit page loaded", + "submit.cfNeedBrowser": "Submitting to this contest requires Cloudflare verification, which cannot be automated. Please open the submit page in your browser to complete verification before submitting.", + "submit.loginRequired": "Submission requires login. Please check whether your AtCoder Cookie is valid or expired; if the submit page triggers Cloudflare verification, open it in your browser to complete it.", + "submit.loginRequiredNoCookie": "Submission requires login. Please set your AtCoder Cookie first.", + "err.submitPage": "Failed to fetch submit page", + "submit.paramsIncomplete": "Incomplete submission parameters", + "load.submitting": "Submitting code...", + "submit.successWaiting": "Code submitted, fetching judge result...", + "err.submit": "Submission failed", + "load.subHistory": "Fetching submission history for {contest}...", + "err.subHistory": "Failed to fetch submission history", + "load.subDetail": "Fetching details for submission {id}...", + "err.subDetail": "Failed to fetch submission details", + "load.standings": "Fetching standings for {contest}...", + "err.standings": "Failed to fetch standings", + "load.homepage": "Fetching AtCoder homepage contest list...", + "err.homepage": "Failed to fetch contest list", + "cph.exportSuccess": "success send to cph", + "cph.exportFailed": "fail to send cph", + + "err.cf": "AtCoder triggered a Cloudflare verification that cannot be bypassed. Please open AtCoder in your browser.\nURL: {url}", + "err.proxy": + "Network proxy connection failed, cannot reach AtCoder.\n" + + "Possible cause: In WSL 2, the proxy address 127.0.0.1 points to WSL itself, not the Windows host.\n" + + "Solutions:\n" + + " 1. Run in WSL: export NO_PROXY=.atcoder.jp\n" + + " 2. Or set the correct host IP: export HTTPS_PROXY=http://$(hostname).local:7897\n" + + " 3. Or connect to the Windows host via the WSL gateway IP (see /etc/resolv.conf)", + "err.login": + "Login is required, please set your AtCoder Cookie.\n" + + "How to get it:\n" + + " 1. Sign in at https://atcoder.jp in your browser\n" + + " 2. Press F12 to open DevTools → Application → Cookies\n" + + " 3. Find REVEL_SESSION under atcoder.jp and copy its Value\n" + + " 4. Enter it in the extension settings as: REVEL_SESSION=", + "err.http403": "Access denied (403). The Cookie may be invalid or expired. Please sign in to AtCoder again and get a fresh REVEL_SESSION", + "err.http404NoCookie": "Request failed (404). The problem may not exist or requires login. Please set your AtCoder Cookie first.", + "err.http404BadCookie": "Request failed (404). The Cookie may be invalid or expired. Please sign in to AtCoder again and get a fresh REVEL_SESSION", + "err.http404NotStarted": "Request failed (404). The contest \"{contest}\" has not started yet; the problems are not public. Please wait until it begins.", + "err.httpStatus": "Request failed with status {status}", + "err.network": "Network error: {msg}", + + "deepl.tooFrequent": "Too many translation requests, please try again later", + "deepl.httpError": "Translation API error ({status})", + "deepl.badResponse": "Translation API returned an unexpected response", + "deepl.timeout": "Translation request timed out", + "deepl.failed": "Translation request failed: {msg}", + "deepl.failedSimple": "Translation request failed", + + "submit.pageFetchFailed": "Cannot fetch the submit page: the contest may have ended, or the submit page structure has changed.", + "submit.noCsrf": "Cannot get CSRF Token, please check whether the Cookie is valid", + "submit.success": "Code submitted successfully", + "submit.failed": "Submission failed, please check whether the Cookie is valid", + + "register.success": "Registered successfully!", + "register.failedBadCookie": "Registration failed, please check whether the Cookie is valid", + "register.formIncomplete": "Registration was not successful: the register page returned a validation result. Please make sure all required fields (e.g. name, email, location) are filled in.", + "register.alreadyDone": "Already registered", + "register.closed": "Registration is closed or the registration info could not be fetched", + "register.requestFailed": "Registration request failed", + + "md.statement": "### Problem Statement", + "md.constraints": "### Constraints", + "md.inputFormat": "### Input Format", + "md.outputFormat": "### Output Format", + "md.sample": "### Sample {index}", + "md.inputLabel": "Input", + "md.outputLabel": "Output", + + "cph.notRunning": + "CPH extension not detected (no response on localhost:27121).\n" + + "Please install and enable the Competitive Programming Helper extension and try again.", + "cph.httpError": "CPH returned status code {status}", + "cph.connectionFailed": "Failed to connect to CPH: {msg}", +}; diff --git a/apps/vscode-extension/src/tools/i18n/index.ts b/apps/vscode-extension/src/tools/i18n/index.ts new file mode 100644 index 0000000..1341c03 --- /dev/null +++ b/apps/vscode-extension/src/tools/i18n/index.ts @@ -0,0 +1,28 @@ +import { zh } from "./zh"; +import { en } from "./en"; +import type { ZhDict } from "./zh"; + +type Dict = Record; + +const dicts: Record = { zh, en }; +const fallback: Dict = en; + +let currentLang = "en"; + +export function init(language: string): void { + const lang = (language || "en").toLowerCase(); + currentLang = lang.startsWith("zh") ? "zh" : "en"; +} + +export type I18nKey = keyof ZhDict; + +export function t(key: I18nKey, params?: Record): string { + const dict = dicts[currentLang] ?? fallback; + let text = dict[key] ?? fallback[key] ?? key; + if (params) { + for (const [k, v] of Object.entries(params)) { + text = text.replace(new RegExp(`\\{${k}\\}`, "g"), String(v)); + } + } + return text; +} diff --git a/apps/vscode-extension/src/tools/i18n/zh.ts b/apps/vscode-extension/src/tools/i18n/zh.ts new file mode 100644 index 0000000..b73fcfe --- /dev/null +++ b/apps/vscode-extension/src/tools/i18n/zh.ts @@ -0,0 +1,120 @@ +export const zh = { + "ext.judgeResult": "评测结果: {status}", + "ext.judgeTimeout": "评测超时,请稍后手动刷新查看结果", + "ext.promptDeeplKey": "请输入 DeepL API Key", + "ext.placeholderDeeplKey": "例如 xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:fx", + "ext.deeplKeySaved": "DeepL API Key 已保存", + "ext.promptCookie": "粘贴 AtCoder 的 Cookie(仅需 REVEL_SESSION)", + "ext.placeholderCookie": "REVEL_SESSION=abcdef1234567890abcdef1234567890", + "ext.cookieFormatWarn": "Cookie 格式似乎不正确,是否添加 REVEL_SESSION= 前缀?", + "ext.autoFix": "自动修复", + "ext.cancel": "取消", + "ext.cookieSavedFixed": "AtCoder Cookie 已保存并自动修复格式", + "ext.cookieSaved": "AtCoder Cookie 已保存", + "ext.submissionPanelTitle": "提交 {id} - {contest}", + + "cmd.copied": "已复制到剪贴板", + "cmd.unknown": "未知命令", + + "err.openInBrowser": "在浏览器中打开", + "err.proxyFailedTitle": "代理连接失败,无法访问 AtCoder", + "err.setNoProxy": "设置 NO_PROXY", + "err.viewWslDoc": "查看 WSL 代理说明", + "load.contestTasks": "正在抓取 {contest} 的题目列表...", + "err.contestTasks": "抓取题目失败", + "err.standingsParse": "排行榜数据解析失败: {reason}", + "load.translateItem": "正在翻译 {name}...", + "deepl.setKeyFirst": "请先设置 DeepL API Key", + "deepl.setKey": "设置 API Key", + "deepl.noKey": "未设置 DeepL API Key", + "err.translate": "翻译失败", + "load.problem": "正在抓取 {contest}/{task} 的题面...", + "err.problem": "抓取题面失败", + "cookie.loaded": "✅ Cookie 已加载,可访问需要登录的题目", + "cookie.notSet": "未设置 Cookie", + "cookie.formatError": "❌ Cookie 格式错误,请以 REVEL_SESSION= 开头", + "cookie.tooShort": "❌ Cookie 值过短,请确认已完整复制 REVEL_SESSION 的值", + "cookie.saved": "AtCoder Cookie 已保存", + "cookie.saveSuccess": "✅ Cookie 保存成功", + "cookie.cleared": "Cookie 已清除", + "load.register": "正在报名 {contest} ...", + "register.already": "已报名,无需重复操作", + "register.failed": "报名失败", + "load.submitPage": "正在获取 {contest} 提交页面信息...", + "submit.pageReady": "已获取提交页面信息", + "submit.cfNeedBrowser": "该比赛提交需要 Cloudflare 验证,插件无法自动完成。请在浏览器中打开提交页完成验证后提交。", + "submit.loginRequired": "提交需要登录,请检查 AtCoder Cookie 是否有效或已过期;若提交页触发 Cloudflare 验证,请用浏览器打开完成验证。", + "submit.loginRequiredNoCookie": "提交需要登录,请先设置 AtCoder Cookie 后再试。", + "err.submitPage": "获取提交页面失败", + "submit.paramsIncomplete": "提交参数不完整", + "load.submitting": "正在提交代码...", + "submit.successWaiting": "代码提交成功,正在获取评测结果...", + "err.submit": "提交失败", + "load.subHistory": "正在获取 {contest} 提交记录...", + "err.subHistory": "获取提交记录失败", + "load.subDetail": "正在获取提交 {id} 的详细信息...", + "err.subDetail": "获取提交详情失败", + "load.standings": "正在获取 {contest} 排行榜...", + "err.standings": "获取排行榜失败", + "load.homepage": "正在抓取 AtCoder 首页比赛列表...", + "err.homepage": "获取比赛列表失败", + "cph.exportSuccess": "success send to cph", + "cph.exportFailed": "fail to send cph", + + "err.cf": "AtCoder 触发了 Cloudflare 验证,插件无法绕过。请在浏览器中直接访问 AtCoder。\nURL: {url}", + "err.proxy": + "网络代理连接失败,无法访问 AtCoder。\n" + + "可能原因:在 WSL 2 中,代理地址 127.0.0.1 指向 WSL 而非 Windows 宿主机。\n" + + "解决方案:\n" + + " 1. 在 WSL 中执行: export NO_PROXY=.atcoder.jp\n" + + " 2. 或设置正确的宿主机 IP: export HTTPS_PROXY=http://$(hostname).local:7897\n" + + " 3. 或连接 Windows 宿主机的 WSL 网关 IP(查看 /etc/resolv.conf)", + "err.login": + "访问需要登录,请设置 AtCoder Cookie。\n" + + "获取方法:\n" + + " 1. 在浏览器中登录 https://atcoder.jp\n" + + " 2. 按 F12 打开开发者工具 → Application → Cookies\n" + + " 3. 找到 atcoder.jp 下的 REVEL_SESSION,复制其 Value\n" + + " 4. 在插件设置中输入: REVEL_SESSION=复制的值", + "err.http403": "访问被拒绝 (403)。Cookie 可能无效或已过期,请重新登录 AtCoder 获取新的 REVEL_SESSION", + "err.http404NoCookie": "访问失败 (404)。题目不存在或需要登录,请先设置 AtCoder Cookie。", + "err.http404BadCookie": "访问失败 (404)。Cookie 可能无效或已过期,请重新登录 AtCoder 获取新的 REVEL_SESSION", + "err.http404NotStarted": "访问失败 (404)。比赛「{contest}」尚未开始,题目还未公开,请等待开赛后再试。", + "err.httpStatus": "Request failed with status {status}", + "err.network": "网络错误: {msg}", + + "deepl.tooFrequent": "翻译请求过于频繁,请稍后再试", + "deepl.httpError": "翻译接口错误 ({status})", + "deepl.badResponse": "翻译接口返回异常", + "deepl.timeout": "翻译请求超时", + "deepl.failed": "翻译请求失败: {msg}", + "deepl.failedSimple": "翻译请求失败", + + "submit.pageFetchFailed": "无法获取提交页面:比赛可能已结束,或提交页面结构发生了变化。", + "submit.noCsrf": "无法获取 CSRF Token,请检查 Cookie 是否有效", + "submit.success": "代码提交成功", + "submit.failed": "提交失败,请检查 Cookie 是否有效", + + "register.success": "报名成功!", + "register.failedBadCookie": "报名失败,请检查 Cookie 是否有效", + "register.formIncomplete": "报名未成功:注册页返回校验结果,请确认表单必填信息(如姓名、邮箱、居住地等)填写完整", + "register.alreadyDone": "已报名", + "register.closed": "报名已截止或无法获取报名信息", + "register.requestFailed": "报名请求失败", + + "md.statement": "### 题目描述", + "md.constraints": "### 约束", + "md.inputFormat": "### 输入格式", + "md.outputFormat": "### 输出格式", + "md.sample": "### 样例 {index}", + "md.inputLabel": "输入", + "md.outputLabel": "输出", + + "cph.notRunning": + "未检测到 CPH 插件(localhost:27121 无响应)。\n" + + "请安装并启用 Competitive Programming Helper 扩展后重试。", + "cph.httpError": "CPH 返回状态码 {status}", + "cph.connectionFailed": "连接 CPH 失败: {msg}", +}; + +export type ZhDict = typeof zh; diff --git a/apps/vscode-extension/src/tools/standings.ts b/apps/vscode-extension/src/tools/standings.ts index fe22297..e0fe1ae 100644 --- a/apps/vscode-extension/src/tools/standings.ts +++ b/apps/vscode-extension/src/tools/standings.ts @@ -1,4 +1,5 @@ import { fetchText } from "./fetch"; +import { t } from "./i18n"; export interface Standing { rank: number; @@ -22,7 +23,7 @@ export async function fetchStandings(contest: string, limit = 100): Promise ({ diff --git a/apps/vscode-extension/src/tools/submit.ts b/apps/vscode-extension/src/tools/submit.ts index a9b5e50..2f04e0a 100644 --- a/apps/vscode-extension/src/tools/submit.ts +++ b/apps/vscode-extension/src/tools/submit.ts @@ -1,4 +1,5 @@ import { fetchText, fetchTextPost, CfError } from "./fetch"; +import { t } from "./i18n"; export interface LanguageOption { id: string; @@ -85,7 +86,7 @@ export async function fetchSubmitPage(contest: string): Promise { } if (!csrfToken || (tasks.length === 0 && langOptions.length === 0)) { - throw new Error("无法获取提交页面:比赛可能已结束,或提交页面结构发生了变化。"); + throw new Error(t("submit.pageFetchFailed")); } const seen = new Set(); @@ -129,18 +130,18 @@ export async function submitCode(contest: string, taskScreenName: string, langua if (responseHtml.includes("/submissions/me") || responseHtml.includes("Submission")) { return { success: true, - message: "代码提交成功", + message: t("submit.success"), url: `https://atcoder.jp/contests/${contest}/submissions/me`, }; } - return { success: false, message: "提交失败,请检查 Cookie 是否有效" }; + return { success: false, message: t("submit.failed") }; } export async function submitCodeWithRedirect(contest: string, taskScreenName: string, languageId: string, sourceCode: string): Promise { const pageData = await fetchSubmitPage(contest); if (!pageData.csrfToken) { - return { success: false, message: "无法获取 CSRF Token,请检查 Cookie 是否有效" }; + return { success: false, message: t("submit.noCsrf") }; } return await submitCode(contest, taskScreenName, languageId, sourceCode, pageData.csrfToken); } diff --git a/apps/vscode-extension/src/tools/webview.ts b/apps/vscode-extension/src/tools/webview.ts index 904f3a8..6aa9652 100644 --- a/apps/vscode-extension/src/tools/webview.ts +++ b/apps/vscode-extension/src/tools/webview.ts @@ -18,6 +18,7 @@ export function getWebviewContent( if (initSubmissionId) { globals.push(`window.__ATCODER_SUBMISSION_ID__ = ${JSON.stringify(initSubmissionId)};`); } + globals.push(`window.__ATCODER_LOCALE__ = ${JSON.stringify(vscode.env.language)};`); const initScript = globals.length > 0 ? `` : ""; return ` diff --git a/packages/webview/src/ContestApp.tsx b/packages/webview/src/ContestApp.tsx index 1005ec4..294891b 100644 --- a/packages/webview/src/ContestApp.tsx +++ b/packages/webview/src/ContestApp.tsx @@ -1,6 +1,7 @@ import React from "react"; import { Button, Card, Spinner } from "@template/ui"; import { useVSCode } from "./VSCodeProvider"; +import { useI18n } from "./i18n"; import type { ContestProblem, SampleCase, Standing, SubmissionRecord, WebviewMessage } from "./types"; import { HtmlContent, TranslatedBlock } from "./components/HtmlContent"; @@ -28,10 +29,10 @@ interface TaskItem { } const TABS: Array<{ key: Tab; label: string }> = [ - { key: "info", label: "信息" }, - { key: "task", label: "题目" }, - { key: "submit", label: "提交" }, - { key: "rating", label: "排行" }, + { key: "info", label: "ui.tabInfo" }, + { key: "task", label: "ui.task" }, + { key: "submit", label: "ui.submit" }, + { key: "rating", label: "ui.tabRating" }, ]; const statusColor = (status: string): string => { @@ -60,6 +61,7 @@ const formatSubmitTime = (time: string): string => { const ContestApp: React.FC = ({ initContest = "" }) => { const vscode = useVSCode(); + const { t } = useI18n(); const contest = initContest; const [activeTab, setActiveTab] = React.useState("task"); @@ -102,38 +104,38 @@ const ContestApp: React.FC = ({ initContest = "" }) => { const loadProblem = (task: string) => { setIsLoading(true); - setStatus(`正在抓取 ${contest}/${task} 的题面...`); + setStatus(t("status.fetchingProblem", { contest, task })); setTranslated(translatedCache[task] ?? null); vscode.postMessage({ command: "loadProblem", contest, task }); }; const handleRegister = () => { setRegistrationMessage(null); - setStatus(`正在报名 ${contest} ...`); + setStatus(t("status.registration", { contest })); vscode.postMessage({ command: "registerContest", contest, rated: isRated }); }; const doTranslate = () => { if (!problem) return; setTranslating(true); - setStatus("正在翻译..."); + setStatus(t("status.translating")); const texts: Record = {}; - if (problem.statement) texts["题目描述"] = problem.statement; - if (problem.constraints) texts["约束"] = problem.constraints; - if (problem.inputFormat) texts["输入格式"] = problem.inputFormat; - if (problem.outputFormat) texts["输出格式"] = problem.outputFormat; + if (problem.statement) texts[t("text.problemStatement")] = problem.statement; + if (problem.constraints) texts[t("text.constraints")] = problem.constraints; + if (problem.inputFormat) texts[t("text.inputFormat")] = problem.inputFormat; + if (problem.outputFormat) texts[t("text.outputFormat")] = problem.outputFormat; vscode.postMessage({ command: "translate", payload: texts, targetLang: "ZH", translationMode }); }; const doCopyMarkdown = () => { if (!problem) return; vscode.postMessage({ command: "copyMarkdown", problem }); - setStatus("正在复制..."); + setStatus(t("status.copying")); }; const doExportToCph = () => { if (!problem) return; - setStatus("正在导出到 CPH..."); + setStatus(t("status.exportingCph")); vscode.postMessage({ command: "sendCph", problem }); }; @@ -143,14 +145,14 @@ const ContestApp: React.FC = ({ initContest = "" }) => { setSubmitTasks([]); setSubmitLanguages([]); setSourceCode(""); - setStatus("正在获取提交页面..."); + setStatus(t("status.fetchingSubmitPage")); vscode.postMessage({ command: "fetchSubmitPage", contest }); }; const handleSubmitCode = () => { if (!selectedSubmitTask || !selectedSubmitLanguage || !sourceCode.trim()) return; setSubmitResult(null); - setStatus("正在提交代码..."); + setStatus(t("status.submitting")); vscode.postMessage({ command: "submitCode", contest, @@ -162,7 +164,7 @@ const ContestApp: React.FC = ({ initContest = "" }) => { const handleFetchHistory = () => { setLoadingHistory(true); - setStatus(`正在获取 ${contest} 提交记录...`); + setStatus(t("status.fetchingHistory", { contest })); vscode.postMessage({ command: "fetchSubmissionHistory", contest }); }; @@ -170,7 +172,7 @@ const ContestApp: React.FC = ({ initContest = "" }) => { setActiveTab(tab); if (tab === "rating" && !standingsCache.current[contest]) { setLoadingStandings(true); - setStatus(`正在获取 ${contest} 排行榜...`); + setStatus(t("status.fetchingStandings", { contest })); vscode.postMessage({ command: "fetchStandings", contest }); } }; @@ -181,7 +183,7 @@ const ContestApp: React.FC = ({ initContest = "" }) => { React.useEffect(() => { setIsLoading(true); - setStatus(`正在加载 ${contest} 的题目...`); + setStatus(t("status.loadingContest", { contest })); vscode.postMessage({ command: "loadContest", contest }); vscode.postMessage({ command: "getCookie" }); }, []); @@ -193,7 +195,7 @@ const ContestApp: React.FC = ({ initContest = "" }) => { setTasks(message.tasks ?? []); setSelectedTask(""); setProblem(null); - setStatus(`已加载 ${(message.tasks ?? []).length} 道题目`); + setStatus(t("status.tasksLoaded", { count: (message.tasks ?? []).length })); setIsLoading(false); } if (message.type === "contestInfo") { @@ -204,22 +206,22 @@ const ContestApp: React.FC = ({ initContest = "" }) => { if (message.type === "cf_challenge") { setCfUrl(message.url ?? null); setIsLoading(false); - setStatus("AtCoder 触发 Cloudflare 验证,插件无法直接访问,请在浏览器中完成验证"); + setStatus(t("status.cfChallenge")); } if (message.type === "loginRequired") { setIsLoading(false); - setStatus("需要登录 AtCoder 账号,请在侧边栏设置 Cookie 后再试"); + setStatus(t("status.loginRequiredSidebar")); } if (message.type === "problem") { setProblem(message.problem ?? null); - setStatus(`已加载题面:${message.problem?.title ?? ""}`); + setStatus(t("status.problemLoaded", { title: message.problem?.title ?? "" })); setIsLoading(false); } if (message.type === "loading" || message.type === "update") { - setStatus(message.text ?? "加载中..."); + setStatus(message.text ?? t("status.loading")); } if (message.type === "error") { - setStatus(message.text ?? "操作失败"); + setStatus(message.text ?? t("err.operationFailed")); setIsLoading(false); setTranslating(false); setLoadingStandings(false); @@ -232,7 +234,7 @@ const ContestApp: React.FC = ({ initContest = "" }) => { if (message.type === "registrationStatus") { setSigned(message.signed ?? false); setRegistrationMessage(message.registrationMessage ?? null); - setStatus(message.registrationMessage ?? (message.signed ? "报名成功" : "报名失败")); + setStatus(message.registrationMessage ?? (message.signed ? t("status.registrationSuccess") : t("status.registrationFail"))); setIsLoading(false); } if (message.type === "translation") { @@ -240,7 +242,7 @@ const ContestApp: React.FC = ({ initContest = "" }) => { setTranslatedCache((prev) => ({ ...prev, [key]: message.translated ?? {} })); setTranslated(message.translated ?? null); setTranslating(false); - setStatus("翻译完成"); + setStatus(t("status.translationDone")); } if (message.type === "submitPage") { setSubmitPageError(null); @@ -252,34 +254,34 @@ const ContestApp: React.FC = ({ initContest = "" }) => { if (message.languages && message.languages.length > 0) { setSelectedSubmitLanguage(message.languages[0].id); } - setStatus("已获取提交页面信息"); + setStatus(t("status.submitPageReady")); setIsLoading(false); } if (message.type === "submitPageError") { - setSubmitPageError({ message: message.message ?? "提交页面无法访问", url: message.url }); - setStatus(message.message ?? "提交页面无法访问"); + setSubmitPageError({ message: message.message ?? t("err.submitPageUnavailable"), url: message.url }); + setStatus(message.message ?? t("err.submitPageUnavailable")); setIsLoading(false); } if (message.type === "submitResult") { setSubmitResult(message.submitResult ?? null); setIsLoading(false); - setStatus(message.submitResult?.success ? "代码提交成功" : (message.submitResult?.message ?? "提交失败")); + setStatus(message.submitResult?.success ? t("status.submitSuccess") : (message.submitResult?.message ?? t("status.submitFailed"))); } if (message.type === "statusUpdate") { const statuses = message.statuses ?? {}; - setTasks((prev) => prev.map((t) => ({ ...t, status: statuses[t.value] }))); + setTasks((prev) => prev.map((task) => ({ ...task, status: statuses[task.value] }))); } if (message.type === "submissionHistory") { setSubmissions(message.submissions ?? []); setLoadingHistory(false); - setStatus(`已获取 ${(message.submissions ?? []).length} 条提交记录`); + setStatus(t("status.historyLoaded", { count: (message.submissions ?? []).length })); } if (message.type === "standings") { const list = message.standings ?? []; standingsCache.current[contest] = list; setStandings(list); setLoadingStandings(false); - setStatus(`已获取 ${list.length} 条排行`); + setStatus(t("status.standingsLoaded", { count: list.length })); } }; window.addEventListener("message", handleMessage); @@ -290,9 +292,9 @@ const ContestApp: React.FC = ({ initContest = "" }) => {
-
{contestTitle || `比赛 ${contest}`}
-
比赛代号:{contest}
-
评级比赛:{rated ? "是" : "否"}
+
{contestTitle || t("ui.contest", { contest })}
+
{t("ui.contestId", { contest })}
+
{t("ui.isRated", { rated: rated ? t("ui.yes") : t("ui.no") })}
{rated && ( )}
@@ -326,7 +328,7 @@ const ContestApp: React.FC = ({ initContest = "" }) => { {announcement && ( -
公告
+
{t("ui.announcement")}
)} @@ -337,7 +339,7 @@ const ContestApp: React.FC = ({ initContest = "" }) => {
{tasks.length > 0 && ( -
题目列表
+
{t("ui.taskList")}
{tasks.map((task) => (
{problem.statement && (
-
题面
+
{t("ui.statement")}
- {translated?.["题目描述"] && ( - + {translated?.[t("text.problemStatement")] && ( + )}
)} {problem.constraints && (
-
约束
+
{t("text.constraints")}
- {translated?.["约束"] && ( - + {translated?.[t("text.constraints")] && ( + )}
)} {problem.inputFormat && (
-
输入格式
+
{t("text.inputFormat")}
- {translated?.["输入格式"] && ( - + {translated?.[t("text.inputFormat")] && ( + )}
)} {problem.outputFormat && (
-
输出格式
+
{t("text.outputFormat")}
- {translated?.["输出格式"] && ( - + {translated?.[t("text.outputFormat")] && ( + )}
)} @@ -440,14 +442,14 @@ const ContestApp: React.FC = ({ initContest = "" }) => { {problem.samples?.length > 0 ? ( problem.samples.map((sample: SampleCase) => (
-
Sample {sample.index}
+
{t("ui.sampleLabel", { index: sample.index })}
Input
{sample.input}
@@ -458,7 +460,7 @@ const ContestApp: React.FC = ({ initContest = "" }) => { @@ -467,31 +469,31 @@ const ContestApp: React.FC = ({ initContest = "" }) => { )) ) : problem.sampleUrl ? (
-
该题目没有内嵌样例,样例在外部链接中。
+
{t("ui.sampleExternal")}
) : ( -
当前题目没有找到样例。
+
{t("ui.noSamples")}
)} )} {!isLoading && tasks.length === 0 && (
- {status || `暂无 ${contest} 的题目`} + {status || t("ui.noTasks", { contest })}
)} {isLoading && (
- 正在抓取数据... + {t("ui.fetchingData")}
)}
@@ -501,19 +503,19 @@ const ContestApp: React.FC = ({ initContest = "" }) => {
-
提交代码到 {contest}
+
{t("ui.submitTo", { contest })}
@@ -526,15 +528,15 @@ const ContestApp: React.FC = ({ initContest = "" }) => { size="sm" className="h-[26px] text-[11px]" > - 在浏览器中打开提交页 + {t("ui.openSubmitPage")}
) : submitTasks.length === 0 && submitLanguages.length === 0 ? ( -
点击「获取提交页面」开始提交。
+
{t("ui.clickToStart")}
) : (
-
题目
+
{t("ui.task")}
setSelectedSubmitLanguage(e.target.value)} @@ -560,11 +562,11 @@ const ContestApp: React.FC = ({ initContest = "" }) => {
-
源代码
+
{t("ui.sourceCode")}