From afa4c0307624aab7a200df21467eacb2d66074d0 Mon Sep 17 00:00:00 2001 From: Ivan Date: Mon, 21 Sep 2026 17:16:21 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D1=83=D1=81=D1=81=D0=BA=D0=B0=D1=8F=20?= =?UTF-8?q?=D0=BB=D0=BE=D0=BA=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=20/=20=E4=BF=84=E8=AF=AD=E6=9C=AC=E5=9C=B0=E5=8C=96=20/?= =?UTF-8?q?=20Russian=20localization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 4 +- package.json | 2 + scripts/check-i18n.mjs | 114 +++ scripts/i18n-runtime.test.mjs | 59 ++ src-tauri/src/i18n/mod.rs | 194 ++++ src-tauri/src/i18n/ru.rs | 87 ++ src-tauri/src/i18n/zh_cn.rs | 85 ++ src-tauri/src/lib.rs | 3 + src-tauri/src/oauth_server.rs | 6 +- src-tauri/src/proxy.rs | 14 +- src-tauri/src/referrals.rs | 31 +- src-tauri/src/tray.rs | 59 +- src/components/AccountList.css | 9 +- src/components/AccountList.tsx | 108 +- src/i18n/index.ts | 32 + src/i18n/ru.ts | 1751 ++++++++++++++++++++++++++++++++ src/i18n/runtime.ts | 110 ++ src/main.tsx | 22 +- 18 files changed, 2585 insertions(+), 105 deletions(-) create mode 100644 scripts/check-i18n.mjs create mode 100644 scripts/i18n-runtime.test.mjs create mode 100644 src-tauri/src/i18n/mod.rs create mode 100644 src-tauri/src/i18n/ru.rs create mode 100644 src-tauri/src/i18n/zh_cn.rs create mode 100644 src/i18n/index.ts create mode 100644 src/i18n/ru.ts create mode 100644 src/i18n/runtime.ts diff --git a/index.html b/index.html index ff93803..0b63a04 100644 --- a/index.html +++ b/index.html @@ -1,10 +1,10 @@ - + - Tauri + React + Typescript + Codex Switcher diff --git a/package.json b/package.json index 6a73513..0d390fd 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,9 @@ "type": "module", "scripts": { "dev": "vite", + "prebuild": "npm run check:i18n", "build": "tsc && vite build", + "check:i18n": "node scripts/check-i18n.mjs && node --test scripts/i18n-runtime.test.mjs", "preview": "vite preview", "tauri": "tauri" }, diff --git a/scripts/check-i18n.mjs b/scripts/check-i18n.mjs new file mode 100644 index 0000000..7624eaa --- /dev/null +++ b/scripts/check-i18n.mjs @@ -0,0 +1,114 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import ts from 'typescript'; + +const sourceRoot = path.resolve('src'); +const localePath = path.join(sourceRoot, 'i18n', 'ru.ts'); +const han = /\p{Script=Han}/u; +const normalize = value => value.replace(/\s+/g, ' ').trim(); + +function sourceFile(file) { + const kind = file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS; + return ts.createSourceFile(file, fs.readFileSync(file, 'utf8'), ts.ScriptTarget.Latest, true, kind); +} + +function visit(node, callback) { + callback(node); + ts.forEachChild(node, child => visit(child, callback)); +} + +function sourceFiles(directory) { + return fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => { + const file = path.join(directory, entry.name); + if (entry.isDirectory()) return sourceFiles(file); + return [file]; + }); +} + +const localeSource = sourceFile(localePath); +const translations = new Map(); +const errors = []; + +visit(localeSource, node => { + if (!ts.isVariableDeclaration(node) || node.name.getText(localeSource) !== 'replacements') return; + if (!node.initializer || !ts.isArrayLiteralExpression(node.initializer)) return; + for (const entry of node.initializer.elements) { + if (!ts.isArrayLiteralExpression(entry) || entry.elements.length !== 2) { + errors.push('ru.ts: каталог должен состоять из пар [исходный текст, перевод]'); + continue; + } + const [source, target] = entry.elements; + if (!ts.isStringLiteralLike(source) || !ts.isStringLiteralLike(target)) { + errors.push('ru.ts: исходный текст и перевод должны быть строковыми литералами'); + continue; + } + const key = normalize(source.text); + if (translations.has(key)) { + errors.push(`ru.ts: повтор исходной строки ${JSON.stringify(key)}`); + } else { + translations.set(key, target.text); + } + if (han.test(target.text)) { + errors.push(`ru.ts: в переводе остались иероглифы: ${JSON.stringify(target.text)}`); + } + } +}); + +if (translations.size === 0) errors.push('ru.ts: каталог переводов не найден или пуст'); + +for (const [source, target] of translations) { + const sourcePlaceholders = source.match(/\{[^{}]*\}/g)?.length ?? 0; + const targetPlaceholders = target.match(/\{[^{}]*\}/g)?.length ?? 0; + if (sourcePlaceholders !== targetPlaceholders) { + errors.push(`ru.ts: число подстановок не совпадает: ${JSON.stringify(source)} -> ${JSON.stringify(target)}`); + } +} + +function checkLiteral(file, ast, node, value, kind) { + const text = normalize(value); + if (!text || !han.test(text)) return; + if (translations.has(text)) return; + const { line } = ast.getLineAndCharacterOfPosition(node.getStart(ast)); + errors.push(`${path.relative(process.cwd(), file)}:${line + 1}: нет точного перевода ${kind} ${JSON.stringify(text)}`); +} + +for (const file of sourceFiles(sourceRoot).filter(file => /\.tsx?$/.test(file))) { + if (file.startsWith(path.join(sourceRoot, 'i18n') + path.sep)) continue; + const ast = sourceFile(file); + visit(ast, node => { + if (ts.isJsxText(node)) { + checkLiteral(file, ast, node, node.getText(ast), 'JSX-текста'); + } else if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) { + checkLiteral(file, ast, node, node.text, 'строки'); + } else if (ts.isTemplateHead(node) || ts.isTemplateMiddle(node) || ts.isTemplateTail(node)) { + checkLiteral(file, ast, node, node.text, 'части шаблона'); + } + }); +} + +const rustRoot = path.resolve('src-tauri', 'src'); +const rustCandidate = /(Err\b|map_err|ok_or|unwrap_or|return\s+"|=>\s*"|Ok\(|format!|\.into\(\)|to_string\(\))/; +const rustLog = /(println!|eprintln!|tracing::|debug!|info!|warn!|trace!)/; + +for (const file of sourceFiles(rustRoot).filter(file => file.endsWith('.rs'))) { + if (file.includes(`${path.sep}i18n${path.sep}`)) continue; + let lines = fs.readFileSync(file, 'utf8').split('\n'); + const tests = lines.findIndex(line => /#\[cfg\(test\)\]/.test(line)); + if (tests >= 0) lines = lines.slice(0, tests); + lines.forEach((line, index) => { + if (/^\s*\/\//.test(line) || rustLog.test(line) || !rustCandidate.test(line)) return; + for (const match of line.matchAll(/"((?:\\.|[^"\\])*)"/g)) { + const text = normalize(match[1].replace(/\\n/g, ' ').replace(/\\"/g, '"')); + if (!han.test(text) || translations.has(text)) continue; + errors.push(`${path.relative(process.cwd(), file)}:${index + 1}: нет перевода возможного сообщения backend ${JSON.stringify(text)}`); + } + }); +} + +if (errors.length > 0) { + console.error(`Проверка локализации не пройдена (${errors.length}):`); + for (const error of errors) console.error(`- ${error}`); + process.exitCode = 1; +} else { + console.log(`Проверка локализации пройдена: ${translations.size} уникальных русских строк.`); +} diff --git a/scripts/i18n-runtime.test.mjs b/scripts/i18n-runtime.test.mjs new file mode 100644 index 0000000..7f7f2ec --- /dev/null +++ b/scripts/i18n-runtime.test.mjs @@ -0,0 +1,59 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import vm from 'node:vm'; +import ts from 'typescript'; + +const source = fs.readFileSync('src/i18n/ru.ts', 'utf8'); +const code = ts.transpileModule(source, { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, +}).outputText; +const module = { exports: {} }; +vm.runInNewContext(code, { module, exports: module.exports }); +const { translateRu } = module.exports; + +function transpileModule(file, context = {}) { + const source = fs.readFileSync(file, 'utf8'); + const code = ts.transpileModule(source, { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, + }).outputText; + const loaded = { exports: {} }; + vm.runInNewContext(code, { + module: loaded, + exports: loaded.exports, + require: name => context[name] ?? {}, + }); + return loaded.exports; +} + +const { resolveAppLocale } = transpileModule('src/i18n/index.ts', { + './ru': { russianLocale: {} }, + './runtime': { installUiLocale() {} }, +}); + +test('system locale selects Russian and unsupported locales fall back to Chinese', () => { + assert.equal(resolveAppLocale('ru'), 'ru'); + assert.equal(resolveAppLocale('ru-RU'), 'ru'); + assert.equal(resolveAppLocale('zh-CN'), 'zh-CN'); + assert.equal(resolveAppLocale('en-US'), 'zh-CN'); + assert.equal(resolveAppLocale(''), 'zh-CN'); +}); + +test('dynamic backend messages retain values and prefer the most specific template', () => { + const cases = [ + ['模型列表请求失败: timeout', 'Не удалось запросить список моделей: timeout'], + [ + 'Server 不可达(primary=http://a, fallback=http://b)', + 'Server недоступен (основной адрес: http://a, резервный: http://b)', + ], + ['已重置 2 个限额窗口', 'Сброшено окон лимита: 2'], + ]; + for (const [input, expected] of cases) assert.equal(translateRu(input), expected); +}); + +test('complete messages win over fragment replacements', () => { + assert.equal( + translateRu('Fast 模式已开启(2x 额度消耗,更快推理)。重启 Codex 生效。'), + 'Режим Fast включён: ответы быстрее, расход квоты удвоен. Перезапустите Codex для применения', + ); +}); diff --git a/src-tauri/src/i18n/mod.rs b/src-tauri/src/i18n/mod.rs new file mode 100644 index 0000000..0b889f6 --- /dev/null +++ b/src-tauri/src/i18n/mod.rs @@ -0,0 +1,194 @@ +mod ru; +#[allow(dead_code)] +mod zh_cn; + +use ru::RussianLocale; +use std::sync::atomic::{AtomicU8, Ordering}; +use zh_cn::ChineseLocale; + +pub const APP_NAME: &str = "Codex Switcher"; + +trait BackendLocale { + fn oauth_success_html(&self) -> &'static str; + fn oauth_failure_response(&self) -> &'static str; + fn tray_tooltip_logged_out(&self) -> String; + fn tray_tooltip_account(&self, name: &str, five_hour_left: f64, weekly_left: f64) -> String; + fn tray_show_main(&self) -> &'static str; + fn tray_next_account(&self) -> &'static str; + fn tray_quit(&self) -> &'static str; + fn notification_account_banned_subtitle(&self) -> &'static str; + fn notification_auto_switch_subtitle(&self) -> &'static str; + fn injected_switch_message(&self, account_name: &str) -> String; + fn referral_unsupported_program(&self) -> &'static str; + fn referral_uncertain_send_suffix(&self) -> &'static str; + fn referral_network_error(&self, error: &reqwest::Error, uncertain: &str) -> String; + fn referral_non_json_response(&self, status: u16, uncertain: &str) -> String; + fn referral_upstream_rejected(&self) -> &'static str; + fn referral_failed_emails(&self, emails: &serde_json::Value) -> String; + fn referral_unrecognized_response(&self, uncertain: &str) -> String; + fn referral_missing_items(&self) -> &'static str; + fn referral_no_available_campaign(&self) -> &'static str; +} + +const RUSSIAN_LOCALE: RussianLocale = RussianLocale; +const CHINESE_LOCALE: ChineseLocale = ChineseLocale; +const LOCALE_AUTO: u8 = 0; +const LOCALE_ZH_CN: u8 = 1; +const LOCALE_RU: u8 = 2; +static ACTIVE_LOCALE: AtomicU8 = AtomicU8::new(LOCALE_AUTO); + +fn active_locale() -> &'static dyn BackendLocale { + match ACTIVE_LOCALE.load(Ordering::Relaxed) { + LOCALE_RU => &RUSSIAN_LOCALE, + LOCALE_ZH_CN => &CHINESE_LOCALE, + _ => { + let language = std::env::var("LANG").unwrap_or_default().to_lowercase(); + if language.starts_with("ru") { + &RUSSIAN_LOCALE + } else { + &CHINESE_LOCALE + } + } + } +} + +#[tauri::command] +pub fn set_app_locale(app: tauri::AppHandle, locale: String) -> Result<(), String> { + let value = match locale.as_str() { + "ru" => LOCALE_RU, + "zh-CN" => LOCALE_ZH_CN, + _ => return Err(format!("Unsupported application locale: {locale}")), + }; + ACTIVE_LOCALE.store(value, Ordering::Relaxed); + crate::tray::update_tray_native_menu(&app).map_err(|error| error.to_string())?; + crate::tray::update_tray_menu(&app); + Ok(()) +} + +fn is_generic_locale(locale: &str) -> bool { + matches!( + locale.trim().to_ascii_lowercase().as_str(), + "c" | "c.utf-8" | "posix" + ) +} + +fn locale_from_environment() -> Option { + ["LC_ALL", "LC_MESSAGES", "LANGUAGE", "LANG"] + .into_iter() + .filter_map(|name| std::env::var(name).ok()) + .map(|value| value.trim().to_string()) + .find(|value| !value.is_empty() && !is_generic_locale(value)) +} + +#[cfg(target_os = "linux")] +fn locale_from_system_file() -> Option { + let contents = std::fs::read_to_string("/etc/default/locale").ok()?; + contents.lines().find_map(|line| { + let (name, value) = line.split_once('=')?; + if name.trim() != "LANG" { + return None; + } + let value = value.trim().trim_matches(['"', '\'']); + (!value.is_empty() && !is_generic_locale(value)).then(|| value.to_string()) + }) +} + +#[cfg(target_os = "macos")] +fn locale_from_system_file() -> Option { + let output = std::process::Command::new("defaults") + .args(["read", "-g", "AppleLocale"]) + .output() + .ok()?; + let value = String::from_utf8(output.stdout).ok()?.trim().to_string(); + (!value.is_empty() && !is_generic_locale(&value)).then_some(value) +} + +#[cfg(not(any(target_os = "linux", target_os = "macos")))] +fn locale_from_system_file() -> Option { + None +} + +#[tauri::command] +pub fn get_system_locale() -> Option { + locale_from_environment().or_else(locale_from_system_file) +} + +pub fn oauth_success_html() -> &'static str { + active_locale().oauth_success_html() +} + +pub fn oauth_failure_response() -> &'static str { + active_locale().oauth_failure_response() +} + +pub fn tray_tooltip_logged_out() -> String { + active_locale().tray_tooltip_logged_out() +} + +pub fn tray_tooltip_account(name: &str, five_hour_left: f64, weekly_left: f64) -> String { + active_locale().tray_tooltip_account(name, five_hour_left, weekly_left) +} + +pub fn tray_tooltip_default() -> String { + APP_NAME.to_string() +} + +pub fn tray_show_main() -> &'static str { + active_locale().tray_show_main() +} + +pub fn tray_next_account() -> &'static str { + active_locale().tray_next_account() +} + +pub fn tray_quit() -> &'static str { + active_locale().tray_quit() +} + +pub fn notification_account_banned_subtitle() -> &'static str { + active_locale().notification_account_banned_subtitle() +} + +pub fn notification_auto_switch_subtitle() -> &'static str { + active_locale().notification_auto_switch_subtitle() +} + +pub fn injected_switch_message(account_name: &str) -> String { + active_locale().injected_switch_message(account_name) +} + +pub fn referral_unsupported_program() -> &'static str { + active_locale().referral_unsupported_program() +} + +pub fn referral_uncertain_send_suffix() -> &'static str { + active_locale().referral_uncertain_send_suffix() +} + +pub fn referral_network_error(error: &reqwest::Error, uncertain: &str) -> String { + active_locale().referral_network_error(error, uncertain) +} + +pub fn referral_non_json_response(status: u16, uncertain: &str) -> String { + active_locale().referral_non_json_response(status, uncertain) +} + +pub fn referral_upstream_rejected() -> &'static str { + active_locale().referral_upstream_rejected() +} + +pub fn referral_failed_emails(emails: &serde_json::Value) -> String { + active_locale().referral_failed_emails(emails) +} + +pub fn referral_unrecognized_response(uncertain: &str) -> String { + active_locale().referral_unrecognized_response(uncertain) +} + +pub fn referral_missing_items() -> &'static str { + active_locale().referral_missing_items() +} + +pub fn referral_no_available_campaign() -> &'static str { + active_locale().referral_no_available_campaign() +} diff --git a/src-tauri/src/i18n/ru.rs b/src-tauri/src/i18n/ru.rs new file mode 100644 index 0000000..4504e65 --- /dev/null +++ b/src-tauri/src/i18n/ru.rs @@ -0,0 +1,87 @@ +use super::{BackendLocale, APP_NAME}; + +pub struct RussianLocale; + +impl BackendLocale for RussianLocale { + fn oauth_success_html(&self) -> &'static str { + "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\n\r\n\ +

Авторизация выполнена

\ +

OpenAI успешно подключён. Можно закрыть это окно и вернуться в приложение.

\ + " + } + + fn oauth_failure_response(&self) -> &'static str { + "HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n\ + Авторизация не выполнена: проверка state не пройдена или отсутствуют параметры" + } + + fn tray_tooltip_logged_out(&self) -> String { + format!("{APP_NAME} - вход не выполнен") + } + + fn tray_tooltip_account(&self, name: &str, five_hour_left: f64, weekly_left: f64) -> String { + format!("{APP_NAME} - {name} | 5ч: {five_hour_left:.0}% неделя: {weekly_left:.0}%") + } + + fn tray_show_main(&self) -> &'static str { + "Открыть главное окно" + } + + fn tray_next_account(&self) -> &'static str { + "Переключиться на следующий аккаунт" + } + + fn tray_quit(&self) -> &'static str { + "Выйти" + } + + fn notification_account_banned_subtitle(&self) -> &'static str { + "Аккаунт заблокирован" + } + + fn notification_auto_switch_subtitle(&self) -> &'static str { + "Автопереключение" + } + + fn injected_switch_message(&self, account_name: &str) -> String { + format!("⚡ [Codex Switcher] Переключено на {account_name}") + } + + fn referral_unsupported_program(&self) -> &'static str { + "Неподдерживаемый тип пригласительной акции" + } + + fn referral_uncertain_send_suffix(&self) -> &'static str { + "; результат отправки не подтверждён. Сначала проверьте историю приглашений и не отправляйте повторно вслепую" + } + + fn referral_network_error(&self, error: &reqwest::Error, uncertain: &str) -> String { + format!("Сетевая ошибка API приглашений: {error}{uncertain}") + } + + fn referral_non_json_response(&self, status: u16, uncertain: &str) -> String { + format!( + "API приглашений вернул ответ HTTP {status} не в формате JSON. Возможно, требуется сеанс входа официального приложения; подтвердить право на участие не удалось{uncertain}" + ) + } + + fn referral_upstream_rejected(&self) -> &'static str { + "Запрос отклонён upstream" + } + + fn referral_failed_emails(&self, emails: &serde_json::Value) -> String { + format!("; email: {emails}") + } + + fn referral_unrecognized_response(&self, uncertain: &str) -> String { + format!("Не удалось распознать ответ API приглашений{uncertain}") + } + + fn referral_missing_items(&self) -> &'static str { + "В истории приглашений отсутствует поле items; подтвердить записи не удалось" + } + + fn referral_no_available_campaign(&self) -> &'static str { + "Для текущего аккаунта нет доступной пригласительной акции; обновите сведения о праве на участие" + } +} diff --git a/src-tauri/src/i18n/zh_cn.rs b/src-tauri/src/i18n/zh_cn.rs new file mode 100644 index 0000000..2799638 --- /dev/null +++ b/src-tauri/src/i18n/zh_cn.rs @@ -0,0 +1,85 @@ +use super::{BackendLocale, APP_NAME}; + +pub struct ChineseLocale; + +impl BackendLocale for ChineseLocale { + fn oauth_success_html(&self) -> &'static str { + "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\n\r\n\ +

授权成功

已成功连接 OpenAI,你可以关闭此窗口并回到应用。

\ + " + } + + fn oauth_failure_response(&self) -> &'static str { + "HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n授权失败: State 校验不通过或参数缺失" + } + + fn tray_tooltip_logged_out(&self) -> String { + format!("{APP_NAME} - 未登录") + } + + fn tray_tooltip_account(&self, name: &str, five_hour_left: f64, weekly_left: f64) -> String { + format!("{APP_NAME} - {name} | 5H: {five_hour_left:.0}% 周: {weekly_left:.0}%") + } + + fn tray_show_main(&self) -> &'static str { + "打开主窗口" + } + + fn tray_next_account(&self) -> &'static str { + "切换到下一个账号" + } + + fn tray_quit(&self) -> &'static str { + "退出" + } + + fn notification_account_banned_subtitle(&self) -> &'static str { + "检测到封号" + } + + fn notification_auto_switch_subtitle(&self) -> &'static str { + "自动切号" + } + + fn injected_switch_message(&self, account_name: &str) -> String { + format!("⚡ [Codex Switcher] 已切换到 {account_name}") + } + + fn referral_unsupported_program(&self) -> &'static str { + "不支持的邀请活动类型" + } + + fn referral_uncertain_send_suffix(&self) -> &'static str { + ";发送结果未确认,请先查询邀请记录,勿直接重发" + } + + fn referral_network_error(&self, error: &reqwest::Error, uncertain: &str) -> String { + format!("邀请接口网络错误:{error}{uncertain}") + } + + fn referral_non_json_response(&self, status: u16, uncertain: &str) -> String { + format!( + "邀请接口返回 HTTP {status} 非 JSON 响应,可能需要官方桌面版登录会话;无法确认活动资格{uncertain}" + ) + } + + fn referral_upstream_rejected(&self) -> &'static str { + "请求被上游拒绝" + } + + fn referral_failed_emails(&self, emails: &serde_json::Value) -> String { + format!(";邮箱:{emails}") + } + + fn referral_unrecognized_response(&self, uncertain: &str) -> String { + format!("邀请接口响应格式无法识别{uncertain}") + } + + fn referral_missing_items(&self) -> &'static str { + "邀请记录响应缺少 items,无法确认记录" + } + + fn referral_no_available_campaign(&self) -> &'static str { + "当前账号没有可发送的邀请活动,请刷新资格" + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e3cf5a5..b772f81 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -9,6 +9,7 @@ pub mod chat_inbound; mod codex_sessions; mod codex_ua; mod deep_link; +mod i18n; mod ide_control; pub mod kimi_quota; pub mod mailbox; @@ -6358,6 +6359,8 @@ pub fn run() { }) .invoke_handler(tauri::generate_handler![ get_accounts, + i18n::set_app_locale, + i18n::get_system_locale, get_current_account_id, import_current_account, switch_account, diff --git a/src-tauri/src/oauth_server.rs b/src-tauri/src/oauth_server.rs index 3d38bcf..a46609b 100644 --- a/src-tauri/src/oauth_server.rs +++ b/src-tauri/src/oauth_server.rs @@ -154,9 +154,7 @@ async fn handle_callback(listener: TcpListener, app_handle: AppHandle, expected_ if let Some(code) = extract_oauth_code_from_request(&request, &expected_state) { // 发送成功 HTML 并通知前端 - let response = "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\n\r\n\ -

授权成功

已成功连接 OpenAI,你可以关闭此窗口并回到应用。

\ - "; + let response = crate::i18n::oauth_success_html(); let _ = socket.write_all(response.as_bytes()).await; if let Err(e) = app_handle.emit("oauth-callback-received", code) { @@ -165,7 +163,7 @@ async fn handle_callback(listener: TcpListener, app_handle: AppHandle, expected_ return; } - let response = "HTTP/1.1 400 Bad Request\r\n\r\n授权失败: State 校验不通过或参数缺失"; + let response = crate::i18n::oauth_failure_response(); let _ = socket.write_all(response.as_bytes()).await; } } diff --git a/src-tauri/src/proxy.rs b/src-tauri/src/proxy.rs index 6a702ee..57327f4 100644 --- a/src-tauri/src/proxy.rs +++ b/src-tauri/src/proxy.rs @@ -2352,8 +2352,10 @@ fn mark_current_banned(state: &ProxyState) { let _ = std::process::Command::new("osascript") .arg("-e") .arg(format!( - "display notification \"{}\" with title \"Codex Switcher\" subtitle \"检测到封号\"", - notify_name + "display notification \"{}\" with title \"{}\" subtitle \"{}\"", + notify_name, + crate::i18n::APP_NAME, + crate::i18n::notification_account_banned_subtitle() )) .output(); }); @@ -2667,8 +2669,10 @@ fn do_switch(state: &ProxyState, new_id: &str, reason: SwitchReason) -> Result<( let _ = std::process::Command::new("osascript") .arg("-e") .arg(format!( - "display notification \"{}\" with title \"Codex Switcher\" subtitle \"自动切号\"", - notify_msg + "display notification \"{}\" with title \"{}\" subtitle \"{}\"", + notify_msg, + crate::i18n::APP_NAME, + crate::i18n::notification_auto_switch_subtitle() )) .output(); }); @@ -2677,7 +2681,7 @@ fn do_switch(state: &ProxyState, new_id: &str, reason: SwitchReason) -> Result<( // 注入 WebSocket 消息标记(可配置,实验性) if inject_enabled { PENDING_INJECT_MSG.lock().ok().map(|mut msg| { - *msg = Some(format!("⚡ [Codex Switcher] 已切换到 {}", to_name)); + *msg = Some(crate::i18n::injected_switch_message(&to_name)); }); } diff --git a/src-tauri/src/referrals.rs b/src-tauri/src/referrals.rs index 23f6732..bb4ff2a 100644 --- a/src-tauri/src/referrals.rs +++ b/src-tauri/src/referrals.rs @@ -8,7 +8,7 @@ fn context(program: &str) -> Result { "codex_referral_consumer" | "codex_referral_workspace" => { Ok(json!({"program_id": program, "entrypoint": "persistent"})) } - _ => Err("不支持的邀请活动类型".into()), + _ => Err(crate::i18n::referral_unsupported_program().into()), } } @@ -36,41 +36,40 @@ fn request( async fn response(req: reqwest::RequestBuilder, sending: bool) -> Result { let uncertain = if sending { - ";发送结果未确认,请先查询邀请记录,勿直接重发" + crate::i18n::referral_uncertain_send_suffix() } else { "" }; let resp = req .send() .await - .map_err(|e| format!("邀请接口网络错误:{e}{uncertain}"))?; + .map_err(|e| crate::i18n::referral_network_error(&e, uncertain))?; let status = resp.status(); - let data = resp.json::().await.map_err(|_| { - format!( - "邀请接口返回 HTTP {} 非 JSON 响应,可能需要官方桌面版登录会话;无法确认活动资格{}", - status.as_u16(), - uncertain - ) - })?; + let data = resp + .json::() + .await + .map_err(|_| crate::i18n::referral_non_json_response(status.as_u16(), uncertain))?; if !status.is_success() { let detail = data.get("detail").unwrap_or(&data); let message = detail .as_str() .or_else(|| detail.get("message").and_then(Value::as_str)) - .unwrap_or("请求被上游拒绝"); + .unwrap_or(crate::i18n::referral_upstream_rejected()); let failed = detail .get("failed_emails") .or_else(|| data.get("failed_emails")); return Err(format!( - "HTTP {}:{}{}{}", + "HTTP {}: {}{}{}", status.as_u16(), message, - failed.map(|v| format!(";邮箱:{v}")).unwrap_or_default(), + failed + .map(crate::i18n::referral_failed_emails) + .unwrap_or_default(), uncertain )); } if !data.is_object() { - return Err(format!("邀请接口响应格式无法识别{uncertain}")); + return Err(crate::i18n::referral_unrecognized_response(uncertain)); } Ok(data) } @@ -104,7 +103,7 @@ pub async fn tracking( } let data = response(req, false).await?; if !data["items"].is_array() { - return Err("邀请记录响应缺少 items,无法确认记录".into()); + return Err(crate::i18n::referral_missing_items().into()); } Ok(data) } @@ -117,7 +116,7 @@ fn send_body( ) -> Result { let mut body = context(program)?; if offer["should_show"] != true { - return Err("当前账号没有可发送的邀请活动,请刷新资格".into()); + return Err(crate::i18n::referral_no_available_campaign().into()); } // Detect an offer change between review and submission, including grant amounts. for key in ["offer_id", "grants", "requires_explicit_confirmation"] { diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs index a80d036..4bac5f8 100644 --- a/src-tauri/src/tray.rs +++ b/src-tauri/src/tray.rs @@ -36,17 +36,7 @@ pub fn init(app: &AppHandle) -> Result<(), Box> { // Windows 右键需要真正挂载 native menu;仅监听 TrayIconEvent 会把右键 // 也当成 popup 点击,系统不会自动生成完整托盘菜单。 - let show_main = MenuItem::with_id(app, "tray-show-main", "打开主窗口", true, None::<&str>)?; - let next_account = MenuItem::with_id( - app, - "tray-next-account", - "切换到下一个账号", - true, - None::<&str>, - )?; - let separator = PredefinedMenuItem::separator(app)?; - let quit = PredefinedMenuItem::quit(app, Some("退出"))?; - let menu = Menu::with_items(app, &[&show_main, &next_account, &separator, &quit])?; + let menu = build_native_menu(app)?; let _tray = TrayIconBuilder::with_id("main") .icon(icon) @@ -87,6 +77,33 @@ pub fn init(app: &AppHandle) -> Result<(), Box> { Ok(()) } +fn build_native_menu(app: &AppHandle) -> tauri::Result> { + let show_main = MenuItem::with_id( + app, + "tray-show-main", + crate::i18n::tray_show_main(), + true, + None::<&str>, + )?; + let next_account = MenuItem::with_id( + app, + "tray-next-account", + crate::i18n::tray_next_account(), + true, + None::<&str>, + )?; + let separator = PredefinedMenuItem::separator(app)?; + let quit = PredefinedMenuItem::quit(app, Some(crate::i18n::tray_quit()))?; + Menu::with_items(app, &[&show_main, &next_account, &separator, &quit]) +} + +pub fn update_tray_native_menu(app: &AppHandle) -> tauri::Result<()> { + if let Some(tray) = app.tray_by_id("main") { + tray.set_menu(Some(build_native_menu(app)?))?; + } + Ok(()) +} + /// 显示/隐藏 tray popup 窗口 fn toggle_popup(app: &AppHandle, position: tauri::PhysicalPosition) { let label = "tray-popup"; @@ -114,7 +131,7 @@ fn toggle_popup(app: &AppHandle, position: tauri::PhysicalPosition) { let url = tauri::WebviewUrl::App("index.html".into()); match WebviewWindowBuilder::new(app, label, url) - .title("Codex Switcher") + .title(crate::i18n::APP_NAME) .inner_size(popup_width, popup_height) .resizable(false) .decorations(false) @@ -215,17 +232,21 @@ pub fn update_tray_menu(app: &AppHandle) { }; if let Some(current_id) = &store.current { if let Some(acc) = store.accounts.get(current_id) { - let quota = acc - .cached_quota + acc.cached_quota .as_ref() - .map(|q| format!(" | 5H: {:.0}% 周: {:.0}%", q.five_hour_left, q.weekly_left)) - .unwrap_or_default(); - format!("Codex Switcher - {}{}", acc.name, quota) + .map(|q| { + crate::i18n::tray_tooltip_account( + &acc.name, + q.five_hour_left, + q.weekly_left, + ) + }) + .unwrap_or_else(|| format!("{} - {}", crate::i18n::APP_NAME, acc.name)) } else { - "Codex Switcher".to_string() + crate::i18n::tray_tooltip_default() } } else { - "Codex Switcher - 未登录".to_string() + crate::i18n::tray_tooltip_logged_out() } // store guard 在 block 结束(这一行)时 drop,set_tooltip 在外面跑 }; diff --git a/src/components/AccountList.css b/src/components/AccountList.css index 0f738cb..520ab82 100644 --- a/src/components/AccountList.css +++ b/src/components/AccountList.css @@ -121,6 +121,14 @@ flex: 1; } +.toolbar-actions { + display: flex; + align-items: center; + gap: 12px; + margin-left: auto; + flex: 0 0 auto; +} + /* 紧凑 filter 按钮(label·count 内联,省 toolbar 宽度) */ .filter-btn-compact { gap: 4px; @@ -185,7 +193,6 @@ .toolbar-icon-btn { width: 30px; height: 30px; - margin-left: 6px; padding: 0; display: inline-flex; align-items: center; diff --git a/src/components/AccountList.tsx b/src/components/AccountList.tsx index a829a54..57bdb60 100644 --- a/src/components/AccountList.tsx +++ b/src/components/AccountList.tsx @@ -888,63 +888,65 @@ export function AccountList({ })}
- - {onAddAccount && ( +
- )} - {onAddRelay && ( - + )} + {onAddRelay && ( + + )} + {onRefreshUsage && ( + + )} + - )} - {onRefreshUsage && ( - - )} - +
diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 0000000..ca67504 --- /dev/null +++ b/src/i18n/index.ts @@ -0,0 +1,32 @@ +import { russianLocale } from './ru'; +import { installUiLocale } from './runtime'; + +const locales = new Map([ + ['ru', russianLocale], +]); + +export type AppLocaleCode = 'ru' | 'zh-CN'; + +function requestedLocale(): string { + return (navigator.language || 'zh-CN').toLowerCase(); +} + +export function resolveAppLocale(language: string): AppLocaleCode { + const normalized = language.toLowerCase(); + return [...locales.keys()].some(code => normalized.startsWith(code)) ? 'ru' : 'zh-CN'; +} + +// Chinese remains the source language and the fallback for unsupported locales. +export function installAppLocale(systemLocale?: string): AppLocaleCode { + const language = systemLocale ?? requestedLocale(); + const localeCode = resolveAppLocale(language); + const locale = localeCode === 'ru' ? russianLocale : undefined; + if (locale) { + installUiLocale(locale); + return localeCode; + } else { + document.documentElement.lang = 'zh-CN'; + document.title = 'Codex Switcher'; + return 'zh-CN'; + } +} diff --git a/src/i18n/ru.ts b/src/i18n/ru.ts new file mode 100644 index 0000000..9e05dbe --- /dev/null +++ b/src/i18n/ru.ts @@ -0,0 +1,1751 @@ +import type { UiLocale } from './runtime'; + +type Replacement = readonly [source: string, target: string]; + +const replacements: Replacement[] = [ + // Messages returned by the Tauri backend. Placeholders are matched at runtime. + ['模型列表请求失败: {}', 'Не удалось запросить список моделей: {}'], + ['模型列表 JSON 解析失败: {}', 'Не удалось разобрать JSON списка моделей: {}'], + ['上游拒绝了模型列表请求', 'Upstream отклонил запрос списка моделей'], + ['上游模型列表为空或响应格式不识别', 'Список моделей upstream пуст или имеет неизвестный формат'], + ['中转账号不可用', 'Relay-аккаунт недоступен'], + ['请先配置此中转的模型 ID', 'Сначала укажите ID модели для этого relay'], + ['该账号未配置这个模型', 'Эта модель не настроена для аккаунта'], + ['设置文件权限失败: {}', 'Не удалось задать права файла: {}'], + ['设置目录权限失败: {}', 'Не удалось задать права каталога: {}'], + ['写入文件失败: {}', 'Не удалось записать файл: {}'], + ['Google 账号不存在', 'Google-аккаунт не найден'], + ['只能选择 Google Antigravity 账号', 'Можно выбрать только аккаунт Google Antigravity'], + ['Google 账号不可用,请先重新授权', 'Google-аккаунт недоступен; сначала повторите авторизацию'], + ['创建目录失败: {}', 'Не удалось создать каталог: {}'], + ['序列化失败: {}', 'Не удалось сериализовать данные: {}'], + ['读取 auth.json 失败: {}', 'Не удалось прочитать auth.json: {}'], + ['解析 auth.json 失败: {}', 'Не удалось разобрать auth.json: {}'], + ['写入临时文件失败: {}', 'Не удалось записать временный файл: {}'], + ['重命名文件失败 (Atomic Write): {}', 'Не удалось атомарно переименовать файл: {}'], + ['账号不存在: {}', 'Аккаунт не найден: {}'], + ['导出失败: {}', 'Не удалось экспортировать: {}'], + ['导入失败: {}', 'Не удалось импортировать: {}'], + ['Base64 解码失败: {}', 'Не удалось декодировать Base64: {}'], + ['base64 解码失败: {}', 'Не удалось декодировать base64: {}'], + ['UTF-8 转换失败: {}', 'Не удалось преобразовать в UTF-8: {}'], + ['JSON 解析失败: {}', 'Не удалось разобрать JSON: {}'], + ['无法定位 Windows 系统目录', 'Не удалось найти системный каталог Windows'], + ['无法启动剪贴板工具: {}', 'Не удалось запустить средство буфера обмена: {}'], + ['剪贴板输入不可写', 'Буфер обмена недоступен для записи'], + ['写入剪贴板失败: {}', 'Не удалось записать в буфер обмена: {}'], + ['等待剪贴板工具失败: {}', 'Ошибка ожидания средства буфера обмена: {}'], + ['剪贴板工具退出失败: {}', 'Средство буфера обмена завершилось с ошибкой: {}'], + ['剪贴板写入超时,请手动复制', 'Истекло время записи в буфер обмена; скопируйте вручную'], + ['创建临时文件失败: {}', 'Не удалось создать временный файл: {}'], + ['重命名临时文件失败: {}', 'Не удалось переименовать временный файл: {}'], + ['邀请奖励或活动条件已变化,请刷新资格后确认', 'Награда или условия акции изменились; обновите сведения и подтвердите повторно'], + ['当前没有剩余奖励名额,已阻止发送邀请', 'Свободных бонусных мест нет; отправка приглашения отменена'], + ['本次最多可邀请 {cap} 个邮箱', 'За один раз можно пригласить не более {cap} адресов email'], + ['邮箱格式不正确', 'Некорректный формат email'], + ['发送结果缺少 invites,请先查询邀请记录,勿直接重发', 'В результате отправки нет поля invites; проверьте историю приглашений и не отправляйте повторно вслепую'], + ['无法绑定 Antigravity OAuth 回调端口 {CALLBACK_PORT}: {e}', 'Не удалось занять порт обратного вызова Antigravity OAuth {CALLBACK_PORT}: {e}'], + ['登录流程状态锁异常', 'Не удалось получить доступ к состоянию входа'], + ['Antigravity 登录流程已过期或未启动', 'Процесс входа Antigravity истёк или не был запущен'], + ['Google 未返回模型额度,请稍后重试;这不表示额度为 0', 'Google не вернул квоты моделей. Повторите позже; это не означает, что квота равна нулю'], + ['zip 解析失败: {}', 'Не удалось разобрать ZIP: {}'], + ['zip entry {} 失败: {}', 'Ошибка элемента ZIP {}: {}'], + ['{} 解析 JSON 失败: {}', 'Не удалось разобрать JSON в {}: {}'], + ['{} 缺 credentials', 'В {} отсутствует поле credentials'], + ['无', 'Нет'], + ['未找到 IDE {} 的配置', 'Конфигурация IDE {} не найдена'], + ['不由自主', 'Нет управления приложением'], + ['不允许发送按键', 'Отправка нажатий клавиш запрещена'], + ['无法执行 osascript: {}', 'Не удалось выполнить osascript: {}'], + ['AppleScript 执行失败: {}', 'Не удалось выполнить AppleScript: {}'], + ['store lock 失败', 'Не удалось заблокировать хранилище'], + ['client/solo 模式禁止本机 rt 刷新;Server 不可达,跳过本机降级', 'В режиме client/solo локальное обновление RT запрещено; Server недоступен, локальный резервный путь пропущен'], + ['chat 入站选中的账号不存在: {}', 'Выбранный для входящего чата аккаунт не найден: {}'], + ['chat 入站选中的账号缺 access_token: {}', 'У выбранного для входящего чата аккаунта нет access_token: {}'], + ['上游请求失败: {}', 'Запрос к upstream не выполнен: {}'], + ['current 已登出', 'Текущий аккаунт вышел из системы'], + ['current 没有 refresh_token', 'У текущего аккаунта нет refresh_token'], + ['上游 HTTP {}', 'Upstream вернул HTTP {}'], + ['读取请求体失败', 'Не удалось прочитать тело запроса'], + ['Responses Relay 缺少 API key', 'Для Responses Relay не задан API-ключ'], + ['上游连接失败: {}', 'Не удалось подключиться к upstream: {}'], + ['响应构建失败', 'Не удалось сформировать ответ'], + ['账号已登出/RT 被轮换', 'Аккаунт вышел из системы или RT был заменён'], + ['缺 refresh_token 无法刷新', 'Обновление невозможно: отсутствует refresh_token'], + ['瞬时刷新失败: {}', 'Временный сбой обновления: {}'], + ['5xx 透传', 'Ответ 5xx передан без изменений'], + ['4xx 透传失败', 'Не удалось передать ответ 4xx'], + ['所有账号额度已耗尽,最早恢复:{}', 'Квоты всех аккаунтов исчерпаны. Ближайшее восстановление: {}'], + ['所有账号额度已耗尽', 'Квоты всех аккаунтов исчерпаны'], + ['Server 不可达: {}', 'Server недоступен: {}'], + ['无法从 {} 构造 Server proxy URL', 'Не удалось сформировать URL прокси Server из {}'], + ['构建 no_proxy client 失败: {}', 'Не удалось создать клиент no_proxy: {}'], + ['转发到 Server 失败: {e:?}', 'Не удалось перенаправить запрос на Server: {e:?}'], + ['silent_retry: 读首 chunk 失败: {}', 'silent_retry: не удалось прочитать первый фрагмент: {}'], + ['未配置 remote_shared_secret', 'Не задан remote_shared_secret'], + ['转发请求失败: {}', 'Не удалось перенаправить запрос: {}'], + ['流构建失败', 'Не удалось сформировать поток'], + ['锁失败', 'Не удалось получить блокировку'], + ['WebSocket 请求构建失败: {}', 'Не удалось сформировать запрос WebSocket: {}'], + ['刷新后 WebSocket 仍连接失败: {}', 'После обновления подключение WebSocket по-прежнему не устанавливается: {}'], + ['101 构建失败', 'Не удалось сформировать ответ 101'], + ['translator 请求处理失败: {}', 'Translator не смог обработать запрос: {}'], + ['Relay base_url 未配置', 'Для Relay не задан base_url'], + ['Relay base_url 解析失败', 'Не удалось разобрать base_url Relay'], + ['relay 上游连接失败: {}', 'Не удалось подключиться к upstream relay: {}'], + ['relay 上游 {}: {}', 'Upstream relay вернул {}: {}'], + ['sync 响应翻译失败: {}', 'Не удалось преобразовать синхронный ответ: {}'], + ['relay SSE 上游错误: {}', 'Ошибка SSE upstream relay: {}'], + ['StepFun /models 请求失败: {}', 'Запрос StepFun /models не выполнен: {}'], + ['上游响应构建失败', 'Не удалось сформировать ответ upstream'], + ['网络请求失败: {}', 'Сетевой запрос не выполнен: {}'], + ['wham/usage 请求失败', 'Запрос wham/usage не выполнен'], + ['刷新后重试失败: {}', 'Повторный запрос после обновления не выполнен: {}'], + ['刷新后重试失败', 'Повторный запрос после обновления не выполнен'], + ['ACCOUNT_LOGGED_OUT:您已登出或登录了其他账号,请重新登录', 'ACCOUNT_LOGGED_OUT: выполнен выход или вход в другой аккаунт; войдите заново'], + ['ACCOUNT_BANNED:该账号已被封禁', 'ACCOUNT_BANNED: аккаунт заблокирован'], + ['当前激活账号访问配额接口返回 401/403;已禁用本地 refresh_token 刷新,请稍后重试或在 Codex 中触发一次请求', 'Запрос квоты активного аккаунта вернул 401/403. Локальное обновление refresh_token отключено; повторите позже или выполните запрос в Codex'], + ['TOKEN_INVALID:授权已失效,请删除该账号后重新登录', 'TOKEN_INVALID: авторизация недействительна; удалите аккаунт и войдите заново'], + ['读取响应失败: {}', 'Не удалось прочитать ответ: {}'], + ['解析 JSON 失败: {}', 'Не удалось разобрать JSON: {}'], + ['ACCOUNT_BANNED:该账号已被封禁(workspace 已停用)', 'ACCOUNT_BANNED: аккаунт заблокирован (workspace отключён)'], + ['{}H 限额', 'Лимит {}ч'], + ['{}天后重置', 'Сброс через {} дн.'], + ['{}小时{}分钟后重置', 'Сброс через {} ч {} мин'], + ['{}分钟后重置', 'Сброс через {} мин'], + ['usage 请求失败: {}', 'Запрос usage не выполнен: {}'], + ['usage JSON 解析失败: {}', 'Не удалось разобрать JSON usage: {}'], + ['上游响应缺 remaining/balance 字段', 'В ответе upstream нет поля remaining или balance'], + ['subscription 请求失败: {}', 'Запрос subscription не выполнен: {}'], + ['subscription JSON 解析失败: {}', 'Не удалось разобрать JSON subscription: {}'], + ['subscription 缺 soft_limit_usd 字段', 'В ответе subscription нет поля soft_limit_usd'], + ['无法从 base_url 解析 origin: {}', 'Не удалось получить origin из base_url: {}'], + ['MiMo Cookie 缺少 api-platform_serviceToken 或 userId', 'В Cookie MiMo отсутствует api-platform_serviceToken или userId'], + ['MiMo usage 响应缺 monthUsage.items', 'В ответе MiMo usage нет monthUsage.items'], + ['MiMo usage 响应缺 percent/used/limit', 'В ответе MiMo usage нет percent, used или limit'], + ['StepFun 额度查询需要 platform.stepfun.com 的 Oasis-Token', 'Для проверки квоты StepFun требуется Oasis-Token с platform.stepfun.com'], + ['StepFun 额度请求失败: {}', 'Запрос квоты StepFun не выполнен: {}'], + ['StepFun 额度 JSON 解析失败: {}', 'Не удалось разобрать JSON квоты StepFun: {}'], + ['上游拒绝了额度查询', 'Upstream отклонил запрос квоты'], + ['订阅 Credit', 'Кредит подписки'], + ['加油包 Credit', 'Дополнительный кредит'], + ['响应中没有可识别的 Step Plan 额度字段', 'В ответе нет распознаваемых полей квоты Step Plan'], + ['StepFun 额度解析失败: {}', 'Не удалось разобрать квоту StepFun: {}'], + ['MiMo usage 请求失败: {}', 'Запрос MiMo usage не выполнен: {}'], + ['MiMo 登录态已失效,请重新登录后复制 Cookie', 'Сеанс MiMo истёк; войдите заново и скопируйте Cookie'], + ['MiMo Cookie 无效或权限不足', 'Cookie MiMo недействителен или не имеет необходимых прав'], + ['MiMo usage JSON 解析失败: {}', 'Не удалось разобрать JSON MiMo usage: {}'], + ['至少需要 1 个邀请邮箱', 'Укажите хотя бы один email для приглашения'], + ['邀请 {} 获得', 'Получено за приглашение {}'], + ['Codex 赠送', 'Подарок Codex'], + ['未知来源', 'Источник неизвестен'], + ['token 失效(401),请刷新或重新登录', 'Токен недействителен (401); обновите его или войдите заново'], + ['需要验证(403)', 'Требуется подтверждение (403)'], + ['请求过于频繁(429),稍后再试', 'Слишком много запросов (429); повторите позже'], + ['上游返回 HTTP {}', 'Upstream вернул HTTP {}'], + ['上游返回非 JSON', 'Upstream вернул ответ не в формате JSON'], + ['上游未返回有效的重置银行明细,当前次数未知(不代表已清空)', 'Upstream не вернул корректные сведения о доступных сбросах. Их число неизвестно; это не означает, что сбросов не осталось'], + ['已重置 {} 个限额窗口', 'Сброшено окон лимита: {}'], + ['当前没有可重置的限额(额度还没用到上限)', 'Сейчас нечего сбрасывать: квота ещё не исчерпана'], + ['上游返回:{}', 'Ответ upstream: {}'], + ['配额已耗尽(usage_limit_reached)', 'Квота исчерпана (usage_limit_reached)'], + ['token 失效(401)', 'Токен недействителен (401)'], + ['需要验证或提交保证书(403)', 'Требуется подтверждение или гарантийное заявление (403)'], + ['配额已耗尽(429)', 'Квота исчерпана (429)'], + ['Server 不可达(fallback={})', 'Server недоступен (резервный адрес: {})'], + ['Server 不可达(primary={})', 'Server недоступен (основной адрес: {})'], + ['Server 不可达(primary={}, fallback={})', 'Server недоступен (основной адрес: {}, резервный: {})'], + ['Server Google token 租约失败: {error}', 'Не удалось получить аренду Google-токена у Server: {error}'], + ['Server Google token 租约失败', 'Не удалось получить аренду Google-токена у Server'], + ['构建 HTTP client 失败: {}', 'Не удалось создать HTTP-клиент: {}'], + ['连接 Server 失败: {}', 'Не удалось подключиться к Server: {}'], + ['Server /health 返回 {}', 'Server /health вернул {}'], + ['解析 /health 响应失败: {}', 'Не удалось разобрать ответ /health: {}'], + ['连接失败: {}', 'Не удалось подключиться: {}'], + ['共享密钥不正确', 'Неверный общий ключ'], + ['Server /accounts 返回 {}', 'Server /accounts вернул {}'], + ['请求失败: {}', 'Запрос не выполнен: {}'], + ['Server 返回 {}', 'Server вернул {}'], + ['响应缺少 accounts 字段', 'В ответе нет поля accounts'], + ['反序列化账号列表失败: {}', 'Не удалось прочитать список аккаунтов: {}'], + ['创建 SSOT 目录失败: {}', 'Не удалось создать каталог SSOT: {}'], + ['读取目录失败: {}', 'Не удалось прочитать каталог: {}'], + ['移除旧 Antigravity skills 链接失败: {}', 'Не удалось удалить старую ссылку навыков Antigravity: {}'], + ['移除旧 Antigravity skills 目录失败: {}', 'Не удалось удалить старый каталог навыков Antigravity: {}'], + ['移除旧 Antigravity skill 失败: {}', 'Не удалось удалить старый навык Antigravity: {}'], + ['移除旧 Antigravity skills manifest 失败: {}', 'Не удалось удалить старый манифест навыков Antigravity: {}'], + ['Antigravity skills 目录不可用', 'Каталог навыков Antigravity недоступен'], + ['移除 Antigravity skills symlink 失败: {}', 'Не удалось удалить символическую ссылку навыков Antigravity: {}'], + ['Antigravity skills 路径不是目录: {:?}', 'Путь навыков Antigravity не является каталогом: {:?}'], + ['创建 Antigravity skills 目录失败: {}', 'Не удалось создать каталог навыков Antigravity: {}'], + ['移除过期 Antigravity skill {} 失败: {}', 'Не удалось удалить устаревший навык Antigravity {}: {}'], + ['覆盖 Antigravity skill {} 失败: {}', 'Не удалось заменить навык Antigravity {}: {}'], + ['生成 Antigravity skills manifest 失败: {}', 'Не удалось создать манифест навыков Antigravity: {}'], + ['写入 Antigravity skills manifest 失败: {}', 'Не удалось записать манифест навыков Antigravity: {}'], + ['移除 Antigravity skill 失败: {}', 'Не удалось удалить навык Antigravity: {}'], + ['移除 Antigravity skills manifest 失败: {}', 'Не удалось удалить манифест навыков Antigravity: {}'], + ['创建 symlink 失败: {}', 'Не удалось создать символическую ссылку: {}'], + ['读取条目失败: {}', 'Не удалось прочитать элемент: {}'], + ['复制文件失败: {}', 'Не удалось скопировать файл: {}'], + ['备份文件失败: {}', 'Не удалось создать резервную копию файла: {}'], + ['写入失败: {}', 'Не удалось записать: {}'], + ['未知 app: {}', 'Неизвестное приложение: {}'], + ['下载失败: {}', 'Не удалось скачать: {}'], + ['下载失败: HTTP {}', 'Не удалось скачать: HTTP {}'], + ['读取失败: {}', 'Не удалось прочитать: {}'], + ['在仓库中未找到 skill: {}', 'Навык не найден в репозитории: {}'], + ['skill 不存在: {}', 'Навык не найден: {}'], + ['打开 ZIP 失败: {}', 'Не удалось открыть ZIP: {}'], + ['读取 ZIP 条目失败: {}', 'Не удалось прочитать элемент ZIP: {}'], + ['创建文件失败: {}', 'Не удалось создать файл: {}'], + ['skill 目录不存在: {}', 'Каталог навыка не найден: {}'], + ['读取目录失败 {:?}: {}', 'Не удалось прочитать каталог {:?}: {}'], + ['添加目录失败: {}', 'Не удалось добавить каталог: {}'], + ['开始文件失败: {}', 'Не удалось начать запись файла: {}'], + ['打开文件失败 {:?}: {}', 'Не удалось открыть файл {:?}: {}'], + ['读取文件失败: {}', 'Не удалось прочитать файл: {}'], + ['写入 zip 失败: {}', 'Не удалось записать ZIP: {}'], + ['finish zip 失败: {}', 'Не удалось завершить создание ZIP: {}'], + ['非法 skill 名: {}', 'Недопустимое имя навыка: {}'], + ['创建 SSOT 失败: {}', 'Не удалось создать SSOT: {}'], + ['创建 staging 失败: {}', 'Не удалось создать временный каталог импорта: {}'], + ['打开 zip 失败: {}', 'Не удалось открыть ZIP: {}'], + ['读取 zip 条目失败: {}', 'Не удалось прочитать элемент ZIP: {}'], + ['zip 条目名非法: {}', 'Недопустимое имя элемента ZIP: {}'], + ['zip 条目缺少顶层目录', 'В элементе ZIP отсутствует корневой каталог'], + ['zip 顶层目录 {:?} 与期望 {} 不一致', 'Корневой каталог ZIP {:?} не совпадает с ожидаемым {}'], + ['创建父目录失败: {}', 'Не удалось создать родительский каталог: {}'], + ['创建文件失败 {:?}: {}', 'Не удалось создать файл {:?}: {}'], + ['读取 zip 内容失败: {}', 'Не удалось прочитать содержимое ZIP: {}'], + ['备份原目录失败: {}', 'Не удалось создать резервную копию исходного каталога: {}'], + ['替换 skill 失败: {}', 'Не удалось заменить навык: {}'], + ['sentinel/req 请求失败: {e}', 'Запрос sentinel/req не выполнен: {e}'], + ['sentinel/req 读取响应失败: {e}', 'Не удалось прочитать ответ sentinel/req: {e}'], + ['sentinel/req JSON 解析失败: {e}', 'Не удалось разобрать JSON sentinel/req: {e}'], + ['构建 HTTP client 失败: {e}', 'Не удалось создать HTTP-клиент: {e}'], + ['/oauth/authorize 请求失败: {e}', 'Запрос /oauth/authorize не выполнен: {e}'], + ['authorize/continue 失败: {e}', 'Запрос authorize/continue не выполнен: {e}'], + ['authorize/continue 读响应失败: {e}', 'Не удалось прочитать ответ authorize/continue: {e}'], + ['send-otp 失败: {e}', 'Не удалось отправить OTP: {e}'], + ['send-otp 非 200: {status} body={}', 'send-otp вернул статус {status}; тело ответа: {}'], + ['email-otp/validate 失败: {e}', 'Не удалось проверить OTP email: {e}'], + ['session_dump 失败: {e}', 'Не удалось получить session_dump: {e}'], + ['workspace/select 失败: {e}', 'Не удалось выбрать workspace: {e}'], + ['无法获取 continue_url,OTP 校验/workspace 选择都没给出,无法继续', 'Не получен continue_url: ни проверка OTP, ни выбор workspace не дали адреса для продолжения'], + ['follow {url} 失败: {e}', 'Не удалось перейти по {url}: {e}'], + ['follow_until_callback 超过 15 跳', 'Превышен лимит из 15 переходов до callback'], + ['URL 解析失败: {e}', 'Не удалось разобрать URL: {e}'], + ['URL 里没有 code 参数: {url}', 'В URL нет параметра code: {url}'], + ['请求令牌失败: {}', 'Не удалось запросить токен: {}'], + ['OpenAI 返回错误: {}', 'OpenAI вернул ошибку: {}'], + ['解析令牌响应失败: {}', 'Не удалось разобрать ответ с токеном: {}'], + ['store 锁中毒', 'Блокировка хранилища повреждена'], + ['刷新令牌失败: {}', 'Не удалось обновить токен: {}'], + ['刷新令牌被拒绝: {}', 'Обновление токена отклонено: {}'], + ['解析刷新响应失败: {}', 'Не удалось разобрать ответ обновления: {}'], + ['OTP 等待超时', 'Истекло время ожидания OTP'], + ['nissanserena GET 失败: {e}', 'Запрос nissanserena GET не выполнен: {e}'], + ['nissanserena 读响应失败: {e}', 'Не удалось прочитать ответ nissanserena: {e}'], + ['回调链接不能为空', 'Ссылка обратного вызова не может быть пустой'], + ['未能从输入中解析出 code 参数', 'Не удалось извлечь параметр code из введённого значения'], + ['state 校验不通过:这个回调链接不属于本次登录流程', 'Проверка state не пройдена: ссылка обратного вызова не относится к текущему процессу входа'], + ['登录流程已过期或未启动,请先点击『立即登录 OpenAI』', 'Процесс входа истёк или не был запущен. Сначала нажмите «Войти в OpenAI сейчас»'], + ['派发 oauth-callback-received 失败: {}', 'Не удалось отправить событие oauth-callback-received: {}'], + ['无法启动 pbcopy: {}', 'Не удалось запустить pbcopy: {}'], + ['pbcopy stdin 不可写', 'Стандартный ввод pbcopy недоступен для записи'], + ['写入 pbcopy 失败: {}', 'Не удалось записать в pbcopy: {}'], + ['等待 pbcopy 退出失败: {}', 'Ошибка ожидания завершения pbcopy: {}'], + ['pbcopy 返回非零: {:?}', 'pbcopy завершился с ненулевым кодом: {:?}'], + ['锁被污染', 'Блокировка повреждена'], + ['登录流程已过期或未启动', 'Процесс входа истёк или не был запущен'], + ['锁获取失败: {}', 'Не удалось получить блокировку: {}'], + ['Google ST 获取失败', 'Не удалось получить Google ST'], + ['Google ST 缺失', 'Отсутствует Google ST'], + ['Google project_id 缺失', 'Отсутствует Google project_id'], + ['账号已被删除', 'Аккаунт удалён'], + ['读取 OAuth body 失败: {error}', 'Не удалось прочитать тело OAuth: {error}'], + ['OAuth JSON 解析失败: {error}', 'Не удалось разобрать JSON OAuth: {error}'], + ['读取 body 失败: {}', 'Не удалось прочитать тело запроса: {}'], + ['保存失败: {}', 'Не удалось сохранить: {}'], + ['锁获取失败', 'Не удалось получить блокировку'], + ['序列化响应失败: {}', 'Не удалось сериализовать ответ: {}'], + ['刷新 token 失败: {}', 'Не удалось обновить токен: {}'], + ['无 access_token 且无 refresh_token', 'Отсутствуют и access_token, и refresh_token'], + ['TOKEN_INVALID:刷新 token 失败: {}', 'TOKEN_INVALID: не удалось обновить токен: {}'], + ['switch_to 失败: {}', 'Не удалось выполнить switch_to: {}'], + ['加载图标失败: {}', 'Не удалось загрузить значок: {}'], + ['安全确认已过期,请重新点击修复', 'Срок подтверждения истёк; снова нажмите «Исправить»'], + ['安全确认无效,请重新点击修复', 'Подтверждение недействительно; снова нажмите «Исправить»'], + ['缺少安全确认,请重新点击修复', 'Отсутствует подтверждение; снова нажмите «Исправить»'], + ['当前 auth.json 缺少 refresh_token,无法自动续期,请重新登录', 'В текущем auth.json нет refresh_token, поэтому автоматическое продление невозможно. Войдите заново'], + ['仅 ChatGPT OAuth 订阅账号支持周期保鲜', 'Продление периода поддерживается только для аккаунтов с подпиской ChatGPT OAuth'], + ['base_url 必须以 http:// 或 https:// 开头', 'base_url должен начинаться с http:// или https://'], + ['api_key 不能为空', 'api_key не может быть пустым'], + ['name 不能为空', 'Имя не может быть пустым'], + ['中转站账号缺 base_url', 'У relay-аккаунта отсутствует base_url'], + ['中转站账号缺 api_key', 'У relay-аккаунта отсутствует api_key'], + ['账号在刷新模型时被删除', 'Аккаунт был удалён во время обновления моделей'], + ['账号在刷新模型时发生变化', 'Аккаунт изменился во время обновления моделей'], + ['账号 {} 不存在', 'Аккаунт {} не найден'], + ['自动探测未命中:上游不支持 /v1/dashboard/billing 或 /v1/usage(可手动选 usage 策略,或保持「不拉取」)', 'Автоопределение не сработало: upstream не поддерживает /v1/dashboard/billing или /v1/usage. Выберите стратегию usage вручную либо оставьте «Не запрашивать»'], + ['MiMo 配额查询需要登录 platform.xiaomimimo.com 后复制 Cookie header', 'Для проверки квоты MiMo войдите на platform.xiaomimimo.com и скопируйте заголовок Cookie'], + ['StepFun 额度查询需要登录 platform.stepfun.com 后复制 Oasis-Token', 'Для проверки квоты StepFun войдите на platform.stepfun.com и скопируйте Oasis-Token'], + ['未支持的 usage_preset: {}', 'Неподдерживаемый usage_preset: {}'], + ['usage 策略未确定', 'Стратегия usage не определена'], + ['OAuth 未返回 refresh_token,无法自动续期', 'OAuth не вернул refresh_token; автоматическое продление невозможно'], + ['无法从授权响应中解析用户信息 (Missing ID Token)', 'Не удалось получить данные пользователя из ответа авторизации: отсутствует ID Token'], + ['没有当前激活账号', 'Нет активного аккаунта'], + ['已覆盖 ~/.codex/auth.json', 'Файл ~/.codex/auth.json перезаписан'], + ['OpenAI OAuth 登录', 'Вход через OpenAI OAuth'], + ['该账号不是 Google Antigravity 账号', 'Этот аккаунт не относится к Google Antigravity'], + ['Antigravity 账号由模型路由自动选择,不切换 Codex 当前身份', 'Аккаунты Antigravity выбираются маршрутизацией моделей автоматически и не меняют текущий аккаунт Codex'], + ['账号缺少 access_token', 'У аккаунта отсутствует access_token'], + ['该账号正在被其他流程刷新,请稍后重试', 'Аккаунт обновляется другим процессом; повторите позже'], + ['仅 solo 模式支持同号操作', 'Операция синхронизации аккаунта поддерживается только в режиме solo'], + ['没有可用账号', 'Нет доступных аккаунтов'], + ['遍历完所有账号,未发现可用配额的账号', 'Проверены все аккаунты; аккаунтов с доступной квотой не найдено'], + ['PROVIDER_ACCOUNT:该 Provider 账号不支持 OpenAI usage 查询', 'PROVIDER_ACCOUNT: этот аккаунт провайдера не поддерживает запрос OpenAI usage'], + ['TOKEN_INVALID:client/solo 模式禁止本机 rt 刷新;请确认账号已同步到 Server', 'TOKEN_INVALID: в режиме client/solo локальное обновление RT запрещено; проверьте синхронизацию аккаунта с Server'], + ['TOKEN_INVALID:无 access_token 且无 refresh_token', 'TOKEN_INVALID: отсутствуют и access_token, и refresh_token'], + ['邀请邮箱过多:{} 个,最多 50 个', 'Слишком много адресов для приглашения: {}; допускается не более 50'], + ['PROVIDER_ACCOUNT:该 Provider 账号不支持 Codex 邀请', 'PROVIDER_ACCOUNT: этот аккаунт провайдера не поддерживает приглашения Codex'], + ['你好', 'Здравствуйте'], + ['UNSUPPORTED_ACCOUNT:仅 ChatGPT OAuth 订阅账号支持 Codex 唤醒', 'UNSUPPORTED_ACCOUNT: запуск Codex поддерживается только для аккаунтов с подпиской ChatGPT OAuth'], + ['PROVIDER_ACCOUNT:该 Provider 账号不支持 codex 终端', 'PROVIDER_ACCOUNT: этот аккаунт провайдера не поддерживает терминал Codex'], + ['账号 auth_json 结构异常', 'Некорректная структура auth_json аккаунта'], + ['账号缺少 codex tokens,无法启动', 'У аккаунта нет токенов Codex; запуск невозможен'], + ['创建 CODEX_HOME 失败: {}', 'Не удалось создать CODEX_HOME: {}'], + ['写 auth.json 失败: {}', 'Не удалось записать auth.json: {}'], + ['写 config.toml 失败: {}', 'Не удалось записать config.toml: {}'], + ['打开 Terminal 失败: {}', 'Не удалось открыть терминал: {}'], + ['已为 {} 打开 codex 终端', 'Терминал Codex открыт для {}'], + ['同步失败:账号不存在或 User ID 不匹配', 'Синхронизация не выполнена: аккаунт не найден или User ID не совпадает'], + ['PROVIDER_ACCOUNT:该 Provider 账号不支持 OpenAI usage', 'PROVIDER_ACCOUNT: этот аккаунт провайдера не поддерживает OpenAI usage'], + ['当前激活账号与 ~/.codex/auth.json 身份不匹配,已拒绝覆盖,请先在 Codex 中切回同一账号', 'Активный аккаунт не совпадает с аккаунтом в ~/.codex/auth.json. Перезапись отклонена; сначала переключите Codex на тот же аккаунт'], + ['session route 不存在: {}', 'Маршрут сессии не найден: {}'], + ['执行失败: {}', 'Не удалось выполнить: {}'], + ['已终止 {} 个 codex 进程', 'Завершено процессов Codex: {}'], + ['读取 {} 失败: {}', 'Не удалось прочитать {}: {}'], + ['写入 {} 失败: {}', 'Не удалось записать {}: {}'], + ['config.toml(失败: {})', 'config.toml (ошибка: {})'], + ['创建 config.toml 失败: {}', 'Не удалось создать config.toml: {}'], + ['读取 config.toml 失败: {}', 'Не удалось прочитать config.toml: {}'], + ['写入 config.toml 失败: {}', 'Не удалось записать config.toml: {}'], + ['~/.codex/config.toml 不存在', 'Файл ~/.codex/config.toml не найден'], + ['Fast 模式已开启(2x 额度消耗,更快推理)。重启 Codex 生效。', 'Режим Fast включён: ответы быстрее, расход квоты удвоен. Перезапустите Codex для применения'], + ['Fast 模式已关闭。重启 Codex 生效。', 'Режим Fast отключён. Перезапустите Codex для применения'], + ['[features] goals = true 已写入。重启 Codex 生效。', 'Параметр [features] goals = true записан. Перезапустите Codex для применения'], + ['[features] goals 已关闭。重启 Codex 生效。', 'Функция [features] goals отключена. Перезапустите Codex для применения'], + ['本地未找到账号 {}', 'Локальный аккаунт {} не найден'], + ['zip task 崩溃: {}', 'Задача ZIP аварийно завершилась: {}'], + ['已停止(当前模式 {})', 'Остановлено (текущий режим: {})'], + ['已启动 http://{}:{}', 'Запущено: http://{}:{}'], + + + // Dynamic fragments and complete mixed-language labels. + ['分钟 后重置', 'мин до сброса'], + ['后台保活已停用(', 'Фоновое продление отключено ('], + [',需重新登录)@', ', требуется повторный вход) @'], + ['后台保活临时失败(', 'Временный сбой фонового продления ('], + ['代理已自动切号 →', 'Прокси автоматически переключил аккаунт →'], + ['检测到封号:', 'Обнаружена блокировка аккаунта:'], + [',已自动切换', ', выполнено автоматическое переключение'], + ['(早期数据·无 account)', '(старые данные, без аккаунта)'], + ['+ 添加', '+ Добавить'], + ['没有匹配「', 'Нет правил, соответствующих «'], + ['已复制 ✓', 'Скопировано ✓'], + ['确认删除路由「', 'Удалить маршрут «'], + ['来源:', 'Источник:'], + ['⚠️ 链接里携带的', '⚠️ Переданное в ссылке значение '], + ['未在内置白名单中,已忽略。账号仍可正常使用, 但额度查询不会自动进行。导入后可在账号详情中手动选 usage preset。', 'не входит во встроенный список разрешённых значений и было проигнорировано. Аккаунт останется доступен, но квота не будет проверяться автоматически. После импорта стратегию проверки квоты можно выбрать в параметрах аккаунта.'], + ['添加路由', 'Добавить маршрут'], + ['同步到 Server 失败:', 'Не удалось синхронизировать с Server:'], + ['已安装 (', 'Установлено ('], + [',此操作将从所有 CLI 目录移除该 skill。', ', навык будет удалён из всех каталогов CLI.'], + ['失败:', 'Ошибка: '], + [',远端账号数', ', аккаунтов на Server: '], + ['✅ 已设为手机锚', '✅ Мобильная привязка установлена'], + ['❌ 绑定失败:', '❌ Не удалось установить привязку: '], + ['✅ 已解除手机锚绑定', '✅ Мобильная привязка снята'], + ['❌ 解除失败:', '❌ Не удалось снять привязку: '], + ['这将尝试移除 Codex App 的安全隔离属性。 系统可能会弹窗要求输入密码以获得权限。是否继续?', 'Будет выполнена попытка удалить quarantine-атрибут Codex App. Система может запросить пароль администратора. Продолжить?'], + ['off=独立;server=Server 侧提供 API;client=本机,rt 旋转走 Server。 老 solo 模式已合并到 client(自动迁移:solo → client + 本机直连上游 + 本机管 current)。', 'off — автономная работа; server — предоставление API на стороне Server; client — работа на этом компьютере с обновлением RT через Server. Прежний режим solo объединён с client и автоматически переносится в client с локальным подключением к upstream и локальным управлением current.'], + ['off(关闭)', 'off (выключено)'], + ['(未设置)', '(не задано)'], + ['优先尝试,建议填局域网 IP,如 http://192.168.2.14:18081', 'Используется первым. Рекомендуется адрес в локальной сети, например http://192.168.2.14:18081'], + ['主不通时自动切换,建议填 ZeroTier IP,如 http://172.26.96.198:18081', 'Используется автоматически, если основной адрес недоступен. Рекомендуется адрес ZeroTier, например http://172.26.96.198:18081'], + ['磁盘 ~/.codex/auth.json 永远跟随此号,Codex.app 手机远程连接绑定此号; 切到其他号时 disk 不动、proxy 出口照切(手机 bridge 不掉线)。 仅 ChatGPT 订阅号可绑定。', 'Файл ~/.codex/auth.json всегда остаётся привязан к этому аккаунту, как и мобильное удалённое подключение Codex.app. При переключении других аккаунтов файл на диске не меняется, а выход прокси переключается без разрыва мобильного подключения. Привязать можно только аккаунт с подпиской ChatGPT.'], + ['· 代理出口', '· выход прокси'], + ['Vs 上月 $', 'К прошлому месяцу: $'], + ['身份在线(手机 bridge 不掉线), 代理出口已切到', 'остаётся активным для мобильного подключения, а выход прокси переключён на '], + ['(代理正在注入当前激活号的 token,', '(прокси подставляет токен активного аккаунта, '], + ['; 关闭代理后 codex 会读到这个号)', '; после отключения прокси Codex прочитает этот аккаунт)'], + ['⚠️ 失效', '⚠️ Недействителен'], + ['💰 额度', '💰 Квота'], + ['代理已启动', 'Прокси запущен'], + ['代理已停止', 'Прокси остановлен'], + ['(新终端窗口生效)', '(вступит в силу в новом окне терминала)'], + ['auto:代理开启时自动走热切(不写 auth.json,不杀进程,仅刷 store.current)。 代理关闭时回退到冷切。cold:无论如何都冷切。', 'auto: при включённом прокси используется горячее переключение без записи auth.json и завершения процессов; обновляется только store.current. При выключенном прокси используется холодное переключение. cold: всегда использовать холодное переключение.'], + ['。 同邮箱已存在的账号会跳过,不覆盖现有 token。', '. Аккаунты с уже существующим email будут пропущены, имеющиеся токены не перезаписываются.'], + ['(同名)', '(с тем же именем)'], + ['新增账号详情(', 'Сведения о новых аккаунтах ('], + ['⚠ 仅 RT,首次请求自动 refresh', '⚠ Только RT; при первом запросе токен обновится автоматически'], + ['), 绕过 Codex 手机验证。支持单个对象、对象数组、或者嵌套容器,会自动递归识别。', '), что позволяет пройти проверку телефона Codex. Поддерживаются отдельный объект, массив объектов и вложенные контейнеры; формат определяется рекурсивно.'], + ['导入账号详情(', 'Сведения об импортированных аккаунтах ('], + ['(id_token 合成)', '(id_token сформирован автоматически)'], + ['失败项(', 'Ошибки ('], + ['glm_zhipu (GLM 自家 quota 接口)', 'glm_zhipu (собственный API квот GLM)'], + ['kimi_coding (Kimi 编程套餐 5H / 7D)', 'kimi_coding (тариф Kimi Coding, 5ч / 7д)'], + ['mimo_token_plan (MiMo 控制台 Cookie)', 'mimo_token_plan (Cookie из консоли MiMo)'], + ['stepfun_plan (StepFun 控制台 Oasis-Token)', 'stepfun_plan (Oasis-Token из консоли StepFun)'], + ['Oasis-Token=...(或直接粘贴 token)', 'Oasis-Token=... (или вставьте сам токен)'], + ['将从本地官方 Codex 的登录状态 (`auth.json`) 中提取认证信息。', 'Данные авторизации будут извлечены из локального файла входа официального Codex (`auth.json`).'], + ['deep link 添加。', 'добавление по ссылке приложения.'], + ['(自定义 base URL)', '(собственный базовый URL)'], + ['kimi_coding · Kimi 编程套餐 5H / 7D', 'kimi_coding · тариф Kimi Coding, 5ч / 7д'], + ['mimo_token_plan · 需 Cookie', 'mimo_token_plan · требуется Cookie'], + ['stepfun_plan · Step Plan 控制台 Token', 'stepfun_plan · токен из консоли Step Plan'], + ['· 已过期', '· истёк'], + ['· 今天', '· сегодня'], + ['账号还有', 'До окончания аккаунта осталось '], + [',点击修改', ', нажмите для изменения'], + ['(消耗了到期', '(использован кредит со сроком до '], + ['的那条)', ')'], + ['[AutoRefresh] 触发', '[AutoRefresh] запущено для '], + ['个账号 reset 后自动刷新:', ' аккаунтов после сброса:'], + [',已刷新额度', ', квота обновлена'], + ['推送 Server 成功(', 'успешно отправлен на Server ('], + ['Google 切号失败:', 'Не удалось переключить Google-аккаунт: '], + ['设为其模型的当前账号(Codex / Google 不变)', 'назначен текущим аккаунтом для своих моделей (Codex и Google не изменены)'], + ['登录已失效:refresh_token 已过期或被撤销,请重新登录', 'Сеанс входа недействителен: refresh_token истёк или был отозван. Войдите заново'], + ['刷新失败:', 'Не удалось обновить: '], + ['📱 手机锚', '📱 Мобильная привязка'], + ['🔄 次数未知', '🔄 Число сбросов неизвестно'], + ['⚡ 当前已被限流(额度桶为 0)——现在用一次主动重置回收最大,点击查看明细', '⚡ Сейчас действует ограничение: окно квоты равно нулю. Активный сброс даст максимальный эффект; нажмите, чтобы посмотреть сведения.'], + ['🚀 启动 codex', '🚀 Запустить Codex'], + ['周期保鲜最近错误:', 'Последняя ошибка продления периода: '], + ['🌿 周期保鲜 ·', '🌿 Продление периода ·'], + ['🌿 保鲜已关 ·', '🌿 Продление отключено ·'], + ['最近尝试:', 'Последняя попытка: '], + ['最近成功:', 'Последнее успешное выполнение: '], + ['最近结果:', 'Последний результат: '], + ['主动重置 ·', 'Активный сброс ·'], + ['拉取明细失败:', 'Не удалось загрузить сведения: '], + [',区别仅到期时间;', ', различается только срок действия; '], + [',也可粘贴包含它的 Cookie header。', '; также можно вставить заголовок Cookie, который его содержит.'], + [':每次切号前后强制抓 quota 写 `~/.codex-switcher/quota-snapshots.jsonl`。同一窗口内任意两次快照的 Δused_pct 配合期间代理 tokens → 推出该 Plan 总容量(capacity = Δtokens / Δpct × 100)。', ': до и после каждого переключения снимок квоты принудительно записывается в `~/.codex-switcher/quota-snapshots.jsonl`. Любые два снимка одного окна вместе с числом токенов прокси за этот период позволяют оценить общую ёмкость тарифа: capacity = Δtokens / Δpct × 100.'], + ['5h 周期(', 'Периоды 5ч ('], + ['周周期(', 'Недельные периоды ('], + ['每格显示「', 'В каждой ячейке показано «'], + ['= `实测累加 ÷ snapshot used_pct × 100`,用快照里 used_pct 最大那个算(量化误差最小)。`?%` 标记的 used_pct 偏小,估算误差大。', '= `фактическая сумма ÷ snapshot used_pct × 100`; расчёт выполняется по снимку с наибольшим used_pct, чтобы снизить ошибку округления. Метка `?%` означает, что used_pct мал и оценка менее точна.'], + ['🔴 = 窗口内触发过限额切号 —— 此时实测累加 ≈ Plan 实际窗口配额。', '🔴 = в этом окне было переключение из-за лимита; фактическая сумма примерно равна реальной квоте окна тарифа.'], + ['点开账号看历史周期,点开周期看 session 明细。', 'Раскройте аккаунт, чтобы посмотреть историю периодов, затем раскройте период для просмотра сессий.'], + ['切号日志 (', 'Журнал переключений ('], + ['后台任务日志 (', 'Журнал фоновых задач ('], + ['% · 窗口', '% · окно '], + ['Token Plan 订阅(tp-key;配额用控制台 Cookie)', 'Подписка Token Plan (tp-key; квота проверяется по Cookie из консоли)'], + ['手动填 base_url;余额自动探测', 'Укажите base_url вручную; баланс определяется автоматически'], + + // Fragments rendered as separate React text nodes. + ['模型', 'Модель'], + ['最后更新:', 'Последнее обновление: '], + ['奖励', 'Награда'], + ['目标账号异常:', 'Ошибка целевого аккаунта: '], + ['还没有路由规则。点', 'Правил маршрутизации пока нет. Нажмите '], + ['」的规则。', '».'], + ['未知账号(已删除? id:', 'Неизвестный аккаунт (возможно, удалён; id:'], + ['当前活跃会话', 'Текущая активная сессия'], + ['授权链接', 'Ссылка авторизации'], + ['以确认:', 'для подтверждения:'], + ['生成', 'Создать'], + ['心跳时自动把本机 current 对齐到 Server 的 current。 Server 不可达会静默跳过,保持本机现状。', 'При проверке связи локальный current автоматически синхронизируется с current на Server. Если Server недоступен, синхронизация молча пропускается и локальное состояние сохраняется.'], + ['上游模型额度:', 'Квоты моделей upstream: '], + [';窗口明细未获取', '; сведения об окнах не получены'], + ['代理已自动切换到账号:', 'Прокси автоматически переключился на аккаунт:'], + ['写入', 'Записать '], + ['goals = true 到 ~/.codex/config.toml。', 'goals = true в ~/.codex/config.toml.'], + ['(codex_credentials zip / 单 .json)、', ' (архив codex_credentials или отдельный .json), '], + ['个账号', ' аккаунтов'], + ['失效(约 30 天)后账号会变成不可用, 需要重新粘贴一次新 session。Plus 账号能正常调用模型,Free 账号即使导入也无 API 权限。', 'истекает примерно через 30 дней, после чего аккаунт становится недоступен и потребуется вставить новую сессию. Аккаунты Plus могут обращаться к моделям; у Free-аккаунтов нет доступа к API даже после импорта.'], + [')粘贴到下方:', ') и вставьте ниже:'], + ['选一个', 'Выберите сервис'], + [',base URL 自动填好,下一步只用粘 API Key。\n 需要在多家 Coding Plan 之间切换额度的话,可以把同一个服务添加多次(用账号名区分)。\n 也支持', ': базовый URL заполнится автоматически, на следующем шаге останется вставить API-ключ. Чтобы переключать квоты между несколькими Coding Plan, добавьте один сервис несколько раз под разными именами аккаунтов. Также поддерживается '], + ['切换服务', 'Сменить сервис'], + ['前缀', 'Префикс'], + ['API 地址(可修改)', 'Адрес API (можно изменить)'], + ['默认模型 ID(可修改)', 'ID модели по умолчанию (можно изменить)'], + ['余额查询', 'Проверка баланса'], + ['额度凭证', 'Данные для проверки квоты'], + ['填写凭据', 'Укажите данные доступа'], + ['← 返回选择', '← Вернуться к выбору'], + ['设为这些模型的当前号:', 'Сделать текущим для моделей: '], + ['(不影响 Codex / Google)', ' (не влияет на Codex / Google)'], + ['(通常最早到期优先,客户端无法指定)。', ' (обычно сначала используется кредит с ближайшим сроком; клиент не может выбрать его вручную).'], + ['消耗 1 次会把当前已耗尽的 5H / 周限额窗口立刻清零,此操作不可撤销。若额度还没用到上限,上游会返回「无可重置」且', 'Одна попытка сразу сбросит исчерпанное окно лимита 5ч или недели. Действие необратимо. Если квота ещё не исчерпана, upstream вернёт «сбрасывать нечего» и '], + ['。登录', '. После входа '], + ['人', ' чел.'], + ['请检查邮箱格式和本次可发送人数(最多', 'Проверьте формат email и доступное число получателей (не более '], + ['个)。', ' email).'], + ['上游已创建', 'Upstream создал '], + ['条邀请', ' приглашений'], + ['本次奖励:', 'Награда за эту отправку: '], + ['未成功:', 'Не отправлено: '], + ['日', 'День'], + ['周', 'Неделя'], + ['月', 'Месяц'], + ['是去掉异常值后最稳的估计。Δpct<3% 的样本被丢弃(used_pct 是整数,量化误差会失真)。', 'даёт наиболее устойчивую оценку после удаления выбросов. Образцы с Δpct<3% отбрасываются: used_pct является целым числом, поэтому ошибка квантования искажает результат.'], + ['= token-history.jsonl 在该窗口内的精确求和(不平均、不打折,无论中间切号几次)。', '= точная сумма из token-history.jsonl за это окно: без усреднения и скидок, независимо от числа переключений аккаунта.'], + ['条)', ' зап.)'], + ['无数据', 'Нет данных'], + ['首次 → 末次', 'Первое → последнее'], + ['直接切换将', 'Прямое переключение '], + ['覆盖', 'перезапишет'], + ['官方插件中的当前登录状态,且无法找回这些未同步的更新。', 'текущее состояние входа официального плагина; восстановить несинхронизированные обновления будет невозможно.'], + + // Relay provider names and descriptions shown in the account dialogs. + ['月之暗面(Kimi)', 'Moonshot AI (Kimi)'], + ['编程套餐:5H / 7D 额度;使用 Kimi Code Key,模型按会员权限选择', 'Тариф для разработки: квоты 5ч / 7д; используется Kimi Code Key, модели зависят от подписки'], + ['智谱', 'Zhipu AI'], + ['开放平台 API;OpenAI 兼容,支持模型映射', 'API открытой платформы; совместим с OpenAI, поддерживает сопоставление моделей'], + ['GLM Coding Plan(codex /v1/responses ↔ /chat/completions 翻译,内置)', 'GLM Coding Plan (встроенный перевод codex /v1/responses ↔ /chat/completions)'], + ['小米', 'Xiaomi'], + ['MiMo 按量付费(sk-key;非 Token Plan,无配额限制按用量计费)', 'MiMo с оплатой по потреблению (sk-key; не Token Plan, оплата по фактическому использованию без квоты)'], + ['PinCC / PackyCode / AICodeMirror / 自建 CLIProxyAPI 等;余额自动探测', 'PinCC, PackyCode, AICodeMirror, собственный CLIProxyAPI и другие; баланс определяется автоматически'], + ['原生 Responses API;支持官网或自有中转,API 地址和模型 ID 均可修改', 'Нативный Responses API; поддерживает официальный или собственный relay, адрес API и ID модели можно изменить'], + ['开放平台 API,原生 Responses;使用平台 API Key,非 Kimi Code 订阅密钥', 'API открытой платформы с нативным Responses; используется API-ключ платформы, а не ключ подписки Kimi Code'], + ['按量付费 / 订阅服务;OpenAI Chat 兼容', 'Оплата по потреблению или подписка; совместим с OpenAI Chat'], + ['阿里云', 'Alibaba Cloud'], + ['百炼开放平台;OpenAI 兼容模式', 'Открытая платформа Bailian; режим совместимости с OpenAI'], + ['通义', 'Qwen'], + ['字节跳动', 'ByteDance'], + ['火山方舟 (Coding Plan / Agent Plan / 按量付费);model 字段需用户填实际 endpoint id', 'Volcano Engine Ark (Coding Plan, Agent Plan или оплата по потреблению); в поле model укажите фактический ID endpoint'], + ['火', 'V'], + ['腾讯', 'Tencent'], + ['腾讯混元 (Token Plan / TokenHub 按量);OpenAI Chat 兼容', 'Tencent Hunyuan (Token Plan или TokenHub с оплатой по потреблению); совместим с OpenAI Chat'], + ['混', 'H'], + ['百度', 'Baidu'], + ['百度千帆 / 文心一言 ERNIE(按量付费 / 中国订阅)', 'Baidu Qianfan / ERNIE (оплата по потреблению или подписка для Китая)'], + ['千', 'B'], + ['优刻得(UCloud)', 'UCloud'], + ['UCloud Modelverse (Coding Plan + 按量付费 国内/海外);多模型聚合', 'UCloud Modelverse (Coding Plan и оплата по потреблению в Китае или за его пределами); агрегатор моделей'], + ['Fireworks AI 海外高速推理(按量 / Fire Pass 国际订阅);OpenAI Chat 兼容', 'Fireworks AI: высокоскоростной инференс за пределами Китая (оплата по потреблению или международная подписка Fire Pass); совместим с OpenAI Chat'], + ['阶跃星辰 Step Plan', 'StepFun Step Plan'], + ['Step Plan 订阅;模型直接读取 /models,额度可用控制台 Oasis-Token 查询', 'Подписка Step Plan; модели загружаются из /models, квоту можно проверить по Oasis-Token из консоли'], + ['阶跃星辰', 'StepFun'], + ['阶跃星辰 step 系列 (按量付费 / 中国订阅 / 国际订阅)', 'Серия StepFun Step (оплата по потреблению, подписка для Китая или международная подписка)'], + ['多模型聚合 API;可配置厂商模型 ID', 'API-агрегатор моделей; можно настроить ID модели провайдера'], + ['Aiberm 全球聚合 API(OpenAI/Anthropic 双协议,模型按 token group 动态)', 'Глобальный API-агрегатор Aiberm (протоколы OpenAI и Anthropic; модели зависят от группы токена)'], + ['神马中转 API 全球按量付费聚合(OpenAI/Anthropic 双协议)', 'Глобальный relay API Shenma с оплатой по потреблению (протоколы OpenAI и Anthropic)'], + ['神', 'S'], + ['魔搭(阿里云)', 'ModelScope (Alibaba Cloud)'], + ['阿里魔搭 ModelScope 按量付费;OpenAI Chat 兼容(Qwen 系列为主)', 'Alibaba ModelScope с оплатой по потреблению; совместим с OpenAI Chat (преимущественно модели Qwen)'], + ['魔', 'M'], + ['Ollama 本地推理(localhost:11434,需先 ollama pull 模型);API Key 任意填一个非空字符串即可', 'Локальный инференс Ollama (localhost:11434; сначала выполните ollama pull для модели); в качестве API-ключа подойдёт любая непустая строка'], + + // English commands that have clear Russian UI equivalents. + ['● Proxy ON', '● Прокси включён'], + ['● Proxy OFF', '● Прокси выключен'], + ['📋 Dashboard', '📋 Открыть панель'], + ['↻ Refresh', '↻ Обновить'], + ['Base URL', 'Базовый URL'], + ['Base URL *', 'Базовый URL *'], + ['API Key', 'API-ключ'], + ['API Key (sk-... / tp-...) *', 'API-ключ (sk-... / tp-...) *'], + ['deep link', 'ссылка приложения'], + ['deep link。', 'ссылка приложения.'], + ['Plan', 'Тариф'], + ['Email / Plan', 'Email / тариф'], + ['Session', 'Сессия'], + ['Token', 'Токены'], + ['检查同步状态失败:', 'Не удалось проверить состояние синхронизации:'], + ['[Frontend] 收到后台刷新通知,重新加载账号列表', '[Frontend] Получено уведомление об обновлении; список аккаунтов загружается заново'], + ['[Frontend] 收到设置更新通知,重新加载设置', '[Frontend] Получено уведомление об изменении; настройки загружаются заново'], + ['同步 IDE 状态失败:', 'Не удалось синхронизировать состояние IDE:'], + ['加载设置失败:', 'Не удалось загрузить настройки:'], + ['加载代理状态失败:', 'Не удалось загрузить состояние прокси:'], + ['次', ' раз'], + ['glm_zhipu · GLM 自家 quota', 'glm_zhipu · собственная квота GLM'], + ['是去掉异常值后最稳的估计。Δpct<3% 的样本被丢弃(used_pct 是整数,量化误差会失真)。', 'даёт наиболее устойчивую оценку после удаления выбросов. Образцы с Δpct<3% отбрасываются: used_pct является целым числом, поэтому ошибка квантования искажает результат.'], + ['心跳时自动把本机 current 对齐到 Server 的 current。', 'При проверке связи локальный current автоматически синхронизируется с current на Server.'], + ['Server 不可达会静默跳过,保持本机现状。', 'Если Server недоступен, синхронизация молча пропускается и локальное состояние сохраняется.'], + ['失效(约 30 天)后账号会变成不可用,', 'истекает примерно через 30 дней, после чего аккаунт становится недоступен; '], + ['需要重新粘贴一次新 session。', 'потребуется вставить новую сессию. '], + ['秒 后重置', 'с до сброса'], + ['⚠️ 登录状态冲突警告', '⚠️ Конфликт состояния входа'], + ['检测到官方 Codex 插件中存在未同步的 Token 更新。', 'В официальном Codex обнаружены несинхронизированные обновления токена.'], + ['当前的账号状态与官方文件不一致:', 'Текущее состояние аккаунта не совпадает с официальным файлом:'], + ['直接切换将覆盖官方插件中的当前登录状态,且无法找回这些未同步的更新。', 'Прямое переключение перезапишет текущий вход официального плагина; эти несинхронизированные обновления нельзя будет восстановить.'], + ['确认覆盖并切换', 'Перезаписать и переключить'], + ['以 IDE 为准 (同步状态)', 'Взять состояние из IDE'], + ['导出成功!', 'Экспорт выполнен.'], + ['导出失败:', 'Экспорт не выполнен:'], + ['加载中...', 'Загрузка...'], + ['加载中…', 'Загрузка...'], + ['代理运行中', 'Прокси запущен'], + ['代理未启动', 'Прокси остановлен'], + ['仪表盘', 'Панель'], + ['账号管理', 'Аккаунты'], + ['代理控制', 'Управление прокси'], + ['本地代理服务器', 'Локальный прокси-сервер'], + ['路由', 'Маршруты'], + ['统计', 'Статистика'], + ['缓存', 'Кэш'], + ['设置', 'Настройки'], + ['+ 登录账号', '+ Войти в аккаунт'], + ['+ 添加中转', '+ Добавить relay'], + ['+ 添加路由', '+ Добавить маршрут'], + ['从 IDE 自动导入', 'Автоимпорт из IDE'], + ['同步状态失败:', 'Не удалось синхронизировать состояние:'], + ['切换检查失败:', 'Не удалось проверить переключение:'], + ['保守切换也失败了:', 'Резервное переключение тоже не удалось:'], + ['确认切换失败:', 'Не удалось подтвердить переключение:'], + ['覆盖 ~/.codex/auth.json 失败:', 'Не удалось перезаписать ~/.codex/auth.json:'], + ['后台保活已停用', 'Фоновое продление отключено'], + ['后台保活临时失败', 'Временный сбой фонового продления'], + ['代理已自动切号', 'Прокси автоматически переключил аккаунт. '], + ['检测到封号', 'Обнаружена блокировка аккаунта'], + ['已自动切换', 'Переключение выполнено автоматически'], + ['当前账号', 'Текущий аккаунт'], + ['取消', 'Отмена'], + ['关闭', 'Закрыть'], + ['确认', 'Подтвердить'], + ['保存', 'Сохранить'], + ['删除', 'Удалить'], + ['刷新', 'Обновить'], + ['切换', 'Переключить'], + ['导入', 'Импорт'], + ['导出', 'Экспорт'], + ['添加', 'Добавить'], + ['移除', 'Убрать'], + ['清空', 'Очистить'], + ['复制', 'Скопировать'], + ['已复制', 'Скопировано'], + ['保存并刷新', 'Сохранить и обновить'], + ['测试连接', 'Проверить соединение'], + ['重启 HTTP 服务', 'Перезапустить HTTP-сервис'], + ['写入环境变量', 'Записать переменные окружения'], + ['环境变量', 'Переменные окружения'], + ['手动启动', 'Ручной запуск'], + ['复制命令到终端运行', 'Скопируйте команду и выполните в терминале'], + ['局域网客户端', 'Клиент в локальной сети'], + ['局域网可用', 'Доступно в LAN'], + ['允许局域网访问', 'Разрешить доступ из LAN'], + ['全局代理(CLI + App 全覆盖)', 'Глобальный прокси (CLI + приложение)'], + ['同时写入 ~/.zshrc、launchctl 和 ~/.codex/config.toml,终端 CLI 和 Codex App 均走代理', 'Записывает настройки в ~/.zshrc, launchctl и ~/.codex/config.toml; терминальный CLI и Codex App будут идти через прокси'], + ['定时额度刷新', 'Плановое обновление квот'], + ['自动刷新账号额度', 'Автоматически обновлять квоты аккаунтов'], + ['刷新间隔(分钟/账号)', 'Интервал обновления (минут на аккаунт)'], + ['每个账号之间的刷新间隔', 'Пауза между обновлением отдельных аккаунтов'], + ['每轮刷新账号数', 'Аккаунтов за цикл'], + ['每轮循环刷新多少个账号', 'Сколько аккаунтов обновлять за один цикл'], + ['切号通知', 'Уведомления о переключении'], + ['macOS 系统通知', 'Системные уведомления macOS'], + ['切号时在屏幕右上角弹出系统通知', 'Показывать системное уведомление при переключении аккаунта'], + ['对话内注入通知(实验性)', 'Вставлять уведомление в диалог (экспериментально)'], + ['切号后在 Codex 对话中插入一条切号提示消息。可能影响对话状态。', 'После переключения вставляет сообщение в диалог Codex. Может повлиять на состояние диалога.'], + ['Codex 配置', 'Настройки Codex'], + ['Fast 模式', 'Режим Fast'], + ['更快推理速度,但消耗 2x 额度。', 'Более быстрые ответы, но расход квоты в 2 раза выше.'], + ['更快推理速度,但消耗 2x 额度。当前:已开启', 'Более быстрые ответы, но расход квоты в 2 раза выше. Сейчас включено'], + ['更快推理速度,但消耗 2x 额度。当前:已关闭', 'Более быстрые ответы, но расход квоты в 2 раза выше. Сейчас выключено'], + ['当前:已开启', ' Сейчас включено'], + ['当前:已关闭', ' Сейчас выключено'], + ['关闭 Fast', 'Отключить Fast'], + ['开启 Fast', 'Включить Fast'], + ['Goals 实验特性', 'Экспериментальная функция Goals'], + ['写入 [features] goals = true 到 ~/.codex/config.toml。', 'Записывает [features] goals = true в ~/.codex/config.toml.'], + ['写入 [features] goals = true 到 ~/.codex/config.toml。当前:已开启', 'Записывает [features] goals = true в ~/.codex/config.toml. Сейчас включено'], + ['写入 [features] goals = true 到 ~/.codex/config.toml。当前:已关闭', 'Записывает [features] goals = true в ~/.codex/config.toml. Сейчас выключено'], + ['写入 [features] goals = true 到 ~/.codex/config.toml', 'Записывает [features] goals = true в ~/.codex/config.toml'], + ['关闭 Goals', 'Отключить Goals'], + ['开启 Goals', 'Включить Goals'], + ['进程管理', 'Управление процессами'], + ['终止所有 Codex 进程', 'Завершить все процессы Codex'], + ['强制终止所有运行中的 codex 进程,用于切换代理模式后重启或排错', 'Принудительно завершает запущенные процессы codex для перезапуска после смены режима прокси или диагностики'], + ['终止中...', 'Завершение...'], + ['终止进程', 'Завершить процессы'], + ['智能切号策略', 'Стратегия автопереключения'], + ['5h 配额预防性切号阈值', 'Порог раннего переключения для квоты 5ч'], + ['剩余配额低于此百分比时提前切号(0 = 仅 429 触发,推荐 10)', 'Переключаться заранее, когда остаток ниже этого процента (0 = только при 429, рекомендуется 10)'], + ['周配额预防性切号阈值', 'Порог раннего переключения для недельной квоты'], + ['剩余周配额低于此百分比时提前切号(0 = 仅 429 触发,推荐 5)', 'Переключаться заранее, когда недельный остаток ниже этого процента (0 = только при 429, рекомендуется 5)'], + ['Free 账号保护线', 'Защитный порог Free-аккаунтов'], + ['Free 账号剩余配额低于此百分比时强制切号(0 = 不特殊处理,推荐 35)', 'Принудительно переключаться с Free-аккаунта ниже этого остатка (0 = без особой обработки, рекомендуется 35)'], + ['智能切号允许 FREE 账号', 'Разрешить Free-аккаунты для автопереключения'], + ['点击“切换下一个账号”时,是否允许自动寻找并切到 FREE 账号(默认优先付费账号)', 'Разрешать ли переход на Free-аккаунт при выборе следующего аккаунта (по умолчанию приоритет у платных)'], + ['SSE Bootstrap 嗅探窗口', 'Окно анализа SSE bootstrap'], + ['字节上限', 'Лимит байт'], + ['时间上限', 'Лимит времени'], + ['读到这么多字节还没见到内容事件就放行 codex(默认 32768,越大越能逮住慢上游的限额事件)', 'Если до этого объёма байт не найдено событие с содержимым, запрос пропускается в codex (по умолчанию 32768; большее значение лучше ловит медленные события лимита)'], + ['等首个内容事件最长等多久(默认 8000ms)。配合 SSE keep-alive 心跳,拉到 30000 也不会让 codex 那头超时。', 'Максимальное ожидание первого события с содержимым (по умолчанию 8000 мс). С SSE keep-alive можно увеличить до 30000 без тайм-аута на стороне codex.'], + ['界面外观', 'Внешний вид'], + ['界面配色', 'Цветовая схема'], + ['主界面颜色的色调风格', 'Цветовой стиль основного окна'], + ['经典蓝', 'Классический синий'], + ['玛瑙绿', 'Агатовый зелёный'], + ['暗黑护眼', 'Тёмная тема'], + ['关于', 'О программе'], + ['多账号智能切换 + 本地代理 + 用量统计', 'Умное переключение аккаунтов, локальный прокси и статистика использования'], + ['你好,', 'Здравствуйте,'], + ['用户', 'пользователь'], + ['手机锚生效:', 'Мобильная привязка активна:'], + ['手机锚', 'Мобильная привязка'], + ['Codex.app 仍以', 'Codex.app остаётся онлайн как'], + ['身份在线(手机 bridge 不掉线),', 'через bridge телефона без разрыва,'], + ['代理出口已切到', 'выход прокси переключён на'], + ['磁盘 auth.json 落后:', 'auth.json на диске отстаёт: '], + ['登录状态不一致:', 'Состояние входа не совпадает: '], + ['停在', 'Остался на '], + ['未知账号', 'Неизвестный аккаунт'], + ['代理正在注入当前激活号的 token,', 'прокси подставляет токен активного аккаунта, '], + ['不影响 codex 工作', 'это не мешает работе codex'], + ['关闭代理后 codex 会读到这个号', 'после отключения прокси codex прочитает этот аккаунт'], + ['检测到 IDE 正在使用', 'Обнаружено, что IDE использует'], + ['同步磁盘', 'Синхронизировать диск'], + ['修正激活状态', 'Исправить активное состояние'], + ['立即导入该账号', 'Сразу импортировать этот аккаунт'], + ['授权已失效,请删除后重新登录', 'Авторизация истекла; удалите аккаунт и войдите заново'], + ['失效', 'Недействителен'], + ['与 ~/.codex/auth.json 身份不匹配', 'Не совпадает с идентификатором в ~/.codex/auth.json'], + ['IDE 当前用:', 'IDE сейчас использует:'], + ['用此账号覆盖 IDE', 'Перезаписать IDE этим аккаунтом'], + ['改用 IDE 当前', 'Взять текущий из IDE'], + ['导入 IDE 当前', 'Импортировать текущий из IDE'], + ['切换账号', 'Переключить аккаунт'], + ['暂无账号', 'Аккаунтов нет'], + ['最佳账号推荐', 'Рекомендуемый аккаунт'], + ['推荐账号', 'Рекомендация'], + ['暂无推荐', 'Рекомендаций нет'], + ['一键切换最佳', 'Переключиться на лучший'], + ['查看所有账号', 'Все аккаунты'], + ['导出账号数据', 'Экспорт аккаунтов'], + ['账号总数', 'Всего аккаунтов'], + ['账号信息', 'Данные аккаунта'], + ['账号名称', 'Имя аккаунта'], + ['搜索邮箱...', 'Поиск по email...'], + ['关闭自动重载 IDE', 'Отключить автообновление IDE'], + ['开启自动重载 IDE', 'Включить автообновление IDE'], + ['刷新所有账号配额', 'Обновить квоты всех аккаунтов'], + ['共 ', 'Всего '], + ['点击复制账号', 'Скопировать аккаунт'], + ['该账号已被 OpenAI 封禁', 'Этот аккаунт заблокирован OpenAI'], + ['封号', 'Заблокирован'], + ['登录已失效,可能是 refresh_token 过期、被撤销或会话在其他设备结束', 'Вход истёк: возможно, refresh_token устарел, отозван или сессия завершена на другом устройстве'], + ['需重新登录', 'Нужно войти заново'], + ['该账号 Token 已过期或失效', 'Токен аккаунта истёк или недействителен'], + ['过期', 'Истёк'], + ['主动重置次数(剩余 0 次,无法重置)', 'Активные сбросы: осталось 0, сброс недоступен'], + ['主动重置次数', 'Активные сбросы'], + ['重置中', 'Сброс...'], + ['该账号当前没有可用的主动重置次数。', 'У этого аккаунта нет доступных активных сбросов.'], + ['消耗 1 次会把当前已耗尽的 5H / 周限额窗口立刻清零,此操作不可撤销。若额度还没用到上限,上游会返回「无可重置」且不扣次数。', 'Один сброс сразу обнулит исчерпанное окно 5ч/неделя. Действие необратимо. Если лимит ещё не исчерпан, upstream вернёт «сбрасывать нечего» и попытка не будет списана.'], + ['仍可直接消耗 1 次', 'Можно списать 1 попытку напрямую'], + ['消耗哪条由服务端决定', 'Какое окно сбросить, решает сервер'], + ['将被消耗', 'Будет списано'], + ['立即重置', 'Сбросить сейчас'], + ['已重置', 'Сброшено'], + ['当前没有可重置的限额', 'Сейчас нет лимита для сброса'], + ['没有可用的主动重置次数了', 'Активных сбросов больше нет'], + ['该重置请求已经兑现过了', 'Этот запрос сброса уже использован'], + ['请求过于频繁', 'Слишком частые запросы'], + ['Token 已失效,需要重新登录', 'Токен недействителен, нужен повторный вход'], + ['账号已被封禁', 'Аккаунт заблокирован'], + ['账号已到期', 'Срок аккаунта истёк'], + ['账号今天到期,点击修改', 'Аккаунт истекает сегодня, нажмите для изменения'], + ['点击设置账号到期日', 'Нажмите, чтобы задать срок аккаунта'], + ['日期格式异常,点击修正', 'Некорректный формат даты, нажмите для исправления'], + ['日期内容异常,点击修正', 'Некорректная дата, нажмите для исправления'], + ['近 7 天', 'Ближайшие 7 дней'], + ['清除日期', 'Очистить дату'], + ['账号到期日', 'Дата окончания аккаунта'], + ['到期日期', 'Дата окончания'], + ['编辑备注', 'Изменить заметку'], + ['发送 Codex 邀请', 'Отправить приглашение Codex'], + ['发送邀请', 'Отправить приглашение'], + ['发送中…', 'Отправка...'], + ['请输入 skill 名称', 'Введите имя навыка'], + ['请至少填写 1 个邀请邮箱', 'Укажите хотя бы один email для приглашения'], + ['当前套餐不支持发邀请(free 号没有邀请权限)', 'Текущий тариф не поддерживает приглашения (у Free-аккаунтов нет такого права)'], + ['已发送,邮件已投递到对方邮箱。', 'Отправлено, письмо доставлено на указанный email.'], + ['邀请未成功', 'Приглашение не отправлено'], + ['该邮箱无法被邀请(通常是已注册过 ChatGPT 的老用户,推荐奖励只对新邮箱有效)', 'Этот email нельзя пригласить: обычно это уже зарегистрированный ChatGPT-пользователь, бонус действует только для новых email'], + ['已经给该邮箱发过邀请了(同一邮箱不能重复邀请)', 'На этот email уже отправляли приглашение; повторить нельзя'], + ['邀请链接', 'Ссылка приглашения'], + ['有效;同一邮箱只能邀一次。', 'действительно; один email можно пригласить только один раз.'], + ['邮箱列表(每行一个)', 'Список email, по одному в строке'], + ['邮箱 OTP 批量自动授权', 'Пакетная OTP-авторизация email'], + ['每个邮箱 OTP 超时(秒)', 'Тайм-аут OTP на email (сек)'], + ['开始批量授权', 'Начать пакетную авторизацию'], + ['授权中…', 'Авторизация...'], + ['重试失败', 'Повторить неудачные'], + ['官方 OAuth 授权', 'Официальная OAuth-авторизация'], + ['OpenAI 登录 (推荐)', 'Вход OpenAI (рекомендуется)'], + ['直接通过 OpenAI 官方渠道登录。支持令牌自动续期,多账号切换更稳定,无需再手动更新 `auth.json`。', 'Вход через официальный канал OpenAI. Поддерживает автообновление токена, стабильнее для нескольких аккаунтов и не требует ручного обновления `auth.json`.'], + ['授权将在你系统的默认浏览器中完成,安全可信。', 'Авторизация откроется в браузере по умолчанию.'], + ['复制授权链接(指定浏览器登录)', 'Скопировать ссылку авторизации'], + ['浏览器没跳回来?手动粘贴回调链接', 'Браузер не вернулся в приложение? Вставьте callback-ссылку вручную'], + ['从浏览器地址栏复制完整 URL(包含', 'Скопируйте полный URL из адресной строки браузера, включая'], + ['将从本地官方 Codex 的登录状态', 'Будут извлечены данные входа из локального официального Codex'], + ['中提取认证信息。', ''], + ['从官方导入', 'Импорт из официального Codex'], + ['批量导入文件', 'Пакетный импорт файлов'], + ['自动识别格式,可一次选多个文件:', 'Формат распознаётся автоматически, можно выбрать несколько файлов:'], + ['codex-switcher 原生 accounts.json', 'родной accounts.json codex-switcher'], + ['chatgpt.com 网页登录的 session JSON', 'session JSON из веб-входа chatgpt.com'], + ['Web session 没有', 'В Web session нет'], + ['绕过 Codex 手机验证。支持单个对象、对象数组、或者嵌套容器,会自动递归识别。', 'обходит телефонную проверку Codex. Поддерживаются одиночный объект, массив объектов и вложенные контейнеры; распознавание рекурсивное.'], + ['解析', 'Распознано'], + ['新增账号详情', 'Детали новых аккаунтов'], + ['导入账号详情', 'Детали импортированных аккаунтов'], + ['失败项', 'Ошибки'], + ['失败', 'Ошибка'], + ['新增', 'Добавлено'], + ['同名', 'с тем же именем'], + ['同邮箱已存在的账号会跳过,不覆盖现有 token。', 'Аккаунты с уже существующим email пропускаются, существующие токены не перезаписываются.'], + ['添加中转 (Coding Plan / 通用 Responses 中转)', 'Добавить relay (Coding Plan / универсальный Responses relay)'], + ['选择中转服务', 'Выберите relay-сервис'], + ['选预设 · 填 Key', 'Выберите пресет и вставьте ключ'], + ['选完进入下一步,base URL 已自动填好', 'После выбора переходите дальше; base URL заполнится автоматически'], + ['选择目标账号', 'Выберите целевой аккаунт'], + ['选择会话', 'Выберите сессию'], + ['从最近会话挑选', 'Выбрать из недавних сессий'], + ['手动输入 ID', 'Ввести ID вручную'], + ['绑定当前活跃会话', 'Привязать текущую активную сессию'], + ['加载最近会话…', 'Загрузка недавних сессий...'], + ['最近没有活跃会话', 'Недавних активных сессий нет'], + ['粘贴 session_id(UUID 格式,36 字符)', 'Вставьте session_id (UUID, 36 символов)'], + ['看起来不像 UUID。请检查长度是否为 36 字符、包含 4 个连字符。', 'Это не похоже на UUID. Проверьте длину 36 символов и 4 дефиса.'], + ['把会话钉到账号', 'Закрепить сессию за аккаунтом'], + ['指定 codex 会话强制使用某个账号,绕过全局自动切号。', 'Заставляет указанную сессию codex использовать выбранный аккаунт, обходя глобальное автопереключение.'], + ['当前绑定账号', 'Текущая привязка'], + ['✓ 当前绑定', '✓ Текущая привязка'], + ['条规则,启用', 'правил, включено'], + ['还没有路由规则。', 'Правил маршрутизации пока нет.'], + ['没有匹配的最近会话。试试调整搜索词或切换到「手动输入 ID」。', 'Подходящих недавних сессий нет. Измените поиск или переключитесь на ручной ввод ID.'], + ['没有匹配', 'Нет совпадений'], + ['删除后该 session 将恢复全局自动切号。', 'После удаления эта session вернётся к глобальному автопереключению.'], + ['确认删除路由', 'Удалить маршрут?'], + ['目标账号已被删除', 'Целевой аккаунт удалён'], + ['Skills', 'Навыки'], + ['Skill', 'Навык'], + ['skill', 'навык'], + ['Usage Monitor', 'Монитор квот'], + ['Proxy ON', 'Прокси включён'], + ['Proxy OFF', 'Прокси выключен'], + ['SESSION', 'СЕССИЯ'], + ['WEEKLY', 'НЕДЕЛЯ'], + ['COST USAGE', 'РАСХОДЫ'], + ['TOKEN USAGE', 'ТОКЕНЫ'], + ['Remaining', 'Осталось'], + ['Spent', 'Потрачено'], + ['Tokens', 'Токены'], + ['HEALTHY', 'НОРМА'], + ['WARNING', 'ВНИМАНИЕ'], + ['CRITICAL', 'КРИТИЧНО'], + ['Resets in', 'Сброс через'], + ['In', 'Вход'], + ['Out', 'Выход'], + ['Vs 上月', 'К прошлому месяцу'], + ['手机锚 = 当前号', 'Мобильная привязка = текущий аккаунт'], + ['代理出口 ', 'выход прокси '], + ['需重登', 'Нужен вход'], + ['Google 当前', 'Текущий Google'], + ['模型 当前', 'Текущая модель'], + ['部分模型当前', 'Часть моделей текущие'], + ['Kimi 当前', 'Текущий Kimi'], + ['DeepSeek 当前', 'Текущий DeepSeek'], + ['当前模型:', 'Текущие модели: '], + ['Token 配额', 'Квота токенов'], + ['Spark 周', 'Spark нед.'], + ['三方', 'Сторонние'], + ['未获取数据', 'Данные не получены'], + ['次数未知', 'Сбросы неизвестны'], + ['上游未返回重置次数,不代表次数已清空。点击查询银行明细。', 'Upstream не вернул число сбросов; это не значит, что они закончились. Нажмите, чтобы проверить детали.'], + ['当前已被限流(额度桶为 0)——现在用一次主动重置回收最大,点击查看明细', 'Сейчас действует лимит (окно квоты равно 0) - активный сброс даст максимальный эффект. Нажмите, чтобы посмотреть детали.'], + ['点击查看所有主动重置次数(含各自到期时间),再消耗一次重置限额窗口', 'Показать все активные сбросы и сроки их действия, затем списать один сброс окна лимита'], + ['登录账号 (OpenAI / Google / 导入)', 'Войти в аккаунт (OpenAI / Google / импорт)'], + ['刷新 Codex 当前账号额度', 'Обновить квоту текущего аккаунта Codex'], + ['刷新当前列表额度', 'Обновить квоты текущего списка'], + ['ACCOUNT INVITE', 'ПРИГЛАШЕНИЕ'], + ['OFFER', 'ПРЕДЛОЖЕНИЕ'], + ['RECIPIENTS', 'ПОЛУЧАТЕЛИ'], + ['RESULT', 'РЕЗУЛЬТАТ'], + ['HISTORY', 'ИСТОРИЯ'], + ['Session 导入', 'Импорт Session'], + ['选择文件并导入', 'Выбрать файлы и импортировать'], + ['解析并导入', 'Распознать и импортировать'], + ['导入中…', 'Импорт...'], + ['跳过', 'Пропущено'], + ['仅 RT,首次请求自动 refresh', 'только RT, при первом запросе будет auto-refresh'], + ['✓ ready', '✓ готово'], + ['含 refresh_token', 'есть refresh_token'], + ['无 refresh_token,到期后需重新导入', 'нет refresh_token, после истечения нужно импортировать заново'], + ['id_token 合成', 'id_token создан автоматически'], + ['注意:', 'Внимание:'], + ['Plus 账号能正常调用模型,Free 账号即使导入也无 API 权限。', 'Plus-аккаунты смогут вызывать модели; у Free-аккаунтов API-доступа нет даже после импорта.'], + ['粘贴', 'Вставьте'], + ['网页登录的 session JSON', 'session JSON из веб-входа'], + ['连接 Google 账号', 'Подключить Google-аккаунт'], + ['等待浏览器授权…', 'Ожидание авторизации в браузере...'], + ['授权后 Gemini 模型通过 Codex Switcher 原生路由;不会修改 Codex 的 OpenAI 登录身份。', 'После авторизации модели Gemini пойдут через родную маршрутизацию Codex Switcher; OpenAI-вход Codex изменён не будет.'], + ['不打开默认浏览器,把 Google 授权链接复制到剪贴板', 'Не открывать браузер по умолчанию, а скопировать ссылку Google-авторизации в буфер'], + ['导入中转站账号', 'Импорт relay-аккаунта'], + ['链接里携带的', 'В ссылке передан'], + ['未在内置白名单中,已忽略。账号仍可正常使用,', 'не входит во встроенный белый список и был проигнорирован. Аккаунт можно использовать,'], + ['但额度查询不会自动进行。导入后可在账号详情中手动选 usage preset。', 'но квоты не будут обновляться автоматически. После импорта можно вручную выбрать usage preset в деталях аккаунта.'], + ['导入失败:', 'Импорт не выполнен:'], + ['导入中转站', 'Импортировать relay'], + ['预设', 'Пресет'], + ['选预设自动填 base_url,贴 API Key 即可。也支持', 'Выберите пресет, base_url заполнится автоматически, затем вставьте API Key. Также поддерживается'], + ['余额查询策略', 'Стратегия проверки баланса'], + ['不拉取', 'Не запрашивать'], + ['登录 platform.stepfun.com 后复制 Oasis-Token,也可粘贴 Cookie header', 'Войдите на platform.stepfun.com и скопируйте Oasis-Token; можно вставить и Cookie header'], + ['登录 platform.xiaomimimo.com 后,从 Network 复制 Cookie header', 'Войдите на platform.xiaomimimo.com и скопируйте Cookie header из Network'], + ['该凭证只用于查询 Step Plan 额度,不会参与模型请求;实际调用仍使用上面的 Step API Key。', 'Эти данные нужны только для проверки квоты Step Plan и не участвуют в запросах к моделям; реальные вызовы используют Step API Key выше.'], + ['这里的 Cookie 只用于查询 Token Plan 用量,不会参与模型请求。实际调用仍使用上面的 tp-key。', 'Этот Cookie нужен только для проверки расхода Token Plan и не участвует в запросах к моделям; реальные вызовы используют tp-key выше.'], + ['上游协议', 'Протокол upstream'], + ['中转站讲什么 wire format', 'какой wire format понимает relay'], + ['模型兜底', 'Резервная модель'], + ['客户端发的 model 没命中映射时,统一替换成这个', 'если model от клиента не найден в карте, заменить на это значение'], + ['模型映射表', 'Таблица сопоставления моделей'], + ['每行', 'Каждая строка'], + ['客户端model=中转站model', 'client_model=relay_model'], + ['如 glm-5.1(留空 = 透传不替换)', 'например glm-5.1 (пусто = передавать без замены)'], + ['修改 StepFun 额度凭证', 'Изменить данные проверки квоты StepFun'], + ['修改 MiMo 配额 Cookie', 'Изменить Cookie проверки квоты MiMo'], + ['账号:', 'Аккаунт: '], + ['后复制', 'и скопируйте'], + ['后,从 Network 请求里复制', 'и скопируйте из запроса Network'], + ['也可粘贴包含它的 Cookie header。', 'также можно вставить Cookie header, где он содержится.'], + ['仓库', 'Репозитории'], + ['发现', 'Поиск'], + ['已安装', 'Установлено'], + ['未链接', 'Не подключено'], + ['已链接', 'Подключено'], + ['暂无已安装的 skill', 'Установленных навыков пока нет'], + ['无描述', 'Без описания'], + ['正在扫描 GitHub 仓库...', 'Сканирование GitHub-репозиториев...'], + ['点击"刷新"从仓库发现 skill', 'Нажмите «Обновить», чтобы найти навыки в репозиториях'], + ['确认卸载', 'Подтвердите удаление'], + ['即将卸载', 'Будет удалён'], + ['此操作将从所有 CLI 目录移除该 skill。', 'Это удалит навык из всех CLI-каталогов.'], + ['以确认:', 'для подтверждения:'], + ['卸载', 'Удалить'], + ['发现失败:', 'Поиск не выполнен:'], + ['安装失败:', 'Установка не выполнена:'], + ['卸载失败:', 'Удаление не выполнено:'], + ['无法读取 SKILL.md', 'Не удалось прочитать SKILL.md'], + ['操作失败:', 'Операция не выполнена:'], + ['同步完成', 'Синхронизация завершена'], + ['同步失败:', 'Синхронизация не выполнена:'], + ['同步中...', 'Синхронизация...'], + ['同步到 Server', 'Синхронизировать на Server'], + ['将本机 SSOT 下所有 skill 推送到 Server(按黑名单跳过)', 'Отправить все навыки из локального SSOT на Server с учетом чёрного списка'], + ['全量同步', 'Полная синхронизация'], + ['搜索 skill...', 'Поиск навыка...'], + ['推送', 'Отправлено'], + ['已断开链接', 'отключено от Skills'], + ['已链接到 Skills', 'подключено к Skills'], + ['已卸载', 'Удалено'], + ['自动导入', 'Автоимпорт'], + ['个新 skill', 'новых навыков'], + [' 个 skill', ' навыков'], + ['已添加', 'Добавлено'], + ['套餐待同步', 'Тариф ожидает синхронизации'], + ['到期,点击修改', 'истекает, нажмите для изменения'], + ['今天到期', 'истекает сегодня'], + ['今天', 'сегодня'], + ['还有 ', 'осталось '], + [' 天到期,点击修改', ' дн. до окончания, нажмите для изменения'], + ['到期日 ', 'Дата окончания '], + ['点击修改', 'нажмите для изменения'], + ['本地已保存,但同步 Server 失败:', 'Локально сохранено, но синхронизация с Server не выполнена:'], + ['已打开 codex 终端', 'терминал codex открыт'], + ['启动失败:', 'Не удалось запустить:'], + ['重置失败:', 'Сброс не выполнен:'], + ['消耗了到期', 'списан сброс со сроком до'], + ['的那条', ''], + ['触发 ', 'Запущено '], + ['个账号 reset 后自动刷新', 'аккаунтов для автообновления после reset'], + ['刷新失败:', 'Обновление не выполнено:'], + ['天 ', ' дн. '], + ['时 ', ' ч '], + ['分', ' мин'], + ['重试中', 'Повтор...'], + ['合并到同邮箱旧账号', 'объединён со старым аккаунтом с тем же email'], + ['更新', 'Обновлено'], + ['已刷新额度', 'квота обновлена'], + ['推送 Server 成功', 'успешно отправлен на Server'], + ['推送失败:', 'Отправка не выполнена:'], + ['Google 当前账号已切换为', 'Текущий Google-аккаунт переключён на'], + ['Google 切号失败:', 'Не удалось переключить Google-аккаунт:'], + ['已将 ', 'Назначен '], + ['设为其模型的当前账号', 'текущим аккаунтом для его моделей'], + ['Codex / Google 不变', 'Codex / Google не меняются'], + ['模型切号失败:', 'Не удалось переключить модель:'], + ['登录已失效:', 'Вход истёк:'], + ['已过期或被撤销', 'истёк или был отозван'], + ['请重新登录', 'войдите заново'], + ['刷新令牌', 'Обновить токен'], + ['保存额度凭证失败:', 'Не удалось сохранить данные проверки квоты:'], + ['点击修改额度查询凭证', 'Изменить данные проверки квоты'], + ['额度余额 ', 'Баланс квоты '], + ['来自 /wham/usage 的 credits.balance', 'Значение credits.balance из /wham/usage'], + ['请刷新该账号额度以查询 credits.balance', 'Обновите квоту аккаунта, чтобы получить credits.balance'], + ['IDE 自动重载仅支持 macOS,请手动重载 IDE', 'Автообновление IDE доступно только на macOS; перезагрузите IDE вручную'], + ['配额状态', 'Состояние квот'], + ['同步/保活', 'Синхронизация/продление'], + ['操作', 'Действия'], + ['点击打开 ', 'Открыть '], + ['刷新模型额度', 'Обновить квоты моделей'], + ['切换 Google 当前账号(不影响 Codex 当前账号)', 'Переключить текущий Google-аккаунт (не влияет на текущий аккаунт Codex)'], + ['邀请同事使用 ChatGPT 桌面版', 'Пригласить коллегу в ChatGPT Desktop'], + ['邀请朋友使用 ChatGPT 桌面版', 'Пригласить друга в ChatGPT Desktop'], + ['确定要永久删除账号', 'Точно удалить аккаунт навсегда'], + ['吗?', '?'], + ['此操作不可恢复,删除后有关该账号的本地授权信息将被清除。', 'Действие необратимо: локальные данные авторизации этого аккаунта будут удалены.'], + ['ChatGPT / Codex 目前没有提供可靠的订阅到期日接口,因此这里由你手工维护。它不会修改登录 Token、不会当作额度重置时间,也不会触发自动切号。留空保存可清除日期。', + 'ChatGPT / Codex сейчас не предоставляет надёжный API даты окончания подписки, поэтому дата ведётся вручную. Она не меняет токен входа, не считается временем сброса квоты и не запускает автопереключение. Сохраните пустое поле, чтобы очистить дату.'], + ['保存中…', 'Сохранение...'], + ['系统默认自动管理全部订阅号,并以 /wham/usage 返回的 primary_window 实际时长判断 5H 或 7D,不绑定套餐名称。首次遇到无法确认是否激活的 100% 窗口时只发一次极小请求;之后按固定 reset_at 到点触发。这里可以单独关闭该账号,client/solo 模式由 Server 单端执行。', + 'По умолчанию система автоматически ведёт все подписочные аккаунты и определяет окно 5ч или 7д по фактической длительности primary_window из /wham/usage, без привязки к названию тарифа. При первом неопределённом окне 100% отправляется один минимальный запрос; дальше запуск идёт по reset_at. Здесь можно отключить это для отдельного аккаунта; в client/solo режиме задачу выполняет только Server.'], + ['当前可用次数无法确认,不代表已清空。请稍后重试查询。', 'Текущее число доступных сбросов не удалось подтвердить; это не значит, что они закончились. Повторите проверку позже.'], + ['次,按到期时间排序(最早在前)。所有次数', 'шт., отсортированы по сроку действия (самые ранние сверху). Все сбросы '], + ['等价', 'равнозначны'], + ['区别仅到期时间;', 'отличается только срок действия;'], + ['剩 ', 'осталось '], + ['不扣次数', 'не списывать сброс'], + ['重新查询', 'Проверить заново'], + ['正在重置…', 'Сброс...'], + ['或直接粘贴 token', 'или вставьте token напрямую'], + ['四段RT', 'RT в 4 частях'], + ['默认 / 上游懂 codex /v1/responses', 'по умолчанию / upstream понимает codex /v1/responses'], + ['StepFun 额度查询需要粘贴 platform.stepfun.com 的 Oasis-Token;如果暂时不查配额,请把余额查询策略改成“不拉取”。', + 'Для проверки квоты StepFun нужен Oasis-Token с platform.stepfun.com. Если квоты пока не нужны, переключите стратегию проверки баланса на «Не запрашивать».'], + ['MiMo 配额查询需要粘贴 platform.xiaomimimo.com 的 Cookie;如果暂时不查配额,请把余额查询策略改成“不拉取”。', + 'Для проверки квоты MiMo нужен Cookie с platform.xiaomimimo.com. Если квоты пока не нужны, переключите стратегию проверки баланса на «Не запрашивать».'], + ['已获取授权码,正在交换令牌...', 'Код авторизации получен, обмен токена...'], + ['授权成功!账号已添加。', 'Авторизация успешна, аккаунт добавлен.'], + ['已获取 Google 授权码,正在验证账号和项目...', 'Код авторизации Google получен, проверка аккаунта и проекта...'], + ['Google Antigravity 账号已添加。', 'Аккаунт Google Antigravity добавлен.'], + ['请输入账号名称', 'Введите имя аккаунта'], + ['正在启动官方浏览器授权...', 'Запуск официальной авторизации в браузере...'], + ['请在打开的浏览器窗口中完成 OpenAI 授权...', 'Завершите авторизацию OpenAI в открывшемся браузере...'], + ['授权链接已复制,请粘贴到目标浏览器完成授权,回调会自动回到本应用...', 'Ссылка авторизации скопирована. Вставьте её в нужный браузер; callback вернётся в приложение автоматически...'], + ['自动复制失败。请在下方选择完整链接手动复制,或重试复制。', 'Автокопирование не удалось. Выделите полную ссылку ниже вручную или повторите копирование.'], + ['正在生成授权链接...', 'Создание ссылки авторизации...'], + ['正在启动 Google Antigravity 授权...', 'Запуск авторизации Google Antigravity...'], + ['请在浏览器中完成 Google 授权...', 'Завершите авторизацию Google в браузере...'], + ['账号导入文件', 'Файлы импорта аккаунтов'], + ['所有文件', 'Все файлы'], + ['请粘贴 ChatGPT session JSON', 'Вставьте ChatGPT session JSON'], + ['已提交回调链接,正在交换令牌...', 'Callback-ссылка отправлена, обмен токена...'], + ['添加账号', 'Добавить аккаунт'], + ['带 ', 'с '], + ['处理中...', 'Обработка...'], + ['导入当前账号', 'Импортировать текущий аккаунт'], + ['立即登录 OpenAI', 'Войти в OpenAI сейчас'], + ['提交中...', 'Отправка...'], + ['开始授权', 'Начать авторизацию'], + ['GLM 自家 quota 接口', 'родной quota API GLM'], + ['Kimi 编程套餐 5H / 7D', 'Kimi Coding Plan 5ч / 7д'], + ['MiMo 控制台 Cookie', 'Cookie консоли MiMo'], + ['StepFun 控制台 Oasis-Token', 'Oasis-Token консоли StepFun'], + ['StepFun 额度凭证', 'Данные проверки квоты StepFun'], + ['基于 new-api / CLIProxyAPI / sub2api 的第三方中转,原生 /v1/responses', 'Сторонний relay на базе new-api / CLIProxyAPI / sub2api, родной /v1/responses'], + ['厂商编程订阅,按服务支持选择 Responses 或 Chat Completions', 'Поставщик coding-подписки; выберите Responses или Chat Completions по поддержке сервиса'], + ['厂商按量付费 API(DeepSeek / Kimi / 通义 / OpenRouter 等)', 'API поставщика с оплатой по расходу (DeepSeek / Kimi / Tongyi / OpenRouter и др.)'], + ['手动填 base_url', 'Заполнить base_url вручную'], + ['保存后直接读取 StepFun /models 返回的原生 step-* 模型,在 Codex 模型列表中选择。', 'После сохранения будут прочитаны родные step-* модели из StepFun /models, их можно выбрать в списке моделей Codex.'], + ['保存后可在 Codex 中选择此中转的模型,无需切换 ChatGPT 账号。', 'После сохранения модели этого relay можно выбирать в Codex без переключения ChatGPT-аккаунта.'], + ['预设仅提供默认地址;使用自有中转时,请填写该中转的 API 地址和对应 Key。', 'Пресет задаёт только адрес по умолчанию; для собственного relay укажите его API-адрес и ключ.'], + ['填写此 API 实际支持的模型 ID', 'Укажите ID модели, которую реально поддерживает этот API'], + ['会显示在 Codex 模型列表;高级设置可添加其他模型。', 'Будет показана в списке моделей Codex; другие модели можно добавить в расширенных настройках.'], + ['需 Cookie', 'нужен Cookie'], + ['Step Plan 控制台 Token', 'Token консоли Step Plan'], + ['从 platform.stepfun.com Cookie 复制 Oasis-Token,或直接粘贴 token', 'Скопируйте Oasis-Token из Cookie на platform.stepfun.com или вставьте token напрямую'], + ['模型来源', 'Источник моделей'], + ['高级设置(模型兜底 / 映射表)', 'Расширенные настройки (резервная модель / таблица сопоставления)'], + ['Step Plan 不使用模型映射;模型列表由官方 API 返回,保存后刷新 Codex 模型列表即可。', 'Step Plan не использует сопоставление моделей; список возвращает официальный API, после сохранения обновите список моделей Codex.'], + ['客户端发的 model 未命中映射时统一替换', 'Единая замена, если model от клиента не найдена в сопоставлении'], + ['API Key 不能为空', 'API Key не может быть пустым'], + ['StepFun 额度查询需要粘贴 platform.stepfun.com 的 Oasis-Token;不查配额请把策略改成「不拉取」。', 'Для проверки квоты StepFun нужен Oasis-Token с platform.stepfun.com; если квоты не нужны, выберите стратегию «Не запрашивать».'], + ['MiMo 配额查询需要粘贴 platform.xiaomimimo.com 的 Cookie;不查配额请把策略改成「不拉取」。', 'Для проверки квоты MiMo нужен Cookie с platform.xiaomimimo.com; если квоты не нужны, выберите стратегию «Не запрашивать».'], + ['账号于 ', 'Аккаунт истекает '], + ['本地已保存,但同步 Server 失败:', 'Локально сохранено, но синхронизация с Server не выполнена: '], + ['启动失败:', 'Не удалось запустить: '], + ['重置失败:', 'Сброс не выполнен: '], + ['切号失败:', 'Не удалось переключить аккаунт: '], + ['模型切号失败:', 'Не удалось переключить модель: '], + ['登录已失效:', 'Вход истёк: '], + ['导入中...', 'Импорт...'], + ['注意:', 'Внимание:'], + ['(带', ' (с '], + ['添加中转', 'Добавить relay'], + ['余额 ', 'Баланс '], + ['✓ 已选', '✓ Выбрано'], + ['一键使用 →', 'Использовать →'], + ['没有账号。先去「账号管理」添加。', 'Аккаунтов нет. Сначала добавьте их в разделе «Аккаунты».'], + ['例如:GLM 跑文档', 'Например: GLM для документов'], + ['在路由列表里显示这个名字,方便识别。', 'Это имя будет показано в списке маршрутов, чтобы его было проще узнать.'], + ['准备就绪 — 点「添加路由」', 'Готово — нажмите «Добавить маршрут»'], + ['选好会话和账号即可添加', 'Выберите сессию и аккаунт, затем добавьте маршрут'], + ['重置未知', 'Сброс неизвестен'], + ['待刷新', 'Нужно обновить'], + ['上游未提供重置时间', 'Upstream не передал время сброса'], + ['重置时间:', 'Время сброса: '], + ['7D 额度已耗尽,请展开查看', 'Квота 7D исчерпана, раскройте подробности'], + ['7D耗尽', '7д исчерпано'], + ['暂无模型额度,点击刷新', 'Квот моделей пока нет, нажмите обновить'], + ['收起全部模型', 'Свернуть все модели'], + ['查看全部模型', 'Показать все модели'], + ['全部模型额度', 'Квоты всех моделей'], + ['剩余额度 · 重置倒计时', 'Остаток квоты · обратный отсчёт до сброса'], + ['默认优先显示 5H;5H 与 7D 同时生效,同组模型共享额度。未获取的窗口显示未知。', 'По умолчанию сначала показано 5ч; 5ч и 7д действуют одновременно, модели одной группы делят квоту. Не полученные окна отображаются как неизвестные.'], + ['模型额度列表', 'Список квот моделей'], + ['早期数据·无 account', 'старые данные · без account'], + ['正在切换...', 'Переключение...'], + ['新账号', 'Новый аккаунт'], + ['复制中…', 'Копирование...'], + ['复制此链接', 'Скопировать эту ссылку'], + ['端口已更新(重启代理后生效)', 'Порт обновлён (вступит в силу после перезапуска прокси)'], + ['保存失败:', 'Не удалось сохранить:'], + ['新终端窗口生效', 'вступит в силу в новом окне терминала'], + ['启动代理', 'Запустить прокси'], + ['关闭代理', 'Остановить прокси'], + ['复制命令到 PowerShell 运行', 'Скопировать команду для запуска в PowerShell'], + ['client 模式:本机不跑 oauth,改为从 Server /quotas 自动同步 cached_quota(约 5 分钟一轮)', 'Режим client: локальный oauth не запускается; cached_quota автоматически синхронизируется из Server /quotas примерно раз в 5 минут'], + ['按最后更新时间排序,自动循环刷新所有账号的配额数据', 'Сортировать по времени последнего обновления и циклически обновлять квоты всех аккаунтов'], + ['邀请同事加入工作区,奖励按官方活动规则发放', 'Пригласите коллегу в workspace; бонус начисляется по официальным правилам акции'], + ['邀请朋友加入,奖励按官方活动规则发放', 'Пригласите друга; бонус начисляется по официальным правилам акции'], + ['关闭邀请窗口', 'Закрыть окно приглашения'], + ['发邀账号', 'Аккаунт отправителя'], + ['当前活动', 'Текущая акция'], + ['活动范围', 'Область акции'], + ['个人账号', 'Личный аккаунт'], + ['工作区', 'Workspace'], + ['查询中…', 'Проверка...'], + ['刷新资格', 'Обновить право на приглашения'], + ['正在查询活动资格…', 'Проверка права на участие...'], + ['每位符合条件的邀请奖励', 'Бонус за каждого подходящего приглашённого'], + ['当前账号暂未开放', 'Для текущего аккаунта пока недоступно'], + ['当前账号暂未开放此邀请活动。', 'Для текущего аккаунта эта акция приглашений пока недоступна.'], + ['可发送邮箱', 'Можно отправить email'], + ['未提供', 'Не предоставлено'], + ['接口未提供实际奖励金额,不能根据活动编号推断为 500 或 1000。', 'API не передал фактический размер бонуса; нельзя выводить 500 или 1000 только по номеру акции.'], + ['接口未提供实际奖励金额,不能根据活动编号推断奖励数额。', 'API не передал фактический размер бонуса; нельзя выводить сумму по номеру акции.'], + ['活动奖励不等于当前余额;对方接受邀请并完成官方要求后才会到账。', 'Бонус акции не равен текущему балансу; он поступит только после принятия приглашения и выполнения официальных условий.'], + ['当前奖励名额为 0,已禁止发送邀请。', 'Доступных бонусных мест 0, отправка приглашений отключена.'], + ['填写受邀邮箱', 'Введите email получателей'], + ['奖励名额已用完', 'Бонусные места закончились'], + ['最多发送 ', 'Можно отправить максимум '], + ['受邀邮箱', 'Email получателей'], + ['每行一个邮箱,也可用逗号或分号分隔', 'Один email на строку; можно разделять запятыми или точками с запятой'], + ['我已确认收件人及奖励条件,发送邀请不代表奖励已到账。', 'Я проверил получателей и условия бонуса; отправка приглашения не означает, что бонус уже начислен.'], + ['发送结果', 'Результат отправки'], + ['邀请记录已创建', 'Запись приглашения создана'], + ['未返回已创建的邀请,请查询记录确认。', 'Созданное приглашение не вернулось в ответе; проверьте историю.'], + ['本次提交已结束。需要再次邀请时,请先核对记录,再重新打开此窗口。', 'Эта отправка завершена. Для нового приглашения сначала проверьте историю, затем откройте окно снова.'], + ['邀请记录', 'История приглашений'], + ['查询近 90 天', 'Проверить последние 90 дней'], + ['近 90 天没有邀请记录。', 'За последние 90 дней приглашений нет.'], + ['上游未提供状态', 'Upstream не передал статус'], + ['加载更多', 'Загрузить ещё'], + ['奖励到账以官方记录为准', 'Начисление бонуса определяется официальной историей'], + ['邀请额度', 'Квота приглашений'], + ['刷新邀请额度', 'Обновить квоту приглашений'], + ['正在查询邀请资格…', 'Проверка права на приглашения...'], + ['每位奖励', 'Бонус за человека'], + ['上次数据已过期,请刷新额度', 'Предыдущие данные устарели, обновите квоту'], + ['剩余 ', 'Осталось '], + ['加载路由规则…', 'Загрузка правил маршрутизации...'], + ['删除路由', 'Удалить маршрут'], + ['点击复制完整 session_id', 'Скопировать полный session_id'], + ['✅ 设置已保存', '✅ Настройки сохранены'], + ['❌ 保存失败:', '❌ Не удалось сохранить:'], + ['生成失败:', 'Не удалось создать: '], + ['已切换到 ', 'Переключено на '], + ['已与 Server 一致,无需切换', 'Уже совпадает с Server, переключение не требуется'], + ['使用 ', 'Используется '], + ['远端账号数 ', 'аккаунтов на удалённой стороне: '], + ['推送全部账号到 Server', 'Отправить все аккаунты на Server'], + ['已上传 ', 'Загружено '], + ['从 Server 拉取全部账号', 'Загрузить все аккаунты с Server'], + ['已合并 ', 'Объединено '], + ['从 Server 同步所有 token', 'Синхронизировать все token с Server'], + ['已写 auth.json', 'auth.json записан'], + ['✅ 修复成功!\n\n现在请尝试重新打开 Codex App。', '✅ Исправлено.\n\nТеперь попробуйте снова открыть Codex App.'], + ['保存中...', 'Сохранение...'], + ['保存设置', 'Сохранить настройки'], + ['client 模式:保活由 Server 负责,本机已强制关闭(避免双路刷新撞飞 refresh_token)', 'Режим client: продление выполняет Server, локально оно принудительно отключено, чтобы два процесса не испортили refresh_token'], + ['Server 负责', 'Отвечает Server'], + ['已开启', 'Включено'], + ['已关闭', 'Отключено'], + ['执行中...', 'Выполнение...'], + ['立即重启', 'Перезапустить сейчас'], + ['更换', 'Заменить'], + ['选择绑定账号', 'Выбрать привязанный аккаунт'], + ['隔离属性修复仅适用于 macOS', 'Исправление quarantine-атрибута доступно только на macOS'], + ['修复中...', 'Исправление...'], + ['立即修复', 'Исправить сейчас'], + ['当前没有 ChatGPT 订阅号', 'Сейчас нет ChatGPT-аккаунтов с подпиской'], + ['没有匹配的账号', 'Подходящих аккаунтов нет'], + ['扫描中...', 'Сканирование...'], + ['个人活动', 'Личная акция'], + ['工作区活动', 'Акция workspace'], + ['个使用额度', 'кредитов использования'], + ['个工作区额度', 'кредитов workspace'], + ['次限额重置', 'сбросов лимита'], + ['活动未提供奖励数额', 'Акция не передала размер бонуса'], + ['发送上限', 'Лимит отправки'], + ['奖励名额', 'Бонусных мест'], + ['250 个使用额度', '250 кредитов использования'], + ['可以 отправить email', 'Можно отправить email'], + ['Codex CLI 通过代理连接 OpenAI,支持无中断自动切号和 429 智能重试', + 'Codex CLI подключается к OpenAI через прокси; поддерживает переключение аккаунтов без обрыва и умный повтор при 429'], + ['auto:代理开启时自动走热切(不写 auth.json,不杀进程,仅刷 store.current)。代理关闭时回退到冷切。cold:无论如何都冷切。', + 'auto: при включённом прокси используется горячее переключение (без записи auth.json, без завершения процессов, обновляется только store.current). При выключенном прокси используется холодное переключение. cold: всегда холодное переключение.'], + ['代理开启时自动走热切', 'при включённом прокси используется горячее переключение'], + ['不写 auth.json,不杀进程,仅刷 store.current', 'без записи auth.json, без завершения процессов, обновляется только store.current'], + ['代理关闭时回退到冷切', 'при выключенном прокси используется холодное переключение'], + ['无论如何都冷切', 'всегда холодное переключение'], + ['开启(默认):current 是中转 / Coding Plan / 三方 API 时,遇到 401/429/quota 自动切到健康的订阅号,避免请求卡死。关闭后这类号出错会把错误透传给客户端,不偷换。', + 'Включено по умолчанию: если current это relay / Coding Plan / сторонний API, при 401/429/quota приложение автоматически переключится на исправный подписной аккаунт, чтобы запрос не завис. Если выключить, такие ошибки будут передаваться клиенту без скрытой подмены аккаунта.'], + ['关闭(默认):用订阅号时自动切号 / affinity 不会路由到中转 / Coding Plan / 三方 API,避免偷扣余额。开启后这类号跟订阅号同等参与轮询(量大但要确认你愿意花对应套餐的额度)。', + 'Выключено по умолчанию: при работе с подписным аккаунтом автопереключение и affinity не будут маршрутизировать запросы в relay / Coding Plan / сторонний API, чтобы случайно не списывать баланс. Если включить, эти аккаунты участвуют в выборе наравне с подписками; используйте только если готовы тратить их квоту.'], + ['开启后 HTTP 也跟 WebSocket 同路,本机直接打上游,不再经 Server 转发;access_token 仍从 Server 拉。适合 Server 出口不稳但本机出口稳的场景。', + 'После включения HTTP пойдёт тем же путём, что и WebSocket: этот компьютер обращается к upstream напрямую, без пересылки через Server; access_token всё равно берётся с Server. Полезно, если выход Server нестабилен, а локальный выход стабилен.'], + ['开启后本机自己决定 current(不被 Server `/current` 反向同步覆盖),`~/.codex/auth.json` 也由本机自己写。等价于旧 solo 模式(已并入 client)。', + 'После включения этот компьютер сам выбирает current (Server `/current` не перезаписывает его обратной синхронизацией), и сам пишет `~/.codex/auth.json`. Это эквивалент старого solo-режима, теперь встроенного в client.'], + ['off=独立;server=Server 侧提供 API;client=本机,rt 旋转走 Server。', + 'off = автономно; server = эта сторона предоставляет API; client = этот компьютер, ротация RT идёт через Server.'], + ['老 solo 模式已合并到 client(自动迁移:solo → client + 本机直连上游 + 本机管 current)。', + 'Старый solo-режим объединён с client (автомиграция: solo -> client + локальный прямой upstream + локальное управление current).'], + ['磁盘 ~/.codex/auth.json 永远跟随此号,Codex.app 手机远程连接绑定此号;切到其他号时 disk 不动、proxy 出口照切(手机 bridge 不掉线)。仅 ChatGPT 订阅号可绑定。', + '~/.codex/auth.json на диске всегда следует этому аккаунту, мобильное удалённое подключение Codex.app привязано к нему. При переходе на другие аккаунты disk не меняется, меняется только выход proxy (мобильный bridge не отваливается). Привязать можно только подписной аккаунт ChatGPT.'], + ['磁盘 ~/.codex/auth.json 永远跟随此号,Codex.app 手机远程连接绑定此号;', + '~/.codex/auth.json на диске всегда следует этому аккаунту, мобильное удалённое подключение Codex.app привязано к нему; '], + ['切到其他号时 disk 不动、proxy 出口照切(手机 bridge 不掉线)。', 'при переходе на другие аккаунты disk не меняется, меняется только выход proxy (мобильный bridge не отваливается).'], + ['仅 ChatGPT 订阅号可绑定。', 'Привязать можно только подписной аккаунт ChatGPT.'], + ['原理', 'Принцип'], + ['每次切号前后强制抓 quota 写', 'перед и после каждого переключения принудительно снимается quota и пишется в'], + ['同一窗口内任意两次快照的', 'любые два снимка в одном окне дают'], + ['任何两次快照的', 'любые два снимка дают'], + ['配合期间代理 tokens', 'вместе с proxy tokens за период'], + ['推出该 Plan 总容量', 'оценку общей ёмкости Plan'], + ['推出 Plan 总容量', 'оценку общей ёмкости Plan'], + ['中位', 'Медиана'], + ['是去掉异常值后最稳的估计', ' - самая устойчивая оценка после удаления выбросов'], + ['是去掉异常值后的稳定估计', ' - устойчивая оценка после удаления выбросов'], + ['Δpct<3% 的样本被丢弃', 'Сэмплы с Δpct < 3% отбрасываются'], + ['样本被丢弃', 'семплы отбрасываются'], + ['used_pct 是整数', 'used_pct целочисленный'], + ['量化误差会失真', 'ошибка квантования искажает результат'], + ['样本会随每次切号自动累积', 'семплы автоматически накапливаются при каждом переключении'], + ['样本数低于', 'если семплов меньше'], + ['时估计仍有偏差,多用几小时即可', 'оценка ещё может быть смещена; дайте системе поработать несколько часов'], + ['每号 Token 历史(精确累加 + 估算上限)', 'История токенов по аккаунтам (точная сумма + оценка лимита)'], + ['邮箱 / Plan', 'Email / Plan'], + ['当前 5h', 'Текущие 5ч'], + ['上次 5h', 'Прошлые 5ч'], + ['当前 周', 'Текущая неделя'], + ['上次 周', 'Прошлая неделя'], + ['每格显示', 'В каждой ячейке показано'], + ['实测累加 / 估算上限', 'фактическая сумма / оценка лимита'], + ['实测累加', 'Фактическая сумма'], + ['在该窗口内的精确求和', 'точная сумма в этом окне'], + ['不平均,不打折,无论中间切号几次', 'без усреднения и скидок, независимо от числа переключений внутри'], + ['估算上限', 'Оценка лимита'], + ['用快照里 used_pct 最大的那个算', 'считается по снимку с максимальным used_pct'], + ['用快照里 used_pct 最大那个算', 'считается по снимку с максимальным used_pct'], + ['量化误差最小', 'минимальная ошибка квантования'], + ['标记的 used_pct 偏小,估算误差大', 'помеченный used_pct маловат, оценка менее точная'], + ['窗口内触发过限额切号', 'в окне было переключение из-за лимита'], + ['此时实测累加', 'в этом случае фактическая сумма'], + ['实际窗口配额', 'фактической квоте окна'], + ['点开账号看历史周期,点开周期看 session 明细', 'раскройте аккаунт для истории циклов, затем цикл для деталей session'], + ['最近', 'недавно'], + ['Token 趋势', 'Динамика токенов'], + ['费用趋势', 'Динамика расходов'], + ['切号日志', 'Журнал переключений'], + ['切换路径', 'Путь переключения'], + ['使用时长', 'Время использования'], + ['暂无切号记录', 'Записей переключения пока нет'], + ['Панель Prompt Cache', 'Панель Prompt Cache'], + ['隐藏旧数据', 'Скрыть старые данные'], + ['早期版本没记 account_id 的历史条目', 'Исторические записи старых версий без account_id'], + ['近 24 小时', 'Последние 24 часа'], + ['近 30 天', 'Последние 30 дней'], + ['节省', 'Сэкономлено'], + ['总花费', 'Итоговый расход'], + ['次请求', 'запросов'], + ['按模型 token 分布', 'Распределение токенов по моделям'], + ['Кэшированный вход', 'Кэшированный вход'], + ['Некэшированный вход', 'Некэшированный вход'], + ['Выход', 'Выход'], + ['本字段是这次新增的,旧条目里没有,不能事后追溯', 'Это поле появилось недавно; в старых записях его нет, восстановить задним числом нельзя'], + ['累计 cached tokens', 'Всего кэшированных токенов'], + ['健康', 'Исправен'], + ['失效或封号', 'Недействителен или заблокирован'], + ['创建于', 'Создано'], + ['命中', 'Срабатываний'], + ['从未', 'Никогда'], + ['刚刚', 'Только что'], + ['分钟前', 'мин назад'], + ['小时前', 'ч назад'], + ['昨天', 'Вчера'], + ['天前', 'дн назад'], + ['中转服务', 'Relay-сервис'], + ['选择预设', 'Выберите пресет'], + ['账号名称 *', 'Имя аккаунта *'], + ['例如:unity2-工作', 'Например: unity2-work'], + ['例如:工作 · GLM Coding', 'Например: Работа · GLM Coding'], + ['例如:工作账号、个人账号', 'Например: рабочий аккаунт, личный аккаунт'], + ['可选的备注信息...', 'Необязательная заметка...'], + ['主页', 'Сайт'], + ['名称', 'Название'], + ['备注(可选)', 'Заметка (необязательно)'], + ['默认自动探测', 'Автоопределение по умолчанию'], + ['自动探测(推荐 · new-api / sub2api 都能识别)', 'Автоопределение (рекомендуется, распознаёт new-api / sub2api)'], + ['responses(默认 / Unity2、ChatGPT、OpenAI key)', 'responses (по умолчанию / Unity2, ChatGPT, OpenAI key)'], + ['chat_completions(GLM/MiMo Coding Plan / 通用 OpenAI Chat)', 'chat_completions (GLM/MiMo Coding Plan / универсальный OpenAI Chat)'], + ['留空 = 透传不替换', 'пусто = передавать без замены'], + ['每行 客户端model=中转站model', 'По одной строке: client_model=relay_model'], + ['添加中…', 'Добавление...'], + ['Base URL 已自动填好', 'Base URL заполнен автоматически'], + ['通用中转', 'Универсальный relay'], + ['三方模型', 'Сторонние модели'], + ['自定义', 'Пользовательский'], + ['厂商自家订阅', 'Подписка провайдера'], + ['厂商按量付费 API', 'API с оплатой по расходу'], + ['中转 / Plan / 三方 出问题时切回订阅号', 'При сбое relay / Plan / стороннего API вернуться на подписной аккаунт'], + ['自动选号可挑中 中转 / Plan / 三方', 'Автовыбор может выбирать relay / Plan / сторонний API'], + ['智谱 GLM', 'Zhipu GLM'], + ['通用 Responses 中转(new-api / CLIProxyAPI / sub2api)', 'Универсальный Responses relay (new-api / CLIProxyAPI / sub2api)'], + ['自定义中转站', 'Пользовательский relay'], + ['阿里 DashScope (通义千问)', 'Alibaba DashScope (Qwen)'], + ['火山方舟 (ByteDance Doubao)', 'Volcano Ark (ByteDance Doubao)'], + ['腾讯混元', 'Tencent Hunyuan'], + ['百度千帆 (ERNIE)', 'Baidu Qianfan (ERNIE)'], + ['优云智算 UCloud Modelverse', 'UCloud Modelverse'], + ['阶跃星辰 Stepfun', 'Stepfun'], + ['神马中转 API (Whatai)', 'Whatai relay API'], + ['魔搭 ModelScope', 'ModelScope'], + ['Ollama 本地推理', 'Локальный Ollama'], + ['按量付费', 'оплата по расходу'], + ['中国订阅', 'китайская подписка'], + ['国际订阅', 'международная подписка'], + ['余额自动探测', 'автоопределение баланса'], + ['OpenAI 兼容', 'OpenAI-совместимый'], + ['OpenAI Chat 兼容', 'совместим с OpenAI Chat'], + ['配额用控制台 Cookie', 'квота через Cookie консоли'], + ['MiMo 配额 Cookie', 'Cookie квоты MiMo'], + ['从 platform.xiaomimimo.com Network 复制 Cookie header', 'Скопируйте Cookie header из Network на platform.xiaomimimo.com'], + ['Prompt Cache 面板', 'Панель кэша промптов'], + ['还没有任何 session 命中过 cache', 'Ни одна сессия пока не использовала кэш'], + ['活跃 session 绑定', 'Активные привязки сессий'], + ['活跃 Session 绑定(evidence-based stickiness)', 'Активные привязки сессий (по подтверждённым попаданиям)'], + ['命中率(按小时)', 'Доля попаданий по часам'], + ['按模型 token 分布(cached vs uncached vs output)', 'Распределение токенов по моделям (кэшированный вход / некэшированный вход / выход)'], + ['vs 全价 input', 'к полной цене input'], + ['请求数', 'Запросов'], + ['命中率', 'Доля попаданий'], + ['全部项目', 'Все элементы'], + ['近 90 天', 'Последние 90 дней'], + ['Token 总量', 'Всего token'], + ['总费用', 'Итоговая стоимость'], + ['切号次数', 'Переключений'], + ['使用账号数', 'Использовано аккаунтов'], + ['Plan 配额上限估算(Δpct 反推,不依赖账号打满)', 'Оценка лимита Plan по Δpct без полного расхода аккаунта'], + ['5h 样本', 'Сэмплы 5ч'], + ['5h 中位', 'Медиана 5ч'], + ['5h 平均', 'Среднее 5ч'], + ['5h min–max', 'Мин-макс 5ч'], + ['5h min-max', 'Мин-макс 5ч'], + ['5H MIN-MAX', 'Мин-макс 5ч'], + ['5h MIN-MAX', 'Мин-макс 5ч'], + ['MIN-MAX 周', 'Мин-макс недели'], + ['周 样本', 'Сэмплы недели'], + ['周 中位', 'Медиана недели'], + ['周 平均', 'Среднее недели'], + ['周 min–max', 'Мин-макс недели'], + ['样本会随每次切号自动累积;样本数低于 ~5 时估计仍有偏差,多用几小时即可。', 'Сэмплы автоматически копятся при переключениях; при числе меньше ~5 оценка может быть неточной, дайте системе поработать несколько часов.'], + ['5h 周期', 'Периоды 5ч'], + ['周周期', 'Недельные периоды'], + ['每格显示「实测累加 / 估算上限」。', 'В каждой ячейке показано «фактическая сумма / оценка лимита».'], + ['窗口', 'Окно'], + ['轮数', 'Раундов'], + ['session 数', 'Session'], + ['状态', 'Статус'], + ['进行中', 'Идёт'], + ['正常', 'Норма'], + ['暂无数据', 'Данных нет'], + ['后台任务日志', 'Журнал фоновых задач'], + ['时间', 'Время'], + ['原因', 'Причина'], + ['目标账号', 'Целевой аккаунт'], + ['任务类型', 'Тип задачи'], + ['刷新后额度', 'Квота после обновления'], + ['成功', 'Успешно'], + ['手动', 'Вручную'], + ['自动刷新', 'Автообновление'], + ['后台保活', 'Фоновое продление'], + ['限额', 'Лимит'], + ['配额充足', 'Квоты достаточно'], + ['配额偏低', 'Квота низкая'], + ['加载统计数据失败:', 'Не удалось загрузить статистику:'], + ['Remote Mode(局域网同步)', 'Удалённый режим (синхронизация в LAN)'], + ['工作模式', 'Режим работы'], + ['仅本机', 'Только этот компьютер'], + ['client(本机)', 'client (этот компьютер)'], + ['server(Server 侧)', 'server (на стороне Server)'], + ['solo(本机接管保活)', 'solo (локальное продление)'], + ['Server API 地址(主)', 'Адрес Server API (основной)'], + ['Server API 地址(回退)', 'Адрес Server API (резервный)'], + ['优先尝试,建议填局域网 IP,如 http:', 'Пробуется первым; лучше указать LAN IP, например http:'], + ['主不通时自动切换,建议填 ZeroTier IP,如 http:', 'Если основной недоступен, используется этот; лучше ZeroTier IP, например http:'], + ['共享密钥', 'Общий секрет'], + ['必须与 Server 端一致', 'Должен совпадать с Server'], + ['已生成新密钥,记得保存设置', 'Новый секрет создан, не забудьте сохранить настройки'], + ['监听地址', 'Адрес прослушивания'], + ['监听端口', 'Порт прослушивания'], + ['Server 侧 HTTP API 端口(默认 18081)', 'Порт HTTP API на стороне Server (по умолчанию 18081)'], + ['0.0.0.0 监听所有网卡;建议仅暴露给 ZeroTier 网段', '0.0.0.0 слушает все интерфейсы; лучше открывать только для ZeroTier-сети'], + ['客户端访问需携带 X-Auth-Token;留空则拒绝所有请求', 'Клиент должен передавать X-Auth-Token; пустое значение запрещает все запросы'], + ['测试连通性 / 推送本机账号 / 从 Server 合并', 'Проверить связь / отправить локальные аккаунты / слить с Server'], + ['推送全部', 'Отправить всё'], + ['拉取合并', 'Получить и слить'], + ['同步所有 token', 'Синхронизировать все token'], + ['自动同号', 'Автосовпадение аккаунта'], + ['本机管 current 指针', 'Этот компьютер управляет указателем current'], + ['当前账号只做权威回流;非活跃账号按独占策略保活刷新(保存后生效)', 'Текущий аккаунт только возвращается как authoritative; неактивные аккаунты продлеваются по эксклюзивной стратегии (после сохранения)'], + ['手工拉取 Server 当前账号并热切过去(自动同号关了也可用)', 'Вручную получить текущий аккаунт Server и горячо переключиться на него (работает даже при выключенном автосовпадении)'], + ['立即同号', 'Синхронизировать current'], + ['后台保活与同步', 'Фоновое продление и синхронизация'], + ['非活跃保活阈值(天)', 'Порог неактивного продления (дни)'], + ['当账号 last_refresh 超过该阈值时,调度器才会尝试保活刷新', 'Планировщик будет пытаться продлить аккаунт, когда last_refresh старше этого порога'], + ['调度间隔(分钟)', 'Интервал планировщика (мин)'], + ['自动重载 IDE', 'Автоперезагрузка IDE'], + ['切换账号后自动重载 IDE 以应用新的 Token', 'Автоматически перезагружать IDE после переключения аккаунта, чтобы применить новый токен'], + ['仅重载选中的 IDE', 'Перезагружать только выбранные IDE'], + ['使用杀进程重启', 'Перезапуск через завершение процессов'], + ['使用 pkill 方式重启(Windsurf 推荐,无需权限)', 'Перезапуск через pkill (рекомендуется для Windsurf, без прав администратора)'], + ['修复 Codex App 闪退', 'Исправить сбой запуска Codex App'], + ['移除 macOS 安全隔离属性 (需要管理员权限)', 'Удалить macOS quarantine-атрибут (нужны права администратора)'], + ['这将尝试移除 Codex App 的安全隔离属性。', 'Будет выполнена попытка удалить quarantine-атрибут Codex App.'], + ['系统可能会弹窗要求输入密码以获得权限。是否继续?', 'Система может запросить пароль для получения прав. Продолжить?'], + ['❌ 修复失败:', '❌ Исправление не удалось:'], + ['安装', 'Установить'], + ['仓库已存在', 'Репозиторий уже существует'], + ['读取失败:', 'Не удалось прочитать:'], + ['5H 限额', 'Лимит 5ч'], + ['24H 限额', 'Лимит 24H'], + ['周限额', 'Недельный лимит'], + ['周配额', 'Недельная квота'], + ['5h 配额', 'Квота 5ч'], + ['加载用量...', 'Загрузка usage...'], + ['即将重置', 'Скоро сброс'], + ['后重置', 'до сброса'], + ['小时', 'ч'], + ['分钟', 'мин'], + ['秒', 'с'], + ['未知', 'Неизвестно'], + ['中转账号请用「中转余额刷新」', 'Для relay-аккаунта используйте «обновление баланса relay»'], + ['未设置当前账号', 'Текущий аккаунт не выбран'], + ['网络请求失败,检查代理/网络', 'Сетевой запрос не удался, проверьте прокси/сеть'], + ['刷新失败:网络或服务暂时异常,未判定账号失效', 'Обновление не удалось: временный сбой сети или сервиса, аккаунт не помечен недействительным'], + ['请求超时(OpenAI 端慢/被节流)', 'Тайм-аут запроса (OpenAI медлит или ограничивает)'], + ['refresh_token 刷新失败', 'Не удалось обновить refresh_token'], + ['网络请求失败', 'Сетевой запрос не удался'], + ['已设为手机锚', 'Мобильная привязка включена'], + ['已解除手机锚绑定', 'Мобильная привязка снята'], + ['选择手机锚账号', 'Выберите аккаунт мобильной привязки'], + ['手机锚:磁盘 ~/.codex/auth.json 永远跟随此号,Codex.app 手机远程连接绑定此号;切到其他号时 disk 不动、proxy 出口照切', 'Мобильная привязка: ~/.codex/auth.json на диске всегда следует этому аккаунту; телефонное подключение Codex.app привязано к нему. При переходе на другие аккаунты диск не меняется, меняется только выход прокси'], + ['仅 ChatGPT 订阅号可绑定(Codex.app 远程连接需要 chatgpt_account_id 鉴权)', 'Привязать можно только подписной аккаунт ChatGPT, потому что удалённое подключение Codex.app требует chatgpt_account_id'], + ['周期保鲜', 'Продление периода'], + ['保鲜已关', 'Продление выключено'], + ['最近尝试', 'Последняя попытка'], + ['最近成功', 'Последний успех'], + ['最近结果', 'Последний результат'], + ['按 primary_window 实际时长自动识别', 'Автоопределение по фактической длительности primary_window'], + ['接口返回 · 5 小时窗口', 'API вернул окно 5 часов'], + ['接口返回 · 7 天窗口', 'API вернул окно 7 дней'], + ['正在拉取重置次数明细…', 'Загрузка деталей активных сбросов...'], + ['拉取明细失败', 'Не удалось загрузить детали'], + ['Input', 'Вход'], + ['Cached', 'Кэшировано'], + ['Output', 'Выход'], + ['Session Key', 'Ключ сессии'], + ['Cached input', 'Кэшированный вход'], + ['Uncached input', 'Некэшированный вход'], + ['cached / input', 'кэшировано / вход'], + ['owner', 'владелец'], + ['repo', 'репозиторий'], + ['branch', 'ветка'], + ['evidence-based', 'по подтверждённым попаданиям'], + ['evidence-based stickiness', 'привязка по подтверждённым попаданиям'], + ['Email', 'Email'], + ['Free', 'Free'], + ['Plus', 'Plus'], + ['Team', 'Team'], + ['后台服务', 'Фоновые службы'], + ['HTTP 也走本机直连上游', 'HTTP тоже идёт напрямую к upstream с этого компьютера'], + ['IDE 重载', 'Перезагрузка IDE'], + ['主力 IDE', 'Основная IDE'], + ['绑定地址', 'Адрес привязки'], + ['修改端口/绑定/密钥后点此应用(保存设置后)', 'Нажмите после изменения порта, привязки или секрета (после сохранения настроек)'], + ['同步操作', 'Операции синхронизации'], + ['Codex.app 手机锚绑定', 'Телефонная привязка Codex.app'], + ['故障修复', 'Устранение неполадок'], + ['代理服务', 'Прокси-сервис'], + ['代理端口', 'Порт прокси'], + ['开启后监听 `0.0.0.0`,同一局域网内的 Windows 可直接连接这台机器的代理', 'После включения слушает `0.0.0.0`; Windows в той же LAN сможет подключаться к прокси на этом компьютере'], + ['切号模式', 'Режим переключения аккаунтов'], + ['auto(代理开=热切)', 'auto (прокси включен = горячее переключение)'], + ['cold(强制冷切)', 'cold (принудительное холодное переключение)'], + ['Windows 机器可把 `OPENAI_BASE_URL` 指向下面这个地址', 'На Windows укажите `OPENAI_BASE_URL` на этот адрес'], + ['5H', '5ч'], + ['5h', '5ч'], + ['7D', '7д'], + ['7d', '7д'], + ['Spark 5H', 'Spark 5ч'], + ['登录账号 (OpenAI / OTP / 导入)', 'Войти в аккаунт (OpenAI / OTP / импорт)'], + ['用该账号开一个真 codex 终端(隔离 + 直连),可发一句"你好"触发 referral 兑现', 'Открыть настоящий терминал codex для этого аккаунта (изоляция + прямое соединение); можно отправить короткое сообщение, чтобы активировать referral.'], + ['推送到 Server', 'Отправить на Server'], + ['确认删除账号', 'Удалить аккаунт?'], + ['彻底删除', 'Удалить окончательно'], + ['不打开默认浏览器,把授权链接复制到剪贴板,由你粘贴到目标浏览器', 'Не открывать браузер по умолчанию; скопировать ссылку авторизации в буфер, чтобы вставить её в нужный браузер'], + ['刷新额度', 'Обновить квоту'], + ['授权成功', 'Авторизация успешна'], + ['OAuth 授权成功', 'OAuth-авторизация успешна'], + ['无法获取用户目录', 'Не удалось получить домашний каталог пользователя'], + ['设置文件权限失败', 'Не удалось задать права файла'], + ['设置目录权限失败', 'Не удалось задать права каталога'], + ['写入文件失败', 'Не удалось записать файл'], + ['创建目录失败', 'Не удалось создать каталог'], + ['创建临时文件失败', 'Не удалось создать временный файл'], + ['写入临时文件失败', 'Не удалось записать временный файл'], + ['重命名临时文件失败', 'Не удалось переименовать временный файл'], + ['重命名文件失败 (Atomic Write)', 'Не удалось переименовать файл (atomic write)'], + ['读取 auth.json 失败', 'Не удалось прочитать auth.json'], + ['解析 auth.json 失败', 'Не удалось разобрать auth.json'], + ['未找到 Codex auth.json,请先登录 Codex', 'Codex auth.json не найден, сначала войдите в Codex'], + ['账号不存在', 'Аккаунт не найден'], + ['当前账号不存在', 'Текущий аккаунт не найден'], + ['没有激活的账号', 'Нет активного аккаунта'], + ['当前账号缺少 access_token', 'У текущего аккаунта нет access_token'], + ['session 绑定账号不存在', 'Привязанный к session аккаунт не найден'], + ['session 绑定账号缺 access_token', 'У привязанного к session аккаунта нет access_token'], + ['账号到期日格式必须是 YYYY-MM-DD', 'Дата окончания аккаунта должна быть в формате YYYY-MM-DD'], + ['不是中转站账号', 'Это не relay-аккаунт'], + ['无效的 Token 格式', 'Недопустимый формат токена'], + ['Base64 解码失败', 'Не удалось декодировать Base64'], + ['UTF-8 转换失败', 'Не удалось преобразовать UTF-8'], + ['JSON 解析失败', 'Не удалось разобрать JSON'], + ['文件不是 UTF-8 文本', 'Файл не является UTF-8 текстом'], + ['无法识别的 JSON 结构', 'Не удалось распознать структуру JSON'], + ['无法识别的文件格式', 'Не удалось распознать формат файла'], + ['zip 解析失败', 'Не удалось разобрать zip'], + ['缺 email', 'Нет email'], + ['缺 access_token', 'Нет access_token'], + ['缺 refresh_token', 'Нет refresh_token'], + ['无 accounts 数组', 'Нет массива accounts'], + ['缺 credentials', 'Нет credentials'], + ['native: 缺 accounts 对象', 'native: нет объекта accounts'], + ['session JSON 不能为空', 'session JSON не должен быть пустым'], + ['未找到包含 accessToken 的 session 对象', 'Не найден session-объект с accessToken'], + ['缺少 accessToken', 'Нет accessToken'], + ['执行失败', 'Не удалось выполнить'], + ['未找到运行中的 codex 进程', 'Запущенные процессы codex не найдены'], + ['未配置 Server 地址', 'Адрес Server не настроен'], + ['未配置共享密钥', 'Общий секрет не настроен'], + ['共享密钥为空,请先生成', 'Общий секрет пуст, сначала сгенерируйте его'], + ['磁盘账号不在管理列表中,请先导入', 'Аккаунта с диска нет в списке управления, сначала импортируйте его'], + ['手机锚只能设在 ChatGPT 订阅号上', 'Телефонный якорь можно включать только для аккаунта с подпиской ChatGPT'], + ['拒绝同步:身份不匹配', 'Синхронизация отклонена: идентичность не совпадает'], + ['拒绝同步:账号名与 token 邮箱不一致', 'Синхронизация отклонена: имя аккаунта не совпадает с email в токене'], + ['刷新令牌被拒绝', 'Refresh token отклонён'], + ['刷新令牌失败', 'Не удалось обновить refresh token'], + ['发现失败', 'Поиск не удался'], + ['安装失败', 'Установка не удалась'], + ['卸载失败', 'Удаление не удалось'], + ['操作失败', 'Операция не удалась'], + ['同步失败', 'Синхронизация не удалась'], + ['✓ 含 refresh_token', '✓ есть refresh_token'], + ['⚠ 无 refresh_token,到期后需重新导入', '⚠ нет refresh_token, после истечения нужно импортировать заново'], + ['每行一个,自动识别 provider', 'По одному на строку, provider определяется автоматически'], + ['运行中', 'Выполняется'], + ['等待中', 'Ожидает'], + ['Base URL 必须以 http:// 或 https:// 开头', 'Base URL должен начинаться с http:// или https://'], + ['API Key 看起来太短', 'API Key выглядит слишком коротким'], + ['账号名不能为空', 'Имя аккаунта не должно быть пустым'], + ['已封号', 'Заблокирован'], + ['Token 失效', 'Токен недействителен'], + ['日期异常', 'Некорректная дата'], + ['已过期', 'Истёк'], + ['天到期', 'дн до окончания'], + ['启动中…', 'Запуск...'], + ['启动 codex', 'Запустить codex'], + ['周期保鲜最近错误', 'Последняя ошибка продления периода'], + ['周期保鲜会在额度窗口到点后自动发一次最小 Codex 请求', 'После сброса окна квоты продление периода автоматически отправит минимальный запрос Codex'], + ['主动重置', 'Активный сброс'], + ['保活:', 'Продление:'], + ['刷新:', 'Обновление:'], + ['到期:', 'Окончание:'], + ['已选', 'Выбрано'], + ['按账号', 'По аккаунтам'], + ['绑定账号', 'Привязанный аккаунт'], + ['命中次数', 'Попаданий'], + ['年龄', 'Возраст'], + ['花费', 'Расход'], + ['搜索 label / session_id / 账号…', 'Поиск label / session_id / аккаунта...'], + ['搜索 cwd / 首条消息…', 'Поиск cwd / первого сообщения...'], + ['搜索账号…', 'Поиск аккаунта...'], + ['搜索邮箱 / 订阅类型 (如 team / pro / free)…', 'Поиск email / типа подписки (например team / pro / free)...'], + ['模型分布', 'Распределение моделей'], + ['切号原因', 'Причины переключений'], + ['费用', 'Стоимость'], + ['上次', 'Прошлое'], + ['当前', 'Текущий'], + ['周期', 'Период'], + ['订阅', 'Подписка'], + ['代理', 'Прокси'], + ['账号', 'Аккаунт'], + ['额度', 'Квота'], + ['配额', 'Квота'], + ['中转', 'Relay'], + ['保活', 'Продление'], + ['绑定', 'Привязка'], + ['解除', 'Снять'], + ['重试', 'Повтор'], + ['错误', 'Ошибка'], + ['开启', 'Включить'], + ['启用', 'Включено'], + ['禁用', 'Отключено'], + ['已启用', 'Включено'], + ['已禁用', 'Отключено'], + ['已停止', 'Остановлено'], + ['已启动', 'Запущено'], + ['未设置', 'Не задано'], + ['未绑定', 'Не привязано'], + ['自动', 'Авто'], + ['本地', 'Локально'], + ['远程', 'Удалённо'], + ['全量', 'Полный'], + ['备注', 'Заметка'], + ['说明', 'Описание'], + ['来源', 'Источник'], + ['到期', 'Истекает'], + ['邮箱', 'Email'], + ['日期', 'Дата'], + ['当前号', 'текущий аккаунт'], + ['封', 'Блок'], + ['条', 'зап.'], + ['个', 'шт.'], + [',', ', '], + ['。', '. '], + [':', ': '], + ['(', ' ('], + [')', ')'], + ['、', ', '], + [';', '; '], + ['「', '«'], + ['」', '»'], + ['【', '['], + ['】', ']'], +]; + +const sortedReplacements = [...replacements].sort((a, b) => b[0].length - a[0].length); +const exactReplacements = new Map(replacements); +const cjkRe = /[\u4e00-\u9fff]/; +const normalizeWhitespace = (value: string) => value.replace(/\s+/g, ' ').trim(); +const normalizedExactReplacements = new Map( + replacements.map(([source, target]) => [normalizeWhitespace(source), target]), +); +const placeholderRe = /\{[^{}]*\}/g; +const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +const dynamicReplacements = replacements + .filter(([source]) => /\{[^{}]*\}/.test(source)) + .sort((a, b) => b[0].length - a[0].length) + .map(([source, target]) => { + placeholderRe.lastIndex = 0; + const parts = normalizeWhitespace(source).split(placeholderRe).map(escapeRegExp); + placeholderRe.lastIndex = 0; + return { pattern: new RegExp(`^${parts.join('(.*?)')}$`, 'u'), target }; + }); +const partialReplacements = sortedReplacements.filter(([source]) => ( + cjkRe.test(source) || /[():「」【】、,。;]/.test(source) +)); +export function translateRu(value: string): string { + if (!value) { + return value; + } + const leading = value.match(/^\s*/)?.[0] ?? ''; + const trailing = value.match(/\s*$/)?.[0] ?? ''; + const core = value.trim(); + const exact = exactReplacements.get(core); + if (exact) { + return `${leading}${exact}${trailing}`; + } + const normalizedExact = normalizedExactReplacements.get(normalizeWhitespace(core)); + if (normalizedExact) { + return `${leading}${normalizedExact}${trailing}`; + } + const normalizedCore = normalizeWhitespace(core); + for (const replacement of dynamicReplacements) { + const match = replacement.pattern.exec(normalizedCore); + if (!match) continue; + let capture = 1; + const translated = replacement.target.replace(placeholderRe, () => match[capture++] ?? ''); + return `${leading}${translated}${trailing}`; + } + if (!cjkRe.test(value)) { + return value; + } + + let out = value; + for (const [source, target] of partialReplacements) { + if (out.includes(source)) { + out = out.split(source).join(target); + } + } + out = out + .replace(/(\d+)\s*天后重置/g, '$1 дн до сброса') + .replace(/(\d+)\s*小时(\d+)\s*分钟后重置/g, '$1 ч $2 мин до сброса') + .replace(/(\d+)\s*分钟后重置/g, '$1 мин до сброса') + .replace(/(\d+)\s*天\s*(\d+)\s*小时\s*(\d+)\s*分钟\s*后重置/g, '$1 дн $2 ч $3 мин до сброса') + .replace(/(\d+)\s*小时\s*(\d+)\s*分钟\s*(\d+)\s*秒\s*后重置/g, '$1 ч $2 мин $3 с до сброса') + .replace(/(\d+)\s*分钟\s*(\d+)\s*秒\s*后重置/g, '$1 мин $2 с до сброса') + .replace(/(\d+)\s*秒\s*后重置/g, '$1 с до сброса') + .replace(/(\d+)\s*天到期/g, '$1 дн до окончания') + .replace(/(\d+)\s*天前/g, '$1 дн назад') + .replace(/(\d+)\s*小时前/g, '$1 ч назад') + .replace(/(\d+)\s*分钟前/g, '$1 мин назад') + .replace(/(\d+)\s*个使用额度/g, '$1 кредитов использования') + .replace(/(\d+)\s*个工作区额度/g, '$1 кредитов workspace') + .replace(/(\d+)\s*次限额重置/g, '$1 сбросов лимита') + .replace(/(\d+)\s*个账号/g, '$1 аккаунтов') + .replace(/(\d+)\s*人/g, '$1 чел.') + .replace(/(\d+)\s*条/g, '$1 зап.') + .replace(/(\d+)\s*小\s*ч/g, '$1 ч') + .replace(/(\d+)\s*天/g, '$1 дн') + .replace(/(\d+)\s*小时/g, '$1 ч') + .replace(/(\d+)\s*分钟/g, '$1 мин') + .replace(/(\d+)\s*秒/g, '$1 с') + .replace(/(\d+)\s*时/g, '$1 ч') + .replace(/(\d+)\s*分/g, '$1 мин') + .replace(/(\d+)\s*ч/g, '$1 ч') + .replace(/(\d+)\s*мин/g, '$1 мин') + .replace(/(\d+)\s*с\b/g, '$1 с'); + return out; +} + +export const russianLocale: UiLocale = { + code: 'ru', + title: 'Codex Switcher', + translate: translateRu, +}; + +export function countRussianReplacements(): number { + return replacements.length; +} diff --git a/src/i18n/runtime.ts b/src/i18n/runtime.ts new file mode 100644 index 0000000..e786c51 --- /dev/null +++ b/src/i18n/runtime.ts @@ -0,0 +1,110 @@ +export interface UiLocale { + code: string; + title: string; + translate(value: string): string; +} + +const translatedMark = 'data-ui-locale'; + +function translateTextNode(node: Text, locale: UiLocale) { + const parent = node.parentElement; + if (!parent || parent.closest('script, style, textarea, input, code, pre')) return; + const translated = locale.translate(node.data); + if (translated !== node.data) node.data = translated; +} + +function translateElementAttributes(element: Element, locale: UiLocale) { + for (const attr of ['title', 'placeholder', 'aria-label', 'alt']) { + const value = element.getAttribute(attr); + if (!value) continue; + const translated = locale.translate(value); + if (translated !== value) element.setAttribute(attr, translated); + } +} + +function translateTree(root: ParentNode, locale: UiLocale) { + const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); + let node = walker.nextNode(); + while (node) { + translateTextNode(node as Text, locale); + node = walker.nextNode(); + } + if (root instanceof Element) translateElementAttributes(root, locale); + root.querySelectorAll?.('[title], [placeholder], [aria-label], [alt]') + .forEach(element => translateElementAttributes(element, locale)); +} + +function wrapDialogs(locale: UiLocale) { + const originalAlert = window.alert.bind(window); + const originalConfirm = window.confirm.bind(window); + const originalPrompt = window.prompt.bind(window); + window.alert = (message?: unknown) => originalAlert(locale.translate(String(message ?? ''))); + window.confirm = (message?: string) => originalConfirm(locale.translate(String(message ?? ''))); + window.prompt = (message?: string, defaultValue?: string) => ( + originalPrompt(locale.translate(String(message ?? '')), defaultValue) + ); +} + +function wrapNotification(locale: UiLocale) { + if (!('Notification' in window)) return; + const OriginalNotification = window.Notification; + const LocalizedNotification = function Notification(title: string, options?: NotificationOptions) { + const localizedOptions = options + ? { ...options, body: options.body ? locale.translate(options.body) : options.body } + : options; + return new OriginalNotification(locale.translate(title), localizedOptions); + } as unknown as typeof Notification; + Object.setPrototypeOf(LocalizedNotification, OriginalNotification); + LocalizedNotification.prototype = OriginalNotification.prototype; + try { + window.Notification = LocalizedNotification; + } catch { + // Some WebView builds expose Notification as read-only. + } +} + +export function installUiLocale(locale: UiLocale) { + document.documentElement.lang = locale.code; + document.title = locale.title; + + const bootStyle = document.createElement('style'); + bootStyle.id = 'ui-locale-boot'; + bootStyle.textContent = `body:not([${translatedMark}="ready"]) { visibility: hidden; }`; + document.head.appendChild(bootStyle); + + wrapDialogs(locale); + wrapNotification(locale); + + const observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + if (mutation.type === 'characterData' && mutation.target.nodeType === Node.TEXT_NODE) { + translateTextNode(mutation.target as Text, locale); + continue; + } + for (const node of mutation.addedNodes) { + if (node.nodeType === Node.TEXT_NODE) { + translateTextNode(node as Text, locale); + } else if (node.nodeType === Node.ELEMENT_NODE) { + translateTree(node as Element, locale); + } + } + if (mutation.type === 'attributes' && mutation.target instanceof Element) { + translateElementAttributes(mutation.target, locale); + } + } + }); + + observer.observe(document.documentElement, { + childList: true, + subtree: true, + characterData: true, + attributes: true, + attributeFilter: ['title', 'placeholder', 'aria-label', 'alt'], + }); + + queueMicrotask(() => { + translateTree(document.body, locale); + document.body.setAttribute(translatedMark, 'ready'); + bootStyle.remove(); + }); +} diff --git a/src/main.tsx b/src/main.tsx index fc750ba..1b12d5b 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,11 +3,23 @@ import ReactDOM from "react-dom/client"; import App from "./App"; import { TrayPopup } from "./components/TrayPopup"; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; +import { invoke } from "@tauri-apps/api/core"; +import { installAppLocale } from "./i18n"; const label = getCurrentWebviewWindow().label; -ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - - {label === "tray-popup" ? : } - , -); +async function startApp() { + const systemLocale = await invoke("get_system_locale").catch(() => null); + const appLocale = installAppLocale(systemLocale ?? undefined); + await invoke("set_app_locale", { locale: appLocale }).catch((error) => { + console.error("Failed to synchronize the application locale", error); + }); + + ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( + + {label === "tray-popup" ? : } + , + ); +} + +void startApp();