From bc481d463bf90a400ad2bba321a182b0505aa15f Mon Sep 17 00:00:00 2001 From: Slavx Date: Thu, 28 May 2026 09:21:02 +0300 Subject: [PATCH 1/4] feat: add Russian locale Co-authored-by: Qwen-Coder --- frontend/src/i18n/index.ts | 10 +- frontend/src/i18n/locales/ru.ts | 7000 +++++++++++++++++++++++++++++++ 2 files changed, 7006 insertions(+), 4 deletions(-) create mode 100644 frontend/src/i18n/locales/ru.ts diff --git a/frontend/src/i18n/index.ts b/frontend/src/i18n/index.ts index 5dab65e8097..8b6f3d55f44 100644 --- a/frontend/src/i18n/index.ts +++ b/frontend/src/i18n/index.ts @@ -1,6 +1,6 @@ import { createI18n } from 'vue-i18n' -type LocaleCode = 'en' | 'zh' +type LocaleCode = 'en' | 'zh' | 'ru' type LocaleMessages = Record @@ -9,11 +9,12 @@ const DEFAULT_LOCALE: LocaleCode = 'en' const localeLoaders: Record Promise<{ default: LocaleMessages }>> = { en: () => import('./locales/en'), - zh: () => import('./locales/zh') + zh: () => import('./locales/zh'), + ru: () => import('./locales/ru') } function isLocaleCode(value: string): value is LocaleCode { - return value === 'en' || value === 'zh' + return value === 'en' || value === 'zh' || value === 'ru' } function getDefaultLocale(): LocaleCode { @@ -85,7 +86,8 @@ export function getLocale(): LocaleCode { export const availableLocales = [ { code: 'en', name: 'English', flag: '🇺🇸' }, - { code: 'zh', name: '中文', flag: '🇨🇳' } + { code: 'zh', name: '中文', flag: '🇨🇳' }, + { code: 'ru', name: 'Русский', flag: '🇷🇺' } ] as const export default i18n diff --git a/frontend/src/i18n/locales/ru.ts b/frontend/src/i18n/locales/ru.ts new file mode 100644 index 00000000000..29f03e963c2 --- /dev/null +++ b/frontend/src/i18n/locales/ru.ts @@ -0,0 +1,7000 @@ +export default { + // Home Page + home: { + viewOnGithub: 'Смотреть на GitHub', + viewDocs: 'Документация', + docs: 'Документация', + switchToLight: 'Светлая тема', + switchToDark: 'Тёмная тема', + dashboard: 'Панель', + login: 'Войти', + getStarted: 'Начать', + goToDashboard: 'Перейти в панель', + // User-focused value proposition + heroSubtitle: 'Один ключ для всех AI-моделей', + heroDescription: 'Не нужно управлять несколькими подписками. Доступ к Claude, GPT, Gemini и другим моделям через один API-ключ', + tags: { + subscriptionToApi: 'Подписка в API', + stickySession: 'Сохранение сессии', + realtimeBilling: 'Оплата по факту' + }, + // Pain points section + painPoints: { + title: 'Знакомая ситуация?', + items: { + expensive: { + title: 'Высокая стоимость подписок', + desc: 'Несколько AI-подписок каждый месяц быстро увеличивают расходы' + }, + complex: { + title: 'Хаос с аккаунтами', + desc: 'Аккаунты и API-ключи разбросаны по разным платформам' + }, + unstable: { + title: 'Перебои сервиса', + desc: 'Один аккаунт упирается в лимиты и мешает работе' + }, + noControl: { + title: 'Нет контроля расхода', + desc: "Сложно понять расходы и ограничить потребление команды" + } + } + }, + // Solutions section + solutions: { + title: 'Мы решаем эти проблемы', + subtitle: 'Три простых шага к удобному AI-доступу' + }, + features: { + unifiedGateway: 'Доступ в один клик', + unifiedGatewayDesc: 'Один API-ключ для всех подключённых AI-моделей. Без отдельных заявок.', + multiAccount: 'Стабильная работа', + multiAccountDesc: 'Умная маршрутизация по нескольким аккаунтам с автоматическим переключением при сбоях.', + balanceQuota: 'Платите за использование', + balanceQuotaDesc: 'Оплата по фактическому расходу, лимиты квот и прозрачная статистика команды.' + }, + // Comparison section + comparison: { + title: 'Почему мы?', + headers: { + feature: 'Сравнение', + official: 'Официальные подписки', + us: 'Наша платформа' + }, + items: { + pricing: { + feature: 'Оплата', + official: 'Фиксированная плата, даже если не используете', + us: 'Оплата только за расход' + }, + models: { + feature: 'Выбор моделей', + official: 'Только один провайдер', + us: 'Свободное переключение моделей' + }, + management: { + feature: 'Управление аккаунтами', + official: 'Каждый сервис отдельно', + us: 'Один ключ и единая панель' + }, + stability: { + feature: 'Стабильность', + official: 'Лимиты одного аккаунта', + us: 'Пул аккаунтов и автопереключение' + }, + control: { + feature: 'Контроль расхода', + official: 'Недоступно', + us: 'Квоты и подробная аналитика' + } + } + }, + providers: { + title: 'Поддерживаемые AI-модели', + description: 'Один API, много вариантов', + supported: 'Поддерживается', + soon: 'Скоро', + claude: 'Claude', + gemini: 'Gemini', + antigravity: 'Antigravity', + more: 'Ещё' + }, + // CTA section + cta: { + title: 'Готовы начать?', + description: 'Зарегистрируйтесь и получите тестовый баланс для удобного AI-доступа', + button: 'Зарегистрироваться бесплатно' + }, + footer: { + allRightsReserved: 'Все права защищены.' + } + }, + + // Key Usage Query Page + keyUsage: { + title: 'Расход API-ключа', + subtitle: 'Введите API-ключ, чтобы увидеть текущий расход и статус', + placeholder: 'sk-ant-mirror-xxxxxxxxxxxx', + query: 'Запросить', + querying: 'Запрос...', + privacyNote: 'Ключ обрабатывается локально в браузере и не сохраняется', + dateRange: 'Период:', + dateRangeToday: 'Сегодня', + dateRange7d: '7 дней', + dateRange30d: '30 дней', + dateRange90d: '90 дней', + dateRangeCustom: 'Свой', + apply: 'Применить', + used: 'Использовано', + detailInfo: 'Детали', + tokenStats: 'Статистика токенов', + dailyDetail: 'По дням', + modelStats: 'Статистика по моделям', + // Table headers + date: 'Дата', + model: 'Модель', + requests: 'Запросы', + inputTokens: 'Входные токены', + outputTokens: 'Выходные токены', + cacheCreationTokens: 'Cache Creation', + cacheReadTokens: 'Cache Read', + cacheWriteTokens: 'Cache Write', + totalTokens: 'Всего токенов', + cost: 'Стоимость', + // Status + quotaMode: 'Режим квоты ключа', + walletBalance: 'Баланс кошелька', + // Ring card titles + totalQuota: 'Общая квота', + limit5h: '5-Hour Limit', + limitDaily: 'Дневной лимит', + limit7d: '7-Day Limit', + limitWeekly: 'Недельный лимит', + limitMonthly: 'Месячный лимит', + // Detail rows + remainingQuota: 'Остаток квоты', + expiresAt: 'Истекает', + todayExpires: '(истекает сегодня)', + daysLeft: '({days} дн.)', + usedQuota: 'Использовано квоты', + resetNow: 'Скоро сброс', + subscriptionType: 'Тип подписки', + subscriptionExpires: 'Подписка истекает', + // Usage stat cells + todayRequests: 'Запросов сегодня', + todayInputTokens: 'Today Input', + todayOutputTokens: 'Today Output', + todayTokens: 'Токенов сегодня', + todayCacheCreation: 'Today Cache Creation', + todayCacheRead: 'Today Cache Read', + todayCost: 'Расход сегодня', + rpmTpm: 'RPM / TPM', + totalRequests: 'Всего запросов', + totalInputTokens: 'Total Input', + totalOutputTokens: 'Total Output', + totalTokensLabel: 'Всего токенов', + totalCacheCreation: 'Total Cache Creation', + totalCacheRead: 'Total Cache Read', + totalCost: 'Общая стоимость', + avgDuration: 'Средняя длительность', + // Messages + enterApiKey: 'Please enter an API Key', + querySuccess: 'Query successful', + queryFailed: 'Query failed', + queryFailedRetry: 'Query failed, please try again later', + noDailyUsage: 'Нет данных за дни', + }, + + // Setup Wizard + setup: { + title: 'Sub2API Setup', + description: 'Configure your Sub2API instance', + database: { + title: 'Database Configuration', + description: 'Connect to your PostgreSQL database', + host: 'Host', + port: 'Port', + username: 'Имя пользователя', + password: 'Пароль', + databaseName: 'Database Name', + sslMode: 'SSL Mode', + passwordPlaceholder: 'Пароль', + ssl: { + disable: 'Отключить', + require: 'Require', + verifyCa: 'Verify CA', + verifyFull: 'Verify Full' + } + }, + redis: { + title: 'Redis Configuration', + description: 'Connect to your Redis server', + host: 'Host', + port: 'Port', + password: 'Password (optional)', + database: 'Database', + passwordPlaceholder: 'Пароль', + enableTls: 'Enable TLS', + enableTlsHint: 'Use TLS when connecting to Redis (public CA certs)' + }, + admin: { + title: 'Admin Account', + description: 'Create your administrator account', + email: 'Email', + password: 'Пароль', + confirmPassword: 'Confirm Password', + passwordPlaceholder: 'Min 8 characters', + confirmPasswordPlaceholder: 'Confirm password', + passwordMismatch: 'Passwords do not match' + }, + ready: { + title: 'Ready to Install', + description: 'Review your configuration and complete setup', + database: 'Database', + redis: 'Redis', + adminEmail: 'Admin Email' + }, + status: { + testing: 'Testing...', + success: 'Connection Successful', + testConnection: 'Test Connection', + installing: 'Installing...', + completeInstallation: 'Complete Installation', + completed: 'Installation completed!', + redirecting: 'Redirecting to login page...', + restarting: 'Service is restarting, please wait...', + timeout: 'Service restart is taking longer than expected. Please refresh the page manually.' + } + }, + + // Common + common: { + loading: 'Загрузка...', + submitting: 'Отправка...', + justNow: 'только что', + save: 'Сохранить', + saved: 'Сохранено', + deleted: 'Удалено', + cancel: 'Отмена', + delete: 'Удалить', + edit: 'Изменить', + create: 'Создать', + update: 'Обновить', + confirm: 'Подтвердить', + reset: 'Сбросить', + search: 'Поиск', + filter: 'Фильтр', + export: 'Экспорт', + import: 'Импорт', + actions: 'Действия', + status: 'Статус', + name: 'Имя', + email: 'Email', + password: 'Пароль', + submit: 'Отправить', + back: 'Назад', + next: 'Далее', + yes: 'Да', + no: 'Нет', + all: 'Все', + none: 'Нет', + selectAll: 'Выбрать все', + noData: 'Нет данных', + expand: 'Развернуть', + collapse: 'Свернуть', + success: 'Успешно', + error: 'Ошибка', + critical: 'Критично', + warning: 'Предупреждение', + info: 'Информация', + active: 'Активен', + inactive: 'Неактивен', + more: 'Ещё', + close: 'Закрыть', + enabled: 'Включено', + disabled: 'Отключено', + total: 'Итого', + balance: 'Баланс', + available: 'Доступно', + copiedToClipboard: 'Скопировано в буфер', + copied: 'Скопировано', + copyFailed: 'Не удалось скопировать', + verifying: 'Проверка...', + processing: 'Обработка...', + contactSupport: 'Связаться с поддержкой', + add: 'Добавить', + invalidEmail: 'Введите корректный email', + optional: 'необязательно', + selectOption: 'Выберите вариант', + searchPlaceholder: 'Поиск...', + noOptionsFound: 'Варианты не найдены', + noGroupsAvailable: 'Нет доступных групп', + unknownError: 'Произошла неизвестная ошибка', + saving: 'Сохранение...', + selectedCount: '(выбрано: {count})', + refresh: 'Обновить', + autoRefresh: { + title: 'Автообновление', + enable: 'Включить автообновление', + countdown: 'Автообновление: {seconds}с', + seconds: '{n} с', + }, + view: 'Просмотр', + settings: 'Настройки', + chooseFile: 'Выбрать файл', + copy: 'Копировать', + notAvailable: 'Н/Д', + now: 'Сейчас', + today: 'Сегодня', + tomorrow: 'Завтра', + unknown: 'Неизвестно', + minutes: 'min', + time: { + never: 'Никогда', + justNow: 'Только что', + minutesAgo: '{n} мин назад', + hoursAgo: '{n} ч назад', + daysAgo: '{n} дн назад', + countdown: { + daysHours: '{d}д {h}ч', + hoursMinutes: '{h}ч {m}м', + minutes: '{m}м', + withSuffix: 'до снятия: {time}' + } + } + }, + + // Navigation + nav: { + dashboard: 'Панель', + announcements: 'Объявления', + apiKeys: 'API-ключи', + usage: 'Расход', + redeem: 'Активировать', + affiliate: 'Партнёрские бонусы', + affiliateManagement: 'Партнёрские бонусы', + affiliateInviteRecords: 'Приглашения', + affiliateRebateRecords: 'Бонусы', + affiliateTransferRecords: 'Переводы', + profile: 'Профиль', + users: 'Пользователи', + groups: 'Группы', + channels: 'Каналы', + availableChannels: 'Доступные каналы', + subscriptions: 'Подписки', + accounts: 'Аккаунты', + proxies: 'Прокси', + redeemCodes: 'Коды активации', + ops: 'Ops', + promoCodes: 'Промокоды', + settings: 'Настройки', + myAccount: 'Мой аккаунт', + lightMode: 'Светлая тема', + darkMode: 'Тёмная тема', + collapse: 'Свернуть', + expand: 'Развернуть', + logout: 'Выйти', + github: 'GitHub', + mySubscriptions: 'Мои подписки', + buySubscription: 'Пополнение / подписка', + docs: 'Документация', + myOrders: 'Мои заказы', + orderManagement: 'Заказы', + paymentDashboard: 'Платежная панель', + paymentConfig: 'Настройки платежей', + paymentPlans: 'Планы', + channelManagement: 'Каналы', + channelPricing: 'Цены каналов', + channelMonitor: 'Монитор каналов', + channelStatus: 'Статус каналов', + riskControl: 'Риск-контроль', + }, + + // Auth + auth: { + welcomeBack: 'С возвращением', + signInToAccount: 'Войдите в аккаунт, чтобы продолжить', + signIn: 'Войти', + signingIn: 'Вход...', + createAccount: 'Создать аккаунт', + signUpToStart: 'Зарегистрируйтесь, чтобы начать пользоваться {siteName}', + signUp: 'Зарегистрироваться', + processing: 'Обработка...', + continue: 'Продолжить', + rememberMe: 'Запомнить меня', + dontHaveAccount: "Нет аккаунта?", + alreadyHaveAccount: 'Уже есть аккаунт?', + registrationDisabled: 'Регистрация сейчас отключена. Обратитесь к администратору.', + emailLabel: 'Email', + emailPlaceholder: 'Введите email', + passwordLabel: 'Пароль', + passwordPlaceholder: 'Введите пароль', + createPasswordPlaceholder: 'Создайте надёжный пароль', + passwordHint: 'Минимум 6 символов', + emailRequired: 'Email обязателен', + invalidEmail: 'Введите корректный email', + passwordRequired: 'Пароль обязателен', + passwordMinLength: 'Пароль должен быть не короче 6 символов', + loginFailed: 'Не удалось войти. Проверьте данные и попробуйте снова.', + errors: { + USER_NOT_ACTIVE: 'Аккаунт отключён.', + }, + registrationFailed: 'Регистрация не удалась. Попробуйте снова.', + emailSuffixNotAllowed: 'This email domain is not allowed for registration.', + emailSuffixNotAllowedWithAllowed: + 'This email domain is not allowed. Allowed domains: {suffixes}', + emailSuffixAllowedMore: 'and {count} more', + loginSuccess: 'Login successful! Welcome back.', + accountCreatedSuccess: 'Account created successfully! Welcome to {siteName}.', + reloginRequired: 'Сессия истекла. Войдите снова.', + turnstileExpired: 'Verification expired, please try again', + turnstileFailed: 'Verification failed, please try again', + completeVerification: 'Please complete the verification', + verifyYourEmail: 'Verify Your Email', + sessionExpired: 'Session expired', + sessionExpiredDesc: 'Please go back to the registration page and start again.', + verificationCode: 'Код подтверждения', + verificationCodeHint: 'Enter the 6-digit code sent to your email', + sendingCode: 'Sending...', + sendCode: 'Отправить код', + clickToResend: 'Click to resend code', + resendCode: 'Resend verification code', + sendCodeDesc: "We'll send a verification code to", + codeSentSuccess: 'Код отправлен. Проверьте почту.', + verifying: 'Проверка...', + verifyAndCreate: 'Подтвердить и создать аккаунт', + resendCountdown: 'Resend code in {countdown}s', + backToRegistration: 'Назад к регистрации', + sendCodeFailed: 'Failed to send verification code. Please try again.', + verifyFailed: 'Verification failed. Please try again.', + codeRequired: 'Verification code is required', + invalidCode: 'Please enter a valid 6-digit code', + promoCodeLabel: 'Промокод', + promoCodePlaceholder: 'Enter promo code (optional)', + promoCodeValid: 'Valid! You will receive ${amount} bonus balance', + promoCodeInvalid: 'Invalid promo code', + promoCodeNotFound: 'Promo code not found', + promoCodeExpired: 'This promo code has expired', + promoCodeDisabled: 'This promo code is disabled', + promoCodeMaxUsed: 'This promo code has reached its usage limit', + promoCodeAlreadyUsed: 'You have already used this promo code', + promoCodeValidating: 'Promo code is being validated, please wait', + promoCodeInvalidCannotRegister: 'Invalid promo code. Please check and try again or clear the promo code field', + invitationCodeLabel: 'Код приглашения', + invitationCodePlaceholder: 'Enter invitation code', + invitationCodeRequired: 'Invitation code is required', + invitationCodeValid: 'Invitation code is valid', + invitationCodeInvalid: 'Invalid or used invitation code', + invitationCodeValidating: 'Validating invitation code...', + invitationCodeInvalidCannotRegister: 'Invalid invitation code. Please check and try again', + oauthOrContinue: 'or continue with others', + linuxdo: { + signIn: 'Continue with Linux.do', + orContinue: 'or continue with email', + callbackTitle: 'Signing you in', + callbackProcessing: 'Completing login, please wait...', + callbackHint: 'If you are not redirected automatically, go back to the login page and try again.', + callbackMissingToken: 'Missing login token, please try again.', + backToLogin: 'Back to Login', + invitationRequired: 'This Linux.do account is not yet registered. The site requires an invitation code — please enter one to complete registration.', + invalidPendingToken: 'The registration token has expired. Please sign in with Linux.do again.', + completeRegistration: 'Complete Registration', + completing: 'Completing registration…', + completeRegistrationFailed: 'Registration failed. Please check your invitation code and try again.' + }, + dingtalk: { + signIn: 'Continue with DingTalk', + callbackTitle: 'Signing you in with DingTalk', + callbackProcessing: 'Completing DingTalk login, please wait...', + callbackHint: 'If you are not redirected automatically, go back to the login page and try again.', + callbackMissingToken: 'Missing login token, please try again.', + backToLogin: 'Back to Login', + invitationRequired: 'This DingTalk account is not yet registered. The site requires an invitation code — please enter one to complete registration.', + invalidPendingToken: 'The registration token has expired. Please sign in with DingTalk again.', + completeRegistration: 'Complete Registration', + completing: 'Completing registration…', + completeRegistrationFailed: 'Registration failed. Please check your invitation code and try again.', + createAccountTitle: 'Create DingTalk Account', + registrationDisabledRedirectToBind: 'New account registration is currently disabled. Please bind to your existing account with its email and password.', + error: { + title: 'DingTalk Sign-in Failed', + csrf: 'Login session expired, please scan again', + corp_rejected: 'Your DingTalk account is not part of this organization. Please contact administrator', + dingtalk_not_enabled: 'DingTalk login is not enabled', + upstream_error: 'DingTalk service is temporarily unavailable. Please try again later', + missing_browser_session: 'Browser session lost. Please login again', + missing_params: 'Request parameters are incomplete', + invalid_state: 'Invalid login state', + provider_error: 'DingTalk authorization failed', + session_error: 'Failed to create session. Please retry', + retry: 'Retry Login' + } + }, + emailOAuth: { + signIn: 'Continue with {providerName}' + }, + oidc: { + signIn: 'Continue with {providerName}', + callbackTitle: 'Signing you in with {providerName}', + callbackProcessing: 'Completing login with {providerName}, please wait...', + callbackHint: 'If you are not redirected automatically, go back to the login page and try again.', + callbackMissingToken: 'Missing login token, please try again.', + backToLogin: 'Back to Login', + invitationRequired: + 'This {providerName} account is not yet registered. The site requires an invitation code — please enter one to complete registration.', + invalidPendingToken: 'The registration token has expired. Please sign in again.', + completeRegistration: 'Complete Registration', + completing: 'Completing registration…', + completeRegistrationFailed: 'Registration failed. Please check your invitation code and try again.' + }, + oauthFlow: { + profileDetailsTitle: 'Use {providerName} profile details', + profileDetailsDescription: 'Choose whether to apply the nickname or avatar from {providerName} to this account.', + useDisplayName: 'Use display name', + useAvatar: 'Use avatar', + avatarAlt: '{providerName} avatar', + reviewProfileBeforeContinue: 'Review the {providerName} profile details before continuing.', + chooseHowToContinue: 'Choose how to continue', + chooseAccountActionHint: 'Choose whether to bind an existing account or create a new one.', + suggestedEmail: 'Suggested email: {email}', + bindExistingAccount: 'Bind existing account', + createNewAccount: 'Create new account', + createAccountHint: 'Enter an email address to create your account and continue.', + bindLoginHint: 'Log in to an existing account to bind this {providerName} sign-in.', + signInThenBindDescription: 'Sign in to an existing account, then bind this {providerName} sign-in to it.', + bindSignInToExistingAccount: 'Bind this {providerName} sign-in to an existing account.', + bindCurrentAccountTitle: 'Bind the current account', + bindCurrentAccountDescription: 'Bind this {providerName} sign-in to the account currently signed in on this browser.', + bindCurrentAccount: 'Bind current account', + logInAndBind: 'Log in and bind', + useDifferentEmail: 'Use a different email', + backToOptions: 'Back to options', + yourAccount: 'your account', + totpHint: 'Enter the 6-digit verification code for {account} to finish binding this {providerName} sign-in.', + verifyAndContinue: 'Verify and continue', + wechatAvailabilityUnknown: 'WeChat sign-in availability could not be confirmed. Refresh and retry.', + wechatSystemBrowserOnly: 'This WeChat sign-in flow is only available in your system browser.', + wechatBrowserOnly: 'This WeChat sign-in flow is only available inside the WeChat browser.', + wechatNotConfigured: 'WeChat sign-in is not configured yet.' + }, + linuxdoCallbackPageTitle: 'LinuxDo Sign-In Callback', + dingtalkCallbackPageTitle: 'DingTalk Sign-In Callback', + oidcCallbackPageTitle: 'OIDC Sign-In Callback', + oauthCallbackPageTitle: 'OAuth Callback', + wechatProviderName: 'WeChat', + wechatCallbackPageTitle: 'WeChat Sign-In Callback', + wechatPaymentCallbackPageTitle: 'WeChat Payment Callback', + wechatPayment: { + callbackTitle: 'Resuming WeChat payment', + callbackProcessing: 'Resuming WeChat payment...', + backToPayment: 'Back to payment', + callbackMissingResumeToken: 'The WeChat payment callback is missing the resume token.' + }, + oauth: { + callbackTitle: 'OAuth Callback', + callbackHint: 'Copy the code and state back to the admin authorization flow when needed.', + invalidCallbackTitle: 'Invalid sign-in callback', + invalidCallbackHint: 'This page does not contain a valid authorization result. Return to the login page and start quick sign-in again.', + code: 'Code', + state: 'State', + fullUrl: 'Full URL' + }, + // Forgot password + forgotPassword: 'Forgot password?', + forgotPasswordTitle: 'Reset Your Password', + forgotPasswordHint: 'Enter your email address and we will send you a link to reset your password.', + sendResetLink: 'Send Reset Link', + sendingResetLink: 'Sending...', + sendResetLinkFailed: 'Failed to send reset link. Please try again.', + resetEmailSent: 'Reset Link Sent', + resetEmailSentHint: 'If an account exists with this email, you will receive a password reset link shortly. Please check your inbox and spam folder.', + backToLogin: 'Back to Login', + rememberedPassword: 'Remembered your password?', + // Reset password + resetPasswordTitle: 'Set New Password', + resetPasswordHint: 'Enter your new password below.', + newPassword: 'Новый пароль', + newPasswordPlaceholder: 'Enter your new password', + confirmPassword: 'Confirm Password', + confirmPasswordPlaceholder: 'Confirm your new password', + confirmPasswordRequired: 'Please confirm your password', + passwordsDoNotMatch: 'Passwords do not match', + resetPassword: 'Reset Password', + resettingPassword: 'Resetting...', + resetPasswordFailed: 'Failed to reset password. Please try again.', + passwordResetSuccess: 'Password Reset Successful', + passwordResetSuccessHint: 'Your password has been reset. You can now sign in with your new password.', + invalidResetLink: 'Invalid Reset Link', + invalidResetLinkHint: 'This password reset link is invalid or has expired. Please request a new one.', + requestNewResetLink: 'Request New Reset Link', + invalidOrExpiredToken: 'The password reset link is invalid or has expired. Please request a new one.' + }, + + // Dashboard + dashboard: { + title: 'Панель', + welcomeMessage: "С возвращением! Вот обзор вашего аккаунта.", + balance: 'Баланс', + apiKeys: 'API-ключи', + todayRequests: 'Запросов сегодня', + todayCost: 'Расход сегодня', + todayTokens: 'Токенов сегодня', + totalTokens: 'Всего токенов', + cacheToday: 'Cache (Today)', + performance: 'Performance', + avgResponse: 'Средний ответ', + averageTime: 'Среднее время', + timeRange: 'Период', + granularity: 'Детализация', + day: 'День', + hour: 'Час', + modelDistribution: 'Распределение моделей', + groupDistribution: 'Group Usage Distribution', + platformBreakdown: 'Per-platform Breakdown', + platformBreakdownEmpty: 'No platform usage yet', + platformCount: '{count} platforms', + platformOther: 'Other', + platformQuota: { + title: 'Quota Usage', + daily: 'Daily', + weekly: 'Weekly', + monthly: 'Monthly (30-day rolling)', + resetsAt: 'Resets {time}', + noLimit: 'unlimited', + disabled: 'Отключено', + }, + tokenUsageTrend: 'Token Usage Trend', + noDataAvailable: 'Нет данных', + model: 'Модель', + group: 'Группа', + noGroup: 'No Group', + requests: 'Запросы', + tokens: 'Tokens', + actual: 'Actual', + standard: 'Standard', + input: 'Input', + output: 'Output', + cache: 'Cache', + recentUsage: 'Последний расход', + last7Days: 'Последние 7 дней', + noUsageRecords: 'Нет записей расхода', + startUsingApi: 'Начните использовать API, чтобы увидеть историю расхода.', + viewAllUsage: 'Весь расход', + quickActions: 'Быстрые действия', + createApiKey: 'Создать API-ключ', + generateNewKey: 'Создать новый API-ключ', + viewUsage: 'Посмотреть расход', + checkDetailedLogs: 'Открыть подробные логи расхода', + redeemCode: 'Активировать код', + addBalanceWithCode: 'Пополнить баланс кодом' + }, + + // Groups (shared) + groups: { + subscription: 'Sub' + }, + + // API Keys + keys: { + title: 'API-ключи', + description: 'Управляйте API-ключами и токенами доступа', + searchPlaceholder: 'Поиск по имени или ключу...', + endpoints: { + title: 'API-эндпоинты', + default: 'По умолчанию', + copied: 'Скопировано', + copiedHint: 'Скопировано в буфер', + clickToCopy: 'Нажмите, чтобы скопировать endpoint', + speedTest: 'Тест скорости', + }, + allGroups: 'Все группы', + allStatus: 'Все статусы', + createKey: 'Создать API-ключ', + editKey: 'Изменить API-ключ', + deleteKey: 'Удалить API-ключ', + deleteConfirmMessage: "Are you sure you want to delete '{name}'? This action cannot be undone.", + apiKey: 'API-ключ', + group: 'Группа', + noGroup: 'Без группы', + searchGroup: 'Поиск групп...', + noGroupFound: 'Группы не найдены', + created: 'Создан', + copyToClipboard: 'Копировать', + copied: 'Copied!', + importToCcSwitch: 'Import to CCS', + enable: 'Включить', + disable: 'Отключить', + nameLabel: 'Имя', + namePlaceholder: 'Мой API-ключ', + groupLabel: 'Группа', + selectGroup: 'Выберите группу', + statusLabel: 'Статус', + selectStatus: 'Выберите статус', + saving: 'Сохранение...', + noKeysYet: 'API-ключей пока нет', + createFirstKey: 'Create your first API key to get started with the API.', + keyCreatedSuccess: 'API-ключ создан', + keyUpdatedSuccess: 'API-ключ обновлён', + keyDeletedSuccess: 'API-ключ удалён', + keyEnabledSuccess: 'API key enabled successfully', + keyDisabledSuccess: 'API key disabled successfully', + failedToLoad: 'Failed to load API keys', + failedToSave: 'Failed to save API key', + failedToDelete: 'Failed to delete API key', + failedToUpdateStatus: 'Failed to update API key status', + clickToChangeGroup: 'Click to change group', + groupChangedSuccess: 'Group changed successfully', + failedToChangeGroup: 'Failed to change group', + groupRequired: 'Please select a group', + usage: 'Расход', + today: 'Сегодня', + total: 'Last 30d', + quota: 'Quota', + lastUsedAt: 'Last Used', + useKey: 'Use Key', + useKeyModal: { + title: 'Use API Key', + description: + 'Add the following environment variables to your terminal profile or run directly in terminal to configure API access.', + copy: 'Копировать', + copied: 'Скопировано', + note: 'These environment variables will be active in the current terminal session. For permanent configuration, add them to ~/.bashrc, ~/.zshrc, or the appropriate configuration file.', + noGroupTitle: 'Please assign a group first', + noGroupDescription: 'This API key has not been assigned to a group. Please click the group column in the key list to assign one before viewing the configuration.', + openai: { + description: 'Add the following configuration files to your Codex CLI config directory.', + configTomlHint: 'Make sure the following content is at the beginning of the config.toml file', + note: 'Make sure the config directory exists. macOS/Linux users can run mkdir -p ~/.codex to create it.', + noteWindows: 'Press Win+R and enter %userprofile%\\.codex to open the config directory. Create it manually if it does not exist.', + }, + cliTabs: { + claudeCode: 'Claude Code', + geminiCli: 'Gemini CLI', + codexCli: 'Codex CLI', + codexCliWs: 'Codex CLI (WebSocket)', + opencode: 'OpenCode', + }, + antigravity: { + description: 'Configure API access for Antigravity group. Select the configuration method based on your client.', + claudeCode: 'Claude Code', + geminiCli: 'Gemini CLI', + claudeNote: 'These environment variables will be active in the current terminal session. For permanent configuration, add them to ~/.bashrc, ~/.zshrc, or the appropriate configuration file.', + geminiNote: 'These environment variables will be active in the current terminal session. For permanent configuration, add them to ~/.bashrc, ~/.zshrc, or the appropriate configuration file.', + }, + gemini: { + description: 'Add the following environment variables to your terminal profile or run directly in terminal to configure Gemini CLI access.', + modelComment: 'If you have Gemini 3 access, you can use: gemini-3-pro-preview', + note: 'These environment variables will be active in the current terminal session. For permanent configuration, add them to ~/.bashrc, ~/.zshrc, or the appropriate configuration file.', + }, + opencode: { + title: 'OpenCode Example', + subtitle: 'opencode.json', + hint: 'Config path: ~/.config/opencode/opencode.json (or opencode.jsonc), create if not exists. Use default providers (openai/anthropic/google) or custom provider_id. API Key can be configured directly or via /connect command. This is an example, adjust models and options as needed.', + }, + }, + customKeyLabel: 'Custom Key', + customKeyPlaceholder: 'Enter your custom key (min 16 chars)', + customKeyHint: 'Only letters, numbers, underscores and hyphens allowed. Minimum 16 characters.', + customKeyTooShort: 'Custom key must be at least 16 characters', + customKeyInvalidChars: 'Custom key can only contain letters, numbers, underscores, and hyphens', + customKeyRequired: 'Please enter a custom key', + ipRestriction: 'IP Restriction', + ipWhitelist: 'IP Whitelist', + ipWhitelistPlaceholder: '192.168.1.100\n10.0.0.0/8', + ipWhitelistHint: 'One IP or CIDR per line. Only these IPs can use this key when set.', + ipBlacklist: 'IP Blacklist', + ipBlacklistPlaceholder: '1.2.3.4\n5.6.0.0/16', + ipBlacklistHint: 'One IP or CIDR per line. These IPs will be blocked from using this key.', + ipRestrictionEnabled: 'IP restriction enabled', + ccSwitchNotInstalled: 'CC-Switch is not installed or the protocol handler is not registered. Please install CC-Switch first or manually copy the API key.', + ccsClientSelect: { + title: 'Select Client', + description: 'Please select the client type to import to CC-Switch:', + claudeCode: 'Claude Code', + claudeCodeDesc: 'Import as Claude Code configuration', + geminiCli: 'Gemini CLI', + geminiCliDesc: 'Import as Gemini CLI configuration', + }, + // Quota and expiration + quotaLimit: 'Quota Limit', + quotaAmount: 'Quota Amount (USD)', + quotaAmountPlaceholder: 'Enter quota limit in USD', + quotaAmountHint: 'Set the maximum amount this key can spend. 0 = unlimited.', + quotaUsed: 'Quota Used', + reset: 'Сбросить', + resetQuotaUsed: 'Reset used quota to 0', + resetQuotaTitle: 'Confirm Reset Quota', + resetQuotaConfirmMessage: 'Are you sure you want to reset the used quota (${used}) for key "{name}" to 0? This action cannot be undone.', + quotaResetSuccess: 'Quota reset successfully', + failedToResetQuota: 'Failed to reset quota', + rateLimitColumn: 'Rate Limit', + rateLimitSection: 'Rate Limit', + resetUsage: 'Сбросить', + rateLimit5h: '5-Hour Limit (USD)', + rateLimit1d: 'Daily Limit (USD)', + rateLimit7d: '7-Day Limit (USD)', + rateLimitHint: 'Set the maximum spending for this key within each time window. 0 = unlimited.', + rateLimitUsage: 'Rate Limit Usage', + resetRateLimitUsage: 'Reset Rate Limit Usage', + resetRateLimitTitle: 'Confirm Reset Rate Limit', + resetRateLimitConfirmMessage: 'Are you sure you want to reset the rate limit usage for key "{name}"? All time window usage will be reset to zero. This action cannot be undone.', + rateLimitResetSuccess: 'Rate limit usage reset successfully', + failedToResetRateLimit: 'Failed to reset rate limit usage', + resetNow: 'Скоро сброс', + expiration: 'Expiration', + expiresInDays: '{days} days', + extendDays: '+{days} days', + customDate: 'Свой', + expirationDate: 'Expiration Date', + expirationDateHint: 'Select when this API key should expire.', + currentExpiration: 'Current expiration', + expiresAt: 'Expires', + noExpiration: 'Никогда', + status: { + active: 'Активен', + inactive: 'Неактивен', + quota_exhausted: 'Quota Exhausted', + expired: 'Истекла', + }, + }, + + // Usage + usage: { + title: 'История расхода', + description: 'Просматривайте и анализируйте историю использования API', + costDetails: 'Разбивка стоимости', + tokenDetails: 'Разбивка токенов', + cacheTtlOverriddenHint: 'Cache TTL Override enabled', + cacheTtlOverriddenLabel: 'TTL Override', + cacheTtlOverridden5m: 'Billed as 5m', + cacheTtlOverridden1h: 'Billed as 1h', + totalRequests: 'Всего запросов', + totalTokens: 'Всего токенов', + totalCost: 'Общая стоимость', + standardCost: 'Standard', + actualCost: 'Actual', + accountCost: 'Стоимость', + userBilled: 'User billed', + accountBilled: 'Account billed', + accountMultiplier: 'Account rate', + avgDuration: 'Средняя длительность', + inSelectedRange: 'in selected range', + perRequest: 'per request', + apiKeyFilter: 'API-ключ', + allApiKeys: 'Все API-ключи', + timeRange: 'Период', + exportCsv: 'Экспорт CSV', + exportExcel: 'Экспорт Excel', + exportingProgress: 'Экспорт данных...', + exportedCount: 'Exported {current}/{total} records', + estimatedTime: 'Estimated time remaining: {time}', + cancelExport: 'Cancel Export', + exportCancelled: 'Export cancelled', + exporting: 'Exporting...', + preparingExport: 'Preparing export...', + model: 'Модель', + requestedModel: 'Requested', + upstreamModel: 'Upstream', + reasoningEffort: 'Reasoning Effort', + endpoint: 'Endpoint', + endpointDistribution: 'Endpoint Distribution', + inbound: 'Inbound', + upstream: 'Upstream', + mapping: 'Mapping', + path: 'Path', + inboundEndpoint: 'Inbound Endpoint', + upstreamEndpoint: 'Upstream Endpoint', + type: 'Type', + tokens: 'Tokens', + cost: 'Стоимость', + firstToken: 'First Token', + duration: 'Duration', + time: 'Time', + ws: 'WS', + stream: 'Stream', + sync: 'Sync', + unknown: 'Неизвестно', + in: 'In', + out: 'Out', + inputTokenPrice: 'Input price', + outputTokenPrice: 'Output price', + perMillionTokens: '/ 1M tokens', + unitPrice: 'Per-request price', + imageUnitPrice: 'Per-image price', + imageTotalPrice: 'Image total price', + imageCount: 'Image count', + imageBillingSize: 'Billing size', + imageInputSize: 'Input size', + imageOutputSize: 'Output size', + imageSizeSource: 'Size source', + imageSizeBreakdown: 'Size breakdown', + imageSizeSourceOutput: 'Upstream output', + imageSizeSourceInput: 'Request input', + imageSizeSourceDefault: 'Default billing tier', + imageSizeSourceLegacy: 'Legacy record', + imageSizeSourceMissing: 'Not recorded', + imageSizeNotRecorded: 'not recorded', + imageSizeLegacyUnstandardized: 'legacy unstandardized', + imageSizeUnknown: 'unknown', + cacheRead: 'Read', + cacheWrite: 'Write', + serviceTier: 'Service tier', + serviceTierPriority: 'Fast', + serviceTierFlex: 'Flex', + serviceTierStandard: 'Standard', + rate: 'Тариф', + original: 'Original', + billed: 'Billed', + noRecords: 'Записи расхода не найдены. Попробуйте изменить фильтры.', + failedToLoad: 'Не удалось загрузить логи расхода', + noDataToExport: 'Нет данных для экспорта', + exportSuccess: 'Данные расхода экспортированы', + exportFailed: 'Failed to export usage data', + exportExcelSuccess: 'Usage data exported successfully (Excel format)', + exportExcelFailed: 'Failed to export usage data', + imageUnit: ' images', + userAgent: 'User-Agent' + }, + + // Shared keys for channel monitor (admin + user views) + monitorCommon: { + status: { + operational: 'Operational', + degraded: 'Degraded', + failed: 'Ошибка', + error: 'Ошибка', + unknown: '-' + }, + providers: { + openai: 'OpenAI', + anthropic: 'Anthropic', + gemini: 'Gemini' + }, + extraModelsHeader: 'Extra Models', + extraModelsEmpty: 'No extra models', + latencyEmpty: '-', + availabilityPrefix: 'Availability', + dialogLatency: 'Dialog Latency', + endpointPing: 'Endpoint PING', + history60pts: 'HISTORY ({n} PTS)', + nextUpdateIn: 'NEXT UPDATE IN {n}s', + past: 'PAST', + now: 'NOW', + maintenancePaused: 'Maintenance · timeline paused', + extraModelsCount: '+ {n} models', + pollEvery: '{n}s polling', + updatedAt: 'Updated {time}', + relativeSecondsAgo: '{n}s ago', + relativeMinutesAgo: '{n} мин назад', + relativeHoursAgo: '{n} ч назад', + relativeDaysAgo: '{n} дн назад' + }, + + // Channel Status (user-facing read-only view) + channelStatus: { + title: 'Статус каналов', + description: 'Inspect channel availability, latency and recent status', + searchPlaceholder: 'Search channels...', + allProviders: 'All Providers', + loadError: 'Failed to load channel status', + detailLoadError: 'Failed to load channel detail', + detailTitle: 'Channel Detail', + closeDetail: 'Закрыть', + windowTab: { + '7d': '7 days', + '15d': '15 days', + '30d': '30 days' + }, + overall: { + operational: 'OPERATIONAL', + degraded: 'DEGRADED', + unavailable: 'UNAVAILABLE' + }, + columns: { + name: 'Имя', + provider: 'Provider', + groupName: 'Группа', + primaryModel: 'Primary Model', + availability7d: '7d Availability', + latency: 'Latency (ms)' + }, + detailColumns: { + model: 'Модель', + latestStatus: 'Latest Status', + latestLatency: 'Latest Latency (ms)', + availability7d: '7d Availability', + availability15d: '15d Availability', + availability30d: '30d Availability', + avgLatency7d: '7d Avg Latency (ms)' + }, + empty: { + title: 'No channels available', + description: 'No monitored channels have been configured yet.' + } + }, + + // Available Channels (user-facing) + availableChannels: { + title: 'Доступные каналы', + description: 'Channels you can access, along with their supported models and pricing', + searchPlaceholder: 'Search channels or models...', + empty: 'No available channels', + noModels: 'No models configured', + noPricing: 'Pricing not configured', + exclusive: 'Exclusive', + public: 'Public', + exclusiveTooltip: 'Exclusive groups granted to you by an admin', + publicTooltip: 'Groups open to all users', + columns: { + name: 'Channel', + description: 'Description', + platform: 'Platform', + groups: 'Your Accessible Groups', + supportedModels: 'Supported Models' + }, + pricing: { + billingMode: 'Billing Mode', + billingModeToken: 'Per Token', + billingModePerRequest: 'Per Request', + billingModeImage: 'Per Image', + inputPrice: 'Input', + outputPrice: 'Output', + cacheWritePrice: 'Cache Write', + cacheReadPrice: 'Cache Read', + imageOutputPrice: 'Image Output', + perRequestPrice: 'Per Request', + intervals: 'Tiered Pricing', + unitPerMillion: '/ 1M tokens', + unitPerRequest: '/ request' + } + }, + + affiliate: { + title: 'Партнёрские бонусы', + description: 'Invite new users and convert your rebate quota into account balance', + yourCode: 'Your Affiliate Code', + inviteLink: 'Invite Link', + copyCode: 'Copy Code', + copyLink: 'Copy Link', + codeCopied: 'Affiliate code copied', + linkCopied: 'Invite link copied', + loadFailed: 'Failed to load affiliate data', + transferFailed: 'Failed to transfer affiliate quota', + stats: { + rebateRate: 'My Rebate Rate', + rebateRateHint: 'What you earn each time an invitee recharges', + invitedUsers: 'Invited Users', + availableQuota: 'Available Rebate Quota', + frozenQuota: 'Frozen', + frozenQuotaHint: 'Recently earned rebates pending release', + totalQuota: 'Historical Rebate Quota' + }, + transfer: { + title: 'Transfer Rebate Quota', + description: 'Move available rebate quota into your account balance', + button: 'Transfer to Balance', + transferring: 'Transferring...', + empty: 'No available rebate quota', + success: '{amount} has been transferred to your balance' + }, + invitees: { + title: 'Invited Users', + empty: 'No invited users yet', + columns: { + email: 'Email', + username: 'Имя пользователя', + rebate: 'Rebate', + joinedAt: 'Joined At' + } + }, + tips: { + title: 'How It Works', + line1: 'Share your affiliate code or invite link with new users.', + line2: 'When invitees recharge, you receive {rate} of the recharge as rebate quota.', + line3: 'Transfer rebate quota to balance at any time.', + line4: 'Newly earned rebates may have a waiting period before they can be transferred.' + } + }, + + // Redeem + redeem: { + title: 'Активировать код', + description: 'Введите код, чтобы пополнить баланс или увеличить параллелизм', + currentBalance: 'Текущий баланс', + concurrency: 'Параллелизм', + requests: 'запросов', + redeemCodeLabel: 'Активировать код', + redeemCodePlaceholder: 'Введите код активации', + redeemCodeHint: 'Коды чувствительны к регистру', + redeeming: 'Активация...', + redeemButton: 'Активировать код', + redeemSuccess: 'Код успешно активирован!', + redeemFailed: 'Активация не удалась', + added: 'Добавлено', + concurrentRequests: 'concurrent requests', + newBalance: 'Новый баланс', + newConcurrency: 'Новый параллелизм', + aboutCodes: 'О кодах активации', + codeRule1: 'Каждый код можно использовать только один раз', + codeRule2: 'Codes may add balance, increase concurrency, or grant trial access', + codeRule3: 'Contact support if you have issues redeeming a code', + codeRule4: 'Balance and concurrency updates are immediate', + recentActivity: 'Recent Activity', + historyWillAppear: 'Your redemption history will appear here', + balanceAddedRedeem: 'Balance Added (Redeem)', + balanceAddedAffiliate: 'Balance Added (Affiliate Transfer)', + balanceAddedAdmin: 'Balance Added (Admin)', + balanceDeductedAdmin: 'Balance Deducted (Admin)', + concurrencyAddedRedeem: 'Concurrency Added (Redeem)', + concurrencyAddedAdmin: 'Concurrency Added (Admin)', + concurrencyReducedAdmin: 'Concurrency Reduced (Admin)', + adminAdjustment: 'Admin Adjustment', + subscriptionAssigned: 'Subscription Assigned', + subscriptionAssignedDesc: 'You have been granted access to {groupName}', + subscriptionDays: '{days} days', + days: ' days', + codeRedeemSuccess: 'Code redeemed successfully!', + failedToRedeem: 'Failed to redeem code. Please check the code and try again.', + subscriptionRefreshFailed: 'Redeemed successfully, but failed to refresh subscription status.', + pleaseEnterCode: 'Please enter a redeem code' + }, + + // Profile + profile: { + title: 'Настройки профиля', + description: 'Управляйте информацией и настройками аккаунта', + accountBalance: 'Баланс аккаунта', + concurrencyLimit: 'Лимит параллелизма', + rpmLimit: 'RPM Limit', + rpmUnlimited: 'Безлимитно', + memberSince: 'Дата регистрации', + overviewTitle: 'Обзор аккаунта', + overviewDescription: 'Check account status, profile sources, and common actions at a glance.', + basicsTitle: 'Профиль и аватар', + basicsDescription: 'Keep your public profile details and avatar aligned.', + linkedProfileSources: 'Profile Sources', + linkedProfileSourcesDescription: 'Some profile details may stay synced from third-party sign-in methods.', + securityTitle: 'Настройки безопасности', + securityDescription: 'Password, two-factor authentication, and alerts live in the right rail.', + administrator: 'Администратор', + user: 'Пользователь', + username: 'Имя пользователя', + email: 'Email', + status: 'Статус', + role: 'Роль', + enterUsername: 'Введите имя пользователя', + editProfile: 'Изменить профиль', + updateProfile: 'Обновить профиль', + updating: 'Updating...', + updateSuccess: 'Профиль обновлён', + updateFailed: 'Не удалось обновить профиль', + usernameRequired: 'Username is required', + changePassword: 'Сменить пароль', + currentPassword: 'Текущий пароль', + newPassword: 'Новый пароль', + confirmNewPassword: 'Подтвердите новый пароль', + passwordHint: 'Password must be at least 8 characters long', + changingPassword: 'Changing...', + changePasswordButton: 'Сменить пароль', + passwordsNotMatch: 'New passwords do not match', + passwordTooShort: 'Password must be at least 8 characters long', + passwordChangeSuccess: 'Password changed successfully', + passwordChangeFailed: 'Failed to change password', + // TOTP 2FA + totp: { + title: 'Двухфакторная аутентификация (2FA)', + description: 'Enhance account security with Google Authenticator or similar apps', + enabled: 'Включено', + enabledAt: 'Enabled at', + notEnabled: 'Not Enabled', + notEnabledHint: 'Enable two-factor authentication to enhance account security', + enable: 'Включить', + disable: 'Отключить', + featureDisabled: 'Feature Unavailable', + featureDisabledHint: 'Two-factor authentication has not been enabled by the administrator', + setupTitle: 'Set Up Two-Factor Authentication', + setupStep1: 'Scan the QR code below with your authenticator app', + setupStep2: 'Enter the 6-digit code from your app', + manualEntry: "Can't scan? Enter the key manually:", + enterCode: 'Enter 6-digit code', + verify: 'Verify', + setupFailed: 'Failed to get setup information', + verifyFailed: 'Invalid code, please try again', + enableSuccess: 'Two-factor authentication enabled', + disableTitle: 'Disable Two-Factor Authentication', + disableWarning: 'After disabling, you will no longer need a verification code to log in. This may reduce your account security.', + enterPassword: 'Enter your current password to confirm', + confirmDisable: 'Confirm Disable', + disableSuccess: 'Two-factor authentication disabled', + disableFailed: 'Failed to disable, please check your password', + loginTitle: 'Two-Factor Authentication', + loginHint: 'Enter the 6-digit code from your authenticator app', + loginFailed: 'Verification failed, please try again', + // New translations for email verification + verifyEmailFirst: 'Please verify your email first', + verifyPasswordFirst: 'Please verify your identity first', + emailCode: 'Email Verification Code', + enterEmailCode: 'Enter 6-digit code', + sendCode: 'Send Code', + codeSent: 'Verification code sent to your email', + sendCodeFailed: 'Failed to send verification code' + }, + balanceNotify: { + title: 'Balance Low Notification', + description: 'Send email alert when account balance falls below threshold', + enabled: 'Enable Balance Low Notification', + threshold: 'Custom Threshold', + thresholdHint: 'Leave empty to use system default', + thresholdPlaceholder: 'Введите сумму', + systemDefault: 'System Default', + extraEmails: 'Notification Emails', + extraEmailsHint: 'You must add and verify an email address to receive low balance alerts', + primaryEmail: 'Primary', + noExtraEmails: 'No extra notification emails', + enterEmail: 'Enter email address', + addEmail: 'Add Email', + emailPlaceholder: 'Enter email address', + sendCode: 'Send Code', + resend: 'Resend', + codeSent: 'Verification code sent', + codeSentTo: 'Code sent to {email}', + enterCode: 'Enter verification code', + codePlaceholder: '6-digit code', + verify: 'Verify', + emailAdded: 'Email added', + emailRemoved: 'Email removed', + verifySuccess: 'Email added successfully', + removeEmail: 'Remove', + removeSuccess: 'Email removed', + emailDuplicate: 'This email already exists', + maxEmailsReached: 'Maximum number of notification emails reached', + unverified: 'Unverified', + verified: 'Verified', + }, + avatar: { + title: 'Profile Avatar', + description: 'Upload an avatar image. Static uploads are compressed to 20KB before saving.', + uploadAction: 'Upload image', + uploadHint: 'Static uploads are compressed to 20KB when possible. GIF uploads must already be within 20KB.', + uploadRequired: 'Upload an avatar image first', + saveSuccess: 'Avatar updated', + deleteSuccess: 'Avatar removed', + invalidType: 'Please choose an image file', + gifTooLarge: 'GIF avatars must already be 20KB or smaller', + compressTooLarge: 'Unable to compress this image below 20KB. Try a smaller image.', + compressFailed: 'Failed to compress the selected image.', + readFailed: 'Failed to read the selected image.', + emptyDeleteHint: 'Avatar is already empty', + }, + authBindings: { + title: 'Connected Sign-In Methods', + description: 'View current bindings and connect another provider to this account.', + bindAction: 'Bind {providerName}', + bindSuccess: 'Account linked successfully', + emailPlaceholder: 'Enter email address', + codePlaceholder: 'Enter verification code', + passwordPlaceholder: 'Set a login password', + replaceEmailPasswordPlaceholder: 'Enter current password', + sendCodeAction: 'Отправить код', + manageEmailAction: 'Manage email', + hideEmailFormAction: 'Hide email form', + confirmEmailBindAction: 'Bind email', + confirmEmailReplaceAction: 'Replace primary email', + codeSentTo: 'Code sent to {email}', + replaceSuccess: 'Primary email updated', + unbindAction: 'Unbind', + unbindSuccess: '{providerName} unbound', + boundCount: '{count} linked records', + status: { + bound: 'Bound', + notBound: 'Not bound', + }, + providers: { + email: 'Email', + linuxdo: 'LinuxDo', + dingtalk: 'DingTalk', + oidc: '{providerName}', + wechat: 'WeChat', + }, + notes: { + emailManagedFromProfile: 'Primary email is managed in the profile form', + canUnbind: 'You can unbind this sign-in method', + bindAnotherBeforeUnbind: 'Bind another sign-in method before unbinding', + }, + source: { + avatar: 'Avatar is currently synced from {providerName}', + username: 'Nickname is currently synced from {providerName}', + }, + } + }, + + // Empty States + empty: { + noData: 'Данные не найдены' + }, + + // Table + table: { + expandActions: 'Показать больше действий', + collapseActions: 'Свернуть действия' + }, + + // Pagination + pagination: { + showing: 'Показано', + to: '–', + of: 'из', + results: 'результатов', + page: 'Страница', + pageOf: 'Страница {page} из {total}', + previous: 'Назад', + next: 'Далее', + perPage: 'На странице', + goToPage: 'Перейти на страницу {page}', + jumpTo: 'Перейти к', + jumpPlaceholder: 'Страница', + jumpAction: 'Перейти' + }, + + // Errors + errors: { + somethingWentWrong: 'Что-то пошло не так', + pageNotFound: 'Страница не найдена', + unauthorized: 'Не авторизован', + forbidden: 'Доступ запрещён', + serverError: 'Ошибка сервера', + networkError: 'Ошибка сети', + timeout: 'Тайм-аут запроса', + tryAgain: 'Попробуйте снова' + }, + + // Dates + dates: { + today: 'Сегодня', + yesterday: 'Вчера', + thisWeek: 'Эта неделя', + lastWeek: 'Прошлая неделя', + thisMonth: 'Этот месяц', + lastMonth: 'Прошлый месяц', + last24Hours: 'Последние 24 часа', + last7Days: 'Последние 7 дней', + last14Days: 'Последние 14 дней', + last30Days: 'Последние 30 дней', + custom: 'Свой', + startDate: 'Дата начала', + endDate: 'Дата окончания', + apply: 'Применить', + selectDateRange: 'Выберите период' + }, + + // Admin + admin: { + // Dashboard + dashboard: { + title: 'Admin Dashboard', + description: 'System overview and real-time statistics', + apiKeys: 'API-ключи', + accounts: 'Аккаунты', + users: 'Пользователи', + todayRequests: 'Запросов сегодня', + newUsersToday: 'New Users Today', + todayTokens: 'Токенов сегодня', + totalTokens: 'Всего токенов', + cacheToday: 'Cache (Today)', + performance: 'Performance', + avgResponse: 'Средний ответ', + active: 'active', + ok: 'ok', + err: 'err', + activeUsers: 'active users', + create: 'Создать', + timeRange: 'Период', + granularity: 'Детализация', + day: 'День', + hour: 'Час', + modelDistribution: 'Распределение моделей', + groupDistribution: 'Group Usage Distribution', + metricTokens: 'By Tokens', + metricActualCost: 'By Actual Cost', + tokenUsageTrend: 'Token Usage Trend', + userUsageTrend: 'User Usage Trend (Top 12)', + model: 'Модель', + group: 'Группа', + noGroup: 'No Group', + requests: 'Запросы', + tokens: 'Tokens', + actual: 'Actual', + standard: 'Standard', + accountCost: 'Стоимость', + noDataAvailable: 'Нет данных', + recentUsage: 'Последний расход', + viewModelDistribution: 'Распределение моделей', + viewSpendingRanking: 'User Spending Ranking', + spendingRankingTitle: 'User Spending Ranking', + spendingRankingUser: 'Пользователь', + spendingRankingRequests: 'Запросы', + spendingRankingTokens: 'Tokens', + spendingRankingSpend: 'Spend', + spendingRankingOther: 'Others', + spendingRankingUsage: 'Расход', + spendShort: 'Spend', + requestsShort: 'Req', + tokensShort: 'Tok', + failedToLoad: 'Failed to load dashboard statistics' + }, + + backup: { + title: 'Database Backup', + description: 'Full database backup to S3-compatible storage with scheduled backup and restore', + s3: { + title: 'S3 Storage Configuration', + description: 'Configure S3-compatible storage (supports Cloudflare R2)', + descriptionPrefix: 'Configure S3-compatible storage (supports', + descriptionSuffix: ')', + enabled: 'Enable S3 Storage', + endpoint: 'Endpoint', + region: 'Region', + bucket: 'Bucket', + prefix: 'Key Prefix', + accessKeyId: 'Access Key ID', + secretAccessKey: 'Secret Access Key', + secretConfigured: 'Already configured, leave empty to keep', + forcePathStyle: 'Force Path Style', + testConnection: 'Test Connection', + testSuccess: 'S3 connection test successful', + testFailed: 'S3 connection test failed', + saved: 'S3 configuration saved' + }, + schedule: { + title: 'Scheduled Backup', + description: 'Configure automatic scheduled backups', + enabled: 'Enable Scheduled Backup', + cronExpr: 'Cron Expression', + cronHint: 'e.g. "0 2 * * *" means every day at 2:00 AM', + retainDays: 'Backup Expire Days', + retainDaysHint: 'Backup files auto-delete after this many days, 0 = never expire', + retainCount: 'Max Retain Count', + retainCountHint: 'Maximum number of backups to keep, 0 = unlimited', + saved: 'Schedule configuration saved' + }, + operations: { + title: 'Backup Records', + description: 'Create manual backups and manage existing backup records', + createBackup: 'Create Backup', + backing: 'Backing up...', + backupCreated: 'Backup created successfully', + expireDays: 'Expire Days', + alreadyInProgress: 'A backup is already in progress', + backupRunning: 'Backup in progress...', + backupFailed: 'Backup failed', + restoreRunning: 'Restore in progress...', + restoreFailed: 'Restore failed', + }, + columns: { + status: 'Статус', + fileName: 'File Name', + size: 'Size', + expiresAt: 'Истекает', + triggeredBy: 'Triggered By', + startedAt: 'Started At', + actions: 'Действия' + }, + status: { + pending: 'Ожидает', + running: 'Running', + completed: 'Завершено', + failed: 'Ошибка' + }, + progress: { + pending: 'Preparing', + dumping: 'Dumping database', + uploading: 'Uploading', + }, + trigger: { + manual: 'Manual', + scheduled: 'Scheduled' + }, + neverExpire: 'Никогда', + empty: 'No backup records', + actions: { + download: 'Download', + restore: 'Restore', + restoreConfirm: 'Are you sure you want to restore from this backup? This will overwrite the current database!', + restorePasswordPrompt: 'Please enter your admin password to confirm the restore operation', + restoreSuccess: 'Database restored successfully', + deleteConfirm: 'Are you sure you want to delete this backup?', + deleted: 'Backup deleted' + }, + r2Guide: { + title: 'Cloudflare R2 Setup Guide', + intro: 'Cloudflare R2 provides S3-compatible object storage with a free tier of 10GB storage + 1M Class A requests/month, ideal for database backups.', + step1: { + title: 'Create an R2 Bucket', + line1: 'Log in to the Cloudflare Dashboard (dash.cloudflare.com), select "R2 Object Storage" from the sidebar', + line2: 'Click "Create bucket", enter a name (e.g. sub2api-backups), choose a region', + line3: 'Click create to finish' + }, + step2: { + title: 'Create an API Token', + line1: 'On the R2 page, click "Manage R2 API Tokens" in the top right', + line2: 'Click "Create API token", set permission to "Object Read & Write"', + line3: 'Recommended: restrict to specific bucket for better security', + line4: 'After creation, you will see the Access Key ID and Secret Access Key', + warning: 'The Secret Access Key is only shown once — copy and save it immediately!' + }, + step3: { + title: 'Get the S3 Endpoint', + desc: 'Find your Account ID on the R2 overview page (in the URL or the right panel). The endpoint format is:', + accountId: 'your_account_id' + }, + step4: { + title: 'Fill in the Configuration', + checkEnabled: 'Checked', + bucketValue: 'Your bucket name', + fromStep2: 'Value from Step 2', + unchecked: 'Unchecked' + }, + freeTier: 'R2 Free Tier: 10GB storage + 1M Class A requests + 10M Class B requests per month — more than enough for database backups.' + } + }, + + dataManagement: { + title: 'Data Management', + description: 'Manage data management agent status, object storage settings, and backup jobs in one place', + agent: { + title: 'Data Management Agent Status', + description: 'The system probes a fixed Unix socket and enables data management only when reachable.', + enabled: 'Data management agent is ready. Data management operations are available.', + disabled: 'Data management agent is unavailable. Only diagnostic information is available now.', + socketPath: 'Socket Path', + version: 'Version', + status: 'Статус', + uptime: 'Uptime', + reasonLabel: 'Unavailable Reason', + reason: { + DATA_MANAGEMENT_AGENT_SOCKET_MISSING: 'Data management socket file is missing', + DATA_MANAGEMENT_AGENT_UNAVAILABLE: 'Data management agent is unreachable', + BACKUP_AGENT_SOCKET_MISSING: 'Backup socket file is missing', + BACKUP_AGENT_UNAVAILABLE: 'Backup agent is unreachable', + UNKNOWN: 'Unknown reason' + } + }, + sections: { + config: { + title: 'Backup Configuration', + description: 'Configure backup source, retention policy, and S3 settings.' + }, + s3: { + title: 'S3 Object Storage', + description: 'Configure and test uploads of backup artifacts to a standard S3-compatible storage.' + }, + backup: { + title: 'Backup Operations', + description: 'Trigger PostgreSQL, Redis, and full backup jobs.' + }, + history: { + title: 'Backup History', + description: 'Review backup job status, errors, and artifact metadata.' + } + }, + form: { + sourceMode: 'Source Mode', + backupRoot: 'Backup Root', + activePostgresProfile: 'Active PostgreSQL Profile', + activeRedisProfile: 'Active Redis Profile', + activeS3Profile: 'Active S3 Profile', + retentionDays: 'Retention Days', + keepLast: 'Keep Last Jobs', + uploadToS3: 'Upload to S3', + useActivePostgresProfile: 'Use Active PostgreSQL Profile', + useActiveRedisProfile: 'Use Active Redis Profile', + useActiveS3Profile: 'Use Active Profile', + idempotencyKey: 'Idempotency Key (Optional)', + secretConfigured: 'Configured already, leave empty to keep unchanged', + source: { + profileID: 'Profile ID (Unique)', + profileName: 'Profile Name', + setActive: 'Set as active after creation' + }, + postgres: { + title: 'PostgreSQL', + host: 'Host', + port: 'Port', + user: 'Пользователь', + password: 'Пароль', + database: 'Database', + sslMode: 'SSL Mode', + containerName: 'Container Name (docker_exec mode)' + }, + redis: { + title: 'Redis', + addr: 'Address (host:port)', + username: 'Имя пользователя', + password: 'Пароль', + db: 'Database Index', + containerName: 'Container Name (docker_exec mode)' + }, + s3: { + enabled: 'Enable S3 Upload', + profileID: 'Profile ID (Unique)', + profileName: 'Profile Name', + endpoint: 'Endpoint (Optional)', + region: 'Region', + bucket: 'Bucket', + accessKeyID: 'Access Key ID', + secretAccessKey: 'Secret Access Key', + prefix: 'Object Prefix', + forcePathStyle: 'Force Path Style', + useSSL: 'Use SSL', + setActive: 'Set as active after creation' + } + }, + sourceProfiles: { + createTitle: 'Create Source Profile', + editTitle: 'Edit Source Profile', + empty: 'No source profiles yet, create one first', + deleteConfirm: 'Delete source profile {profileID}?', + columns: { + profile: 'Профиль', + active: 'Активен', + connection: 'Connection', + database: 'Database', + updatedAt: 'Updated At', + actions: 'Действия' + } + }, + s3Profiles: { + createTitle: 'Create S3 Profile', + editTitle: 'Edit S3 Profile', + empty: 'No S3 profiles yet, create one first', + editHint: 'Click "Изменить" to modify profile details in the right drawer.', + deleteConfirm: 'Delete S3 profile {profileID}?', + columns: { + profile: 'Профиль', + active: 'Активен', + storage: 'Storage', + updatedAt: 'Updated At', + actions: 'Действия' + } + }, + history: { + total: '{count} jobs', + empty: 'No backup jobs yet', + columns: { + jobID: 'Job ID', + type: 'Type', + status: 'Статус', + triggeredBy: 'Triggered By', + pgProfile: 'PostgreSQL Profile', + redisProfile: 'Redis Profile', + s3Profile: 'S3 Profile', + finishedAt: 'Finished At', + artifact: 'Artifact', + error: 'Ошибка' + }, + status: { + queued: 'Queued', + running: 'Running', + succeeded: 'Succeeded', + failed: 'Ошибка', + partial_succeeded: 'Partial Succeeded' + } + }, + actions: { + refresh: 'Refresh Status', + disabledHint: 'Start datamanagementd first and ensure the socket is reachable.', + reloadConfig: 'Reload Config', + reloadSourceProfiles: 'Reload Source Profiles', + reloadProfiles: 'Reload Profiles', + newSourceProfile: 'New Source Profile', + saveConfig: 'Save Config', + configSaved: 'Configuration saved', + testS3: 'Test S3 Connection', + s3TestOK: 'S3 connection test succeeded', + s3TestFailed: 'S3 connection test failed', + newProfile: 'New Profile', + saveProfile: 'Save Profile', + activateProfile: 'Activate', + profileIDRequired: 'Profile ID is required', + profileNameRequired: 'Profile name is required', + profileSelectRequired: 'Select a profile to edit first', + profileCreated: 'S3 profile created', + profileSaved: 'S3 profile saved', + profileActivated: 'S3 profile activated', + profileDeleted: 'S3 profile deleted', + sourceProfileCreated: 'Source profile created', + sourceProfileSaved: 'Source profile saved', + sourceProfileActivated: 'Source profile activated', + sourceProfileDeleted: 'Source profile deleted', + createBackup: 'Create Backup Job', + jobCreated: 'Backup job created: {jobID} ({status})', + refreshJobs: 'Refresh Jobs', + loadMore: 'Load More' + } + }, + + affiliates: { + invitesDescription: 'View site-wide inviter and invitee relationships', + rebatesDescription: 'View recharge orders that generated affiliate rebates', + transfersDescription: 'View affiliate quota transfers into account balance', + errors: { + loadFailed: 'Failed to load affiliate records' + }, + records: { + search: 'Поиск', + searchPlaceholder: 'Email, username, user ID, or order number', + startAt: 'Start date', + endAt: 'End date', + inviter: 'Inviter', + invitee: 'Invitee', + user: 'Пользователь', + affCode: 'Invite Code', + order: 'Order', + totalRebate: 'Total Rebate', + orderAmount: 'Top-up Amount', + payAmount: 'Paid Amount', + rebateAmount: 'Rebate Amount', + paymentType: 'Способ оплаты', + orderStatus: 'Order Status', + transferAmount: 'Transfer Amount', + balanceAfter: 'Balance After', + availableQuotaAfter: 'Available After', + frozenQuotaAfter: 'Frozen After', + historyQuotaAfter: 'Historical Rebate After', + invitedAt: 'Invited At', + rebatedAt: 'Rebated At', + transferredAt: 'Transferred At' + }, + overview: { + title: 'Affiliate User Overview', + affCode: 'Invite Code', + rebateRate: 'Rebate Rate', + invitedCount: 'Invited Users', + rebatedInviteeCount: 'Rebated Invitees', + availableQuota: 'Available Quota', + historyQuota: 'Historical Rebate' + } + }, + + // Users + users: { + title: 'User Management', + description: 'Manage users and their permissions', + createUser: 'Create User', + editUser: 'Edit User', + deleteUser: 'Delete User', + searchUsers: 'Search by email, username, notes, or API key...', + allRoles: 'All Roles', + allStatus: 'Все статусы', + allGroups: 'Все группы', + searchGroups: 'Поиск групп...', + fuzzySearch: 'Fuzzy search', + admin: 'Admin', + user: 'Пользователь', + disabled: 'Отключено', + email: 'Email', + password: 'Пароль', + username: 'Имя пользователя', + notes: 'Notes', + enterEmail: 'Enter email', + enterPassword: 'Enter password', + enterUsername: 'Enter username (optional)', + enterNotes: 'Enter notes (admin only)', + notesHint: 'This note is only visible to administrators', + enterNewPassword: 'Enter new password (optional)', + leaveEmptyToKeep: 'Leave empty to keep current password', + generatePassword: 'Generate random password', + copyPassword: 'Copy password', + creating: 'Creating...', + updating: 'Updating...', + form: { + rpmLimit: 'Requests Per Minute (RPM)', + rpmLimitPlaceholder: '0 = unlimited', + rpmLimitHint: 'Max requests per minute for this user; 0 = unlimited. Acts as a fallback only when the group has no rpm_limit set.' + }, + columns: { + user: 'Пользователь', + id: 'ID', + email: 'Email', + username: 'Имя пользователя', + notes: 'Notes', + role: 'Роль', + groups: 'Группы', + subscriptions: 'Подписки', + balance: 'Баланс', + balancePlatformQuota: 'Balance (Platform Quota)', + usage: 'Расход', + usageAnthropic: 'Usage (Claude)', + usageOpenAI: 'Usage (OpenAI)', + usageGemini: 'Usage (Gemini)', + usageAntigravity: 'Usage (Antigravity)', + concurrency: 'Параллелизм', + status: 'Статус', + lastActive: 'Last Active', + lastUsed: 'Last Used', + created: 'Создан', + actions: 'Действия' + }, + today: 'Сегодня', + total: 'Last 30d', + sortBy: 'Sort By', + sortCurrentPageOnly: 'Sorts current page only', + noSubscription: 'No subscription', + publicGroupCount: '+{count} public', + exclusiveLabel: 'exclusive', + publicLabel: 'public', + daysRemaining: '{days}d', + expired: 'Истекла', + disable: 'Отключить', + enable: 'Включить', + disableUser: 'Disable User', + enableUser: 'Enable User', + viewApiKeys: 'View API Keys', + groups: 'Группы', + apiKeys: 'API-ключи', + userApiKeys: 'User API Keys', + noApiKeys: 'This user has no API keys', + group: 'Группа', + none: 'Нет', + groupChangedSuccess: 'Group updated successfully', + groupChangedWithGrant: 'Group updated. User auto-granted access to "{group}"', + groupChangeFailed: 'Failed to update group', + noUsersYet: 'No users yet', + createFirstUser: 'Create your first user to get started.', + userCreated: 'User created successfully', + userUpdated: 'User updated successfully', + userDeleted: 'User deleted successfully', + userEnabled: 'User enabled successfully', + userDisabled: 'User disabled successfully', + failedToLoad: 'Failed to load users', + failedToCreate: 'Failed to create user', + failedToUpdate: 'Failed to update user', + failedToDelete: 'Failed to delete user', + failedToToggle: 'Failed to update user status', + failedToLoadApiKeys: 'Failed to load user API keys', + emailRequired: 'Please enter email', + concurrencyMin: 'Concurrency must be at least 1', + soraStorageQuota: 'Sora Storage Quota', + soraStorageQuotaHint: 'In GB, 0 means use group or system default quota', + amountRequired: 'Please enter a valid amount', + insufficientBalance: 'Insufficient balance', + deleteConfirm: "Are you sure you want to delete '{email}'? This action cannot be undone.", + setAllowedGroups: 'Set Allowed Groups', + allowedGroupsHint: + 'Select which standard groups this user can use. Subscription groups are managed separately.', + noStandardGroups: 'No standard groups available', + allowAllGroups: 'Allow All Groups', + allowAllGroupsHint: 'User can use any non-exclusive group', + allowedGroupsUpdated: 'Allowed groups updated successfully', + failedToLoadGroups: 'Failed to load groups', + failedToUpdateAllowedGroups: 'Failed to update allowed groups', + // User Group Configuration + groupConfig: 'User Group Configuration', + groupConfigHint: 'Configure custom rate multipliers for user {email} (overrides group defaults)', + exclusiveGroups: 'Exclusive Groups', + publicGroups: 'Public Groups (Default Available)', + defaultRate: 'Default Rate', + customRate: 'Custom Rate', + useDefaultRate: 'Use Default', + customRatePlaceholder: 'Leave empty for default', + groupConfigUpdated: 'Group configuration updated successfully', + replaceGroup: 'Replace Group', + clickToReplace: 'Click to replace', + replaceGroupTitle: 'Replace Exclusive Group', + replaceGroupHint: 'Select a new group to replace "{old}". Keys will be migrated and permissions updated automatically.', + replaceGroupConfirm: 'Confirm Replace', + replaceGroupSuccess: 'Group replaced successfully, {count} key(s) migrated', + selectNewGroup: 'Select target group', + noOtherGroups: 'No other exclusive groups available', + deposit: 'Deposit', + withdraw: 'Withdraw', + depositAmount: 'Deposit Amount', + withdrawAmount: 'Withdraw Amount', + withdrawAll: 'Все', + currentBalance: 'Текущий баланс', + depositNotesPlaceholder: + 'e.g., New user registration bonus, promotional credit, compensation, etc.', + withdrawNotesPlaceholder: + 'e.g., Service issue refund, incorrect charge reversal, account closure refund, etc.', + notesOptional: 'Notes are optional but helpful for record keeping', + amountHint: 'Please enter a positive amount', + newBalance: 'Новый баланс', + depositing: 'Depositing...', + withdrawing: 'Withdrawing...', + confirmDeposit: 'Confirm Deposit', + confirmWithdraw: 'Confirm Withdraw', + depositSuccess: 'Deposit successful', + withdrawSuccess: 'Withdraw successful', + failedToDeposit: 'Failed to deposit', + failedToWithdraw: 'Failed to withdraw', + useDepositWithdrawButtons: 'Please use deposit/withdraw buttons to adjust balance', + // Balance History + balanceHistory: 'Recharge History', + balanceHistoryTip: 'Click to open recharge history', + columnAlwaysVisible: 'This column is always visible', + // Per-platform usage breakdown (hover tooltip) + platformBreakdown: 'Per-platform breakdown', + platformBreakdownEmpty: 'No platform usage yet', + platformBreakdownHint: 'Hover for per-platform usage', + platformOther: 'Other', + balanceHistoryTitle: 'User Recharge & Concurrency History', + noBalanceHistory: 'No records found for this user', + allTypes: 'All Types', + typeBalance: 'Balance (Redeem)', + typeAffiliateBalance: 'Balance (Affiliate Transfer)', + typeAdminBalance: 'Balance (Admin)', + typeConcurrency: 'Concurrency (Redeem)', + typeAdminConcurrency: 'Concurrency (Admin)', + typeSubscription: 'Subscription', + failedToLoadBalanceHistory: 'Failed to load balance history', + createdAt: 'Создан', + totalRecharged: 'Total Recharged', + roles: { + admin: 'Admin', + user: 'Пользователь' + }, + // Settings Dropdowns + filterSettings: 'Filter Settings', + columnSettings: 'Column Settings', + filterValue: 'Enter value', + // User Attributes + attributes: { + title: 'User Attributes', + description: 'Configure custom user attribute fields', + configButton: 'Attributes', + addAttribute: 'Add Attribute', + editAttribute: 'Edit Attribute', + deleteAttribute: 'Delete Attribute', + deleteConfirm: "Are you sure you want to delete attribute '{name}'? All user values for this attribute will be deleted.", + noAttributes: 'No custom attributes', + noAttributesHint: 'Click the button above to add custom attributes', + key: 'Attribute Key', + keyHint: 'For programmatic reference, only letters, numbers and underscores', + name: 'Display Name', + nameHint: 'Name shown in forms', + type: 'Attribute Type', + fieldDescription: 'Description', + fieldDescriptionHint: 'Description text for the attribute', + placeholder: 'Placeholder', + placeholderHint: 'Placeholder text for input field', + required: 'Required', + enabled: 'Включено', + options: 'Options', + optionsHint: 'For select/multi-select types', + addOption: 'Add Option', + optionValue: 'Option Value', + optionLabel: 'Display Text', + validation: 'Validation Rules', + minLength: 'Min Length', + maxLength: 'Max Length', + min: 'Min Value', + max: 'Max Value', + pattern: 'Regex Pattern', + patternMessage: 'Validation Error Message', + types: { + text: 'Text', + textarea: 'Textarea', + number: 'Number', + email: 'Email', + url: 'URL', + date: 'Дата', + select: 'Select', + multi_select: 'Multi-Select' + }, + created: 'Attribute created successfully', + updated: 'Attribute updated successfully', + deleted: 'Attribute deleted successfully', + reordered: 'Attribute order updated successfully', + failedToLoad: 'Failed to load attributes', + failedToCreate: 'Failed to create attribute', + failedToUpdate: 'Failed to update attribute', + keyRequired: 'Please enter attribute key', + nameRequired: 'Please enter display name', + optionsRequired: 'Please add at least one option', + failedToDelete: 'Failed to delete attribute', + failedToReorder: 'Failed to update order', + keyExists: 'Attribute key already exists', + dragToReorder: 'Drag to reorder' + }, + platformQuota: { + menuItem: 'Platform Quotas', + title: 'Platform Quotas', + subtitle: 'Configure daily / weekly / monthly USD usage limits for each upstream platform for user {email}', + columns: { + platform: 'Platform', + daily: 'Daily (USD)', + weekly: 'Weekly (USD)', + monthly: 'Monthly (USD, 30-day rolling)', + usage: 'Current Usage', + }, + placeholder: 'unlimited', + save: 'Сохранить', + saving: 'Сохранение...', + cancel: 'Отмена', + clearAll: 'Clear All (remove all limits)', + clearAllConfirm: 'Clear daily / weekly / monthly limits for ALL platforms? All platforms will become "unlimited" with no local undo — you must manually re-enter values before saving.', + reset: { + button: 'Reset window', + confirm: 'Reset the {window} usage for {platform} for this user? This is effective immediately.', + success: 'Reset {platform} {window} usage', + failed: 'Reset failed', + }, + updateSuccess: 'Platform quotas updated', + updateFailed: 'Save failed', + loadFailed: 'Load failed', + hint: 'Empty = no limit for that window.', + windowDaily: 'daily', + windowWeekly: 'weekly', + windowMonthly: 'monthly', + cellNotConfigured: 'Not configured', + cellColumnTooltip: 'Only platforms with a limit are shown', + subscriptionWarning: 'This user has an active subscription. Platform quotas only apply to balance (standard) mode requests; subscription mode requests are not subject to these limits.', + invalidNumber: 'The following fields contain invalid numbers. Please fix them before saving: {fields}', + } + }, + + // Groups + groups: { + title: 'Group Management', + description: 'Manage API key groups and rate multipliers', + searchGroups: 'Поиск групп...', + createGroup: 'Create Group', + editGroup: 'Edit Group', + deleteGroup: 'Delete Group', + sortOrder: 'Sort', + sortOrderHint: 'Drag groups to adjust display order, groups at the top will be displayed first', + sortOrderUpdated: 'Sort order updated', + failedToUpdateSortOrder: 'Failed to update sort order', + allPlatforms: 'All Platforms', + allStatus: 'Все статусы', + allGroups: 'Все группы', + exclusive: 'Exclusive', + nonExclusive: 'Non-Exclusive', + public: 'Public', + columns: { + name: 'Имя', + platform: 'Platform', + rateMultiplier: 'Rate Multiplier', + rpmOverride: 'RPM Override', + rpmOverrideHint: 'Per-user RPM cap in this group; empty = group default; 0 = unlimited', + rateDefault: 'default', + rpmDefault: 'default', + type: 'Type', + accounts: 'Аккаунты', + capacity: 'Capacity', + usage: 'Расход', + status: 'Статус', + actions: 'Действия', + billingType: 'Billing Type', + userName: 'Имя пользователя', + userEmail: 'Email', + userNotes: 'Notes', + userStatus: 'Статус' + }, + usageToday: 'Сегодня', + usageTotal: 'Итого', + accountsAvailable: 'Avail:', + accountsRateLimited: 'Limited:', + accountsTotal: 'Total:', + accountsUnit: '', + rateAndAccounts: '{rate}x rate · {count} accounts', + accountsCount: '{count} accounts', + form: { + name: 'Имя', + description: 'Description', + platform: 'Platform', + rateMultiplier: 'Rate Multiplier', + status: 'Статус', + exclusive: 'Exclusive Group', + rpmLimit: 'Requests Per Minute (RPM)', + rpmLimitPlaceholder: '0 = unlimited', + rpmLimitHint: 'Max requests per minute for each user in this group; 0 = unlimited. Once set, it takes over per-user rate limiting in this group (overrides the user-level rpm_limit fallback).' + }, + enterGroupName: 'Enter group name', + optionalDescription: 'Optional description', + platformHint: 'Select the platform this group is associated with', + platformNotEditable: 'Platform cannot be changed after creation', + rateMultiplierHint: 'Cost multiplier for this group (e.g., 1.5 = 150% of base cost)', + exclusiveHint: 'Exclusive group, manually assign to specific users', + exclusiveTooltip: { + title: 'What is an exclusive group?', + description: 'When enabled, users cannot see this group when creating API Keys. Only after an admin manually assigns a user to this group can they use it.', + example: 'Use case:', + exampleContent: 'Public group rate is 0.8. Create an exclusive group with 0.7 rate, manually assign VIP users to give them better pricing.' + }, + noGroupsYet: 'No groups yet', + createFirstGroup: 'Create your first group to organize API keys.', + creating: 'Creating...', + updating: 'Updating...', + limitDay: 'd', + limitWeek: 'w', + limitMonth: 'mo', + groupCreated: 'Group created successfully', + groupUpdated: 'Group updated successfully', + groupDeleted: 'Group deleted successfully', + failedToLoad: 'Failed to load groups', + failedToCreate: 'Failed to create group', + failedToUpdate: 'Failed to update group', + failedToDelete: 'Failed to delete group', + nameRequired: 'Please enter group name', + rateMultipliers: 'Rate Multipliers', + rateMultipliersTitle: 'Group Rate Multipliers', + addUserRate: 'Add User Rate Multiplier', + rpmOverrides: 'RPM Overrides', + rpmOverridesTitle: 'Group RPM Overrides', + addUserRpm: 'Add User RPM Override', + noRpmOverrides: 'No users have an RPM override yet', + rpmSaved: 'RPM overrides saved', + groupRpmDefault: 'Group default RPM', + searchUserPlaceholder: 'Search user email...', + noRateMultipliers: 'No user rate multipliers configured', + rateUpdated: 'Rate multiplier updated', + rateDeleted: 'Rate multiplier removed', + rateAdded: 'Rate multiplier added', + clearAll: 'Clear All', + confirmClearAll: 'Are you sure you want to clear all rate multiplier settings for this group? This cannot be undone.', + rateCleared: 'All rate multipliers cleared', + batchAdjust: 'Batch Adjust Rates', + multiplierFactor: 'Factor', + applyMultiplier: 'Применить', + rateAdjusted: 'Rates adjusted successfully', + rateSaved: 'Rate multipliers saved', + finalRate: 'Final Rate', + unsavedChanges: 'Unsaved changes', + revertChanges: 'Revert', + userInfo: 'User Info', + platforms: { + all: 'All Platforms', + anthropic: 'Anthropic', + openai: 'OpenAI', + gemini: 'Gemini', + antigravity: 'Antigravity', + }, + deleteConfirm: + "Are you sure you want to delete '{name}'? All associated API keys will no longer belong to any group.", + deleteConfirmSubscription: + "Are you sure you want to delete subscription group '{name}'? This will invalidate all API keys bound to this subscription and delete all related subscription records. This action cannot be undone.", + subscription: { + title: 'Subscription Settings', + type: 'Billing Type', + typeHint: + 'Standard billing deducts from user balance. Subscription mode uses quota limits instead.', + typeNotEditable: 'Billing type cannot be changed after group creation.', + standard: 'Standard (Balance)', + subscription: 'Subscription (Quota)', + dailyLimit: 'Daily Limit (USD)', + weeklyLimit: 'Weekly Limit (USD)', + monthlyLimit: 'Monthly Limit (USD)', + defaultValidityDays: 'Default Validity (Days)', + validityHint: 'Number of days the subscription is valid when assigned to a user', + noLimit: 'No limit' + }, + imagePricing: { + title: 'Image Generation Pricing', + description: 'Configure image generation access and base image prices. Leave empty to use default prices.', + allowImageGeneration: 'Allow image generation for this group', + independentMultiplier: 'Use independent image multiplier', + imageMultiplier: 'Image multiplier', + modeHint: 'By default, image billing uses image price × current effective group multiplier. Independent mode uses image price × image multiplier.', + finalPricePreview: 'Final per-image price preview', + notConfigured: 'Not configured' + }, + modelsList: { + title: 'Custom /v1/models Model List', + hint: 'Only changes the /v1/models response. Whitelist model calls and account routing are unchanged.', + loading: 'Loading model list...', + empty: 'No displayable models' + }, + claudeCode: { + title: 'Claude Code Client Restriction', + tooltip: 'When enabled, this group only allows official Claude Code clients. Non-Claude Code requests will be rejected or fallback to the specified group.', + enabled: 'Claude Code Only', + disabled: 'Allow All Clients', + fallbackGroup: 'Fallback Group', + fallbackHint: 'Non-Claude Code requests will use this group. Leave empty to reject directly.', + noFallback: 'No Fallback (Reject)' + }, + openaiMessages: { + title: 'OpenAI Messages Dispatch', + allowDispatch: 'Allow /v1/messages dispatch', + allowDispatchHint: 'When enabled, API keys in this OpenAI group can dispatch requests through /v1/messages endpoint', + familyMappingTitle: 'Family Default Mapping', + familyMappingHint: 'Requests that match the Opus, Sonnet, or Haiku families will prefer the target model configured here.', + opusModel: 'Opus Target Model', + opusModelPlaceholder: 'e.g., gpt-5.4', + sonnetModel: 'Sonnet Target Model', + sonnetModelPlaceholder: 'e.g., gpt-5.3-codex', + haikuModel: 'Haiku Target Model', + haikuModelPlaceholder: 'e.g., gpt-5.4-mini', + exactMappingTitle: 'Exact Model Overrides', + exactMappingHint: 'Exact Claude model overrides take priority over the family defaults and can route a specific Claude model to a different target model.', + noExactMappings: 'No exact model overrides yet', + addExactMapping: 'Add Exact Mapping', + claudeModel: 'Claude Model', + claudeModelPlaceholder: 'e.g., claude-sonnet-4-5-20250929', + targetModel: 'Target Model', + targetModelPlaceholder: 'e.g., gpt-5.4', + removeExactMapping: 'Remove Exact Mapping' + }, + invalidRequestFallback: { + title: 'Invalid Request Fallback Group', + hint: 'Triggered only when upstream explicitly returns prompt too long. Leave empty to disable fallback.', + noFallback: 'No Fallback' + }, + copyAccounts: { + title: 'Copy Accounts from Groups', + tooltip: 'Select one or more groups of the same platform. After creation, all accounts from these groups will be automatically bound to the new group (deduplicated).', + tooltipEdit: 'Select one or more groups of the same platform. After saving, current group accounts will be replaced with accounts from these groups (deduplicated).', + selectPlaceholder: 'Select groups to copy accounts from...', + hint: 'Multiple groups can be selected, accounts will be deduplicated', + hintEdit: '⚠️ Warning: This will replace all existing account bindings' + }, + modelRouting: { + title: 'Model Routing', + tooltip: 'Configure specific model requests to be routed to designated accounts. Supports wildcard matching, e.g., claude-opus-* matches all opus models.', + enabled: 'Включено', + disabled: 'Отключено', + disabledHint: 'Routing rules will only take effect when enabled', + addRule: 'Add Routing Rule', + modelPattern: 'Model Pattern', + modelPatternPlaceholder: 'claude-opus-*', + modelPatternHint: 'Supports * wildcard, e.g., claude-opus-* matches all opus models', + accounts: 'Priority Accounts', + selectAccounts: 'Select accounts', + noAccounts: 'No accounts in this group', + loadingAccounts: 'Loading accounts...', + removeRule: 'Remove Rule', + noRules: 'No routing rules', + noRulesHint: 'Add routing rules to route specific model requests to designated accounts', + searchAccountPlaceholder: 'Search accounts...', + accountsHint: 'Select accounts to prioritize for this model pattern' + }, + mcpXml: { + title: 'MCP XML Protocol Injection', + tooltip: 'When enabled, if the request contains MCP tools, an XML format call protocol prompt will be injected into the system prompt. Disable this to avoid interference with certain clients.', + enabled: 'Включено', + disabled: 'Отключено' + }, + claudeMaxSimulation: { + title: 'Claude Max Usage Simulation', + tooltip: + 'When enabled, for Claude models without upstream cache-write usage, the system deterministically maps tokens to a small input plus 1h cache creation while keeping total tokens unchanged.', + enabled: 'Enabled (simulate 1h cache)', + disabled: 'Отключено', + hint: 'Only token categories in usage billing logs are adjusted. No per-request mapping state is persisted.' + }, + supportedScopes: { + title: 'Supported Model Families', + tooltip: 'Select the model families this group supports. Unchecked families will not be routed to this group.', + claude: 'Claude', + geminiText: 'Gemini Text', + geminiImage: 'Gemini Image', + hint: 'Select at least one model family' + } + }, + + // Available Channels (aggregated read-only view) + availableChannels: { + title: 'Доступные каналы', + description: 'Aggregated view: each channel with its linked groups and supported models (wildcards expanded)', + searchPlaceholder: 'Search channels or models...', + columns: { + name: 'Channel', + status: 'Статус', + billingSource: 'Billing Model Source', + groups: 'Linked Groups', + supportedModels: 'Supported Models' + }, + empty: 'Нет данных', + noGroups: 'No linked groups', + noModels: 'No model mapping configured', + noPricing: 'Pricing not configured', + statusActive: 'Активен', + statusDisabled: 'Отключено', + billingSource: { + requested: 'Requested model', + upstream: 'Upstream model', + channel_mapped: 'Channel-mapped model' + }, + pricing: { + billingMode: 'Billing Mode', + billingModeToken: 'Per Token', + billingModePerRequest: 'Per Request', + billingModeImage: 'Per Image', + inputPrice: 'Input', + outputPrice: 'Output', + cacheWritePrice: 'Cache Write', + cacheReadPrice: 'Cache Read', + imageOutputPrice: 'Image Output', + perRequestPrice: 'Per Request', + intervals: 'Tiered Pricing', + unitPerMillion: '/ 1M tokens', + unitPerRequest: '/ request' + } + }, + + // Channel Management + channels: { + title: 'Channel Management', + description: 'Manage channels and custom model pricing', + searchChannels: 'Search channels...', + createChannel: 'Create Channel', + editChannel: 'Edit Channel', + deleteChannel: 'Delete Channel', + statusActive: 'Активен', + statusDisabled: 'Отключено', + allStatus: 'Все статусы', + groupsUnit: 'groups', + pricingUnit: 'pricing rules', + noChannelsYet: 'No Channels Yet', + createFirstChannel: 'Create your first channel to manage model pricing', + loadError: 'Failed to load channels', + createSuccess: 'Channel created', + updateSuccess: 'Channel updated', + deleteSuccess: 'Channel deleted', + createError: 'Failed to create channel', + updateError: 'Failed to update channel', + deleteError: 'Failed to delete channel', + nameRequired: 'Please enter a channel name', + duplicateModels: 'Model "{0}" appears in multiple pricing entries', + modelConflict: "Model patterns '{model1}' and '{model2}' conflict: overlapping match range", + mappingConflict: "Mapping source patterns '{model1}' and '{model2}' conflict: overlapping match range", + deleteConfirm: 'Are you sure you want to delete channel "{name}"? This cannot be undone.', + columns: { + name: 'Имя', + description: 'Description', + status: 'Статус', + groups: 'Группы', + pricing: 'Оплата', + createdAt: 'Создан', + actions: 'Действия' + }, + billingMode: { + token: 'Token', + perRequest: 'Per Request', + image: 'Image (Per Request)' + }, + form: { + name: 'Имя', + namePlaceholder: 'Enter channel name', + description: 'Description', + descriptionPlaceholder: 'Optional description', + status: 'Статус', + groups: 'Associated Groups', + noGroupsAvailable: 'Нет доступных групп', + inOtherChannel: 'In "{name}"', + modelPricing: 'Model Pricing', + models: 'Модели', + modelsPlaceholder: 'Type full model name and press Enter', + modelInputHint: 'Press Enter to add, supports paste for batch import.', + billingMode: 'Billing Mode', + defaultPrices: 'Default prices (fallback when no interval matches)', + inputPrice: 'Input', + outputPrice: 'Output', + cacheWritePrice: 'Cache Write', + cacheReadPrice: 'Cache Read', + imageTokenPrice: 'Image Output', + imageOutputPrice: 'Image Output Price', + pricePlaceholder: 'По умолчанию', + intervals: 'Context Intervals (optional)', + addInterval: 'Add Interval', + requestTiers: 'Request Tiers', + imageTiers: 'Image Tiers (Per Request)', + addTier: 'Add Tier', + noTiersYet: 'No tiers yet. Click add to configure per-request pricing.', + noPricingRules: 'No pricing rules yet. Click "Добавить" to create one.', + perRequestPrice: 'Price per Request', + perRequestPriceRequired: 'Per-request price or billing tiers required for per-request/image billing mode', + tierLabel: 'Tier', + resolution: 'Resolution', + modelMapping: 'Model Mapping', + modelMappingHint: 'Map request model names to actual model names. Runs before account-level mapping.', + noMappingRules: 'No mapping rules. Click "Добавить" to create one.', + mappingSource: 'Source model', + mappingTarget: 'Target model', + billingModelSource: 'Billing Model', + billingModelSourceChannelMapped: 'Bill by channel-mapped model', + billingModelSourceRequested: 'Bill by requested model', + billingModelSourceUpstream: 'Bill by final upstream model', + billingModelSourceHint: 'Controls which model name is used for pricing lookup', + selectedCount: '{count} selected', + searchGroups: 'Поиск групп...', + noGroupsMatch: 'No groups match your search', + restrictModels: 'Restrict Models', + restrictModelsHint: 'When enabled, only models in the pricing list are allowed. Others will be rejected.', + defaultPerRequestPrice: 'Default per-request price (fallback when no tier matches)', + defaultImagePrice: 'Default image price (fallback when no tier matches)', + platformConfig: 'Platform Configuration', + webSearchEmulation: 'Web Search Emulation', + webSearchEmulationHint: '⚠️ When enabled, all accounts in this channel\'s Anthropic groups will intercept web_search requests. Use with caution.', + webSearchEmulationGlobalDisabled: 'Please enable the global switch first in Settings → Gateway → Web Search Emulation', + codexImageGenerationBridge: 'Codex Image Generation Bridge', + codexImageGenerationBridgeHint: 'When enabled, Codex /responses text requests in OpenAI groups may be automatically given the image_generation tool. Keep off unless the routed accounts support image generation.', + bedrockCCCompat: 'Bedrock CC Compatibility', + bedrockCCCompatHint: '⚠️ When enabled, requests to Bedrock accounts in this channel will be transformed for Claude Code compatibility (thinking type conversion, tool_use ID sanitization).', + basicSettings: 'Basic Settings', + addPlatform: 'Add Platform', + noPlatforms: 'Click "Add Platform" to start configuring the channel', + mappingCount: 'mappings', + pricingEntry: 'Pricing Entry', + noModels: 'No models added', + applyPricingToAccountStats: 'Apply Pricing to Account Stats', + applyPricingToAccountStatsDesc: 'When enabled, requests not matched by custom rules will use standard model pricing for account stats calculation', + accountStatsPricingRules: 'Custom Account Stats Pricing Rules', + addRule: 'Add Rule', + noRulesConfigured: 'No custom rules configured. Channel model pricing above will be used.', + ruleName: 'Rule name (optional)', + ruleGroups: 'Группы', + ruleAccounts: 'Аккаунты', + searchAccountPlaceholder: 'Search accounts...', + ruleAccountsHint: 'Leave empty to match all accounts', + ruleModelPricing: 'Model Pricing', + noGroupsInChannel: 'No groups selected in platform tabs above', + unnamed: 'Unnamed', + syncLatestModels: 'Sync Latest Models', + syncingModels: 'Syncing...', + syncModelsSuccess: 'Synced {count} new model(s)', + syncModelsAlreadyUpToDate: 'Models already up to date', + syncModelsError: 'Failed to sync models' + } + }, + + riskControl: { + title: 'Риск-контроль', + description: 'Configure content moderation and review audit records', + loadFailed: 'Failed to load risk control', + saveFailed: 'Failed to save content moderation config', + logsFailed: 'Failed to load audit records', + saved: 'Content moderation config saved', + refresh: 'Обновить', + config: 'Content Moderation Config', + configHint: 'Use OpenAI Moderations to score request content and handle threshold hits by mode.', + openSettings: 'Moderation Settings', + settingsTitle: 'Content Moderation Settings', + refreshStatus: 'Refresh Status', + records: 'Audit Records', + recordsHint: 'Shows hits, blocks, errors, and sampled records.', + saveConfig: 'Save Moderation Config', + statusFailed: 'Failed to load runtime status', + enabled: 'Enable Content Moderation', + enabledHint: 'When off, gateway requests are not moderated even if the menu is enabled.', + mode: 'Global Mode', + modePreBlock: 'Pre-Block', + modePreBlockDesc: 'Synchronously reviews the latest user input before every request and rejects hits immediately.', + modeObserve: 'Observe Only', + modeObserveDesc: 'Requests pass through while the latest user input is queued for async review; hits are recorded, notified, and counted.', + modeOff: 'Off', + modeOffDesc: 'Content moderation is disabled and no audit records are written.', + baseUrl: 'OpenAI Base URL', + model: 'Модель', + apiKey: 'OpenAI API Key', + apiKeys: 'OpenAI API Keys', + apiKeyCount: '{count} keys', + apiKeyPlaceholder: 'Enter API Key', + apiKeysPlaceholder: 'Add API Keys, one per line. They will be appended on save.', + apiKeysPlaceholderReplace: 'Replace API Keys, one per line. Stored keys will be replaced on save.', + apiKeysPlaceholderKeep: 'Add API Keys, one per line. They will be appended on save.', + apiKeysHint: '{count} keys are currently stored. This input only adds keys; save appends and de-duplicates them.', + apiKeysWriteMode: 'Write mode', + apiKeysModeAppend: 'Добавить', + apiKeysModeReplace: 'Replace', + apiKeysModeAppendHint: 'Default: save appends input keys and keeps stored keys.', + apiKeysModeReplaceHint: 'Replace mode: save replaces all stored keys with input keys.', + apiKeysReplaceWarning: 'Replace mode', + apiKeysReplaceNoInput: 'Replace mode requires at least 1 API Key', + apiKeyPlaceholderKeep: 'Leave empty to keep current key', + apiKeyWillClear: 'Configured key will be cleared on save', + apiKeyConfigured: 'Configured', + apiKeyTemporary: 'Ожидает', + apiKeyPendingDelete: 'Pending delete', + apiKeyPendingDeleteCount: '{count} keys pending deletion', + deleteApiKey: 'Delete this key', + undoDeleteApiKey: 'Undo delete', + inputApiKeyCount: '{count} keys in input', + storedApiKeyCount: '{count} stored keys', + testInputApiKeys: 'Test input keys', + testStoredApiKeys: 'Test stored keys', + testContentWithStoredApiKey: 'Test content with stored key', + testingApiKeys: 'Testing', + apiKeyTestNoInput: 'Enter OpenAI API Keys to test first', + apiKeyTestDone: 'Key test completed for {count} keys', + apiKeyTestFailed: 'Failed to test OpenAI API Keys', + apiKeyHealth: 'Key Availability', + apiKeyFreezeRule: '400 does not freeze; 401/403 freeze for 10 minutes; 429/529 freeze for 1 minute; other HTTP errors freeze for 10 seconds.', + apiKeyRows: '{count} keys', + apiKeyRowsCollapsed: '{count} keys hidden', + apiKeyRowsExpanded: 'Showing all {count} keys', + expandApiKeyRows: 'Развернуть', + collapseApiKeyRows: 'Свернуть', + apiKeyHealthEmpty: 'No key status yet', + apiKeyHealthEmptyHint: 'Save keys or test input keys to see availability.', + apiKeyStatusOk: 'Доступно', + apiKeyStatusError: 'Ошибка', + apiKeyStatusFrozen: 'Frozen', + apiKeyStatusUnknown: 'Untested', + apiKeyFailureCount: '{count} failures', + apiKeyLatency: '{ms} ms', + apiKeyHTTPStatus: 'HTTP {status}', + apiKeyFrozenUntil: 'Frozen until {time}', + apiKeyLastChecked: 'Checked at {time}', + apiKeyNotTested: 'Not tested', + auditTestInput: 'Audit Test Input', + auditTestInputHint: 'Enter a prompt and upload or paste images; images are sent as base64 and are not stored.', + auditTestPromptPlaceholder: 'Enter a user prompt to test; leave empty to only test key availability.', + auditTestImages: 'Test Images', + auditTestImagesHint: 'Upload, drag, or paste images. Up to 1 image, 8MB each.', + addAuditTestImage: 'Add image', + clearAuditTest: 'Clear test', + auditTestImageLimit: 'You can add up to {count} test images', + auditTestImageTooLarge: 'Each test image must be 8MB or smaller', + auditTestImageReadFailed: 'Failed to read test image', + auditTestResult: 'Audit Test Result', + auditTestHighest: 'Top category {category}, score {score}', + auditTestComposite: 'Composite score', + auditTestFlagged: 'Threshold hit', + auditTestPassed: 'Pass', + notConfigured: 'Not configured', + clearApiKey: 'Clear stored key', + keepApiKey: 'Keep stored key', + timeoutMs: 'HTTP Timeout (ms)', + retryCount: 'Retry Count', + sampleRate: 'Sample Rate', + recordNonHits: 'Record Non-Hits', + recordNonHitsHint: 'When enabled, sampled non-hit request summaries are redacted before storage.', + preHashCheck: 'Enable Pre-Hash Check', + preHashCheckHint: 'Hashes from async hits are blocked before moderation; this does not send email or increment ban counters.', + flaggedHashCount: 'Current hash collection size: {count}', + flaggedHashHint: 'Hashes are stored permanently in Redis; paste a full 64-character hash to remove a false block, or clear all stored hashes.', + flaggedHashPlaceholder: 'Paste full 64-character input hash', + deleteFlaggedHash: 'Delete hash', + clearFlaggedHashes: 'Clear all', + clearFlaggedHashesConfirm: 'Clear all risk input hashes? This does not delete audit records, but removes all historical hash blocks.', + flaggedHashDeleted: 'Risk hash deleted', + flaggedHashNotFound: 'Risk hash not found', + flaggedHashDeleteFailed: 'Failed to delete risk hash', + flaggedHashesCleared: 'Cleared {count} risk hashes', + flaggedHashesClearFailed: 'Failed to clear risk hashes', + workerCount: 'Worker Count', + queueSize: 'Async Queue Size', + blockStatus: 'Block HTTP Status', + blockMessage: 'Custom Block Message', + emailOnHit: 'Email on Hit', + emailOnHitHint: 'When enabled, send a risk-control email on every hit; auto-ban notices are always sent.', + autoBan: 'Auto Ban User', + autoBanHint: 'Disable the user, invalidate auth cache, and send a ban notice after the hit threshold is reached.', + banThreshold: 'Ban Threshold', + violationWindowHours: 'Count Window (hours)', + hitRetentionDays: 'Hit Record Retention (days)', + nonHitRetentionDays: 'Non-Hit Record Retention (days, max 3)', + violationCount: '{count} hits', + emailSent: 'Email sent', + emailNotSent: 'No email', + autoBanned: 'Banned', + unbanUser: 'Unban', + unbanSuccess: 'User has been unbanned', + unbanFailed: 'Failed to unban user', + inputDetailTitle: 'Input Summary Detail', + inputDetailContent: 'Full Content', + queueDelay: 'Queued {ms} ms', + allGroups: 'Все группы', + allGroupsHint: 'Auditing all groups', + selectedGroupsHint: 'Auditing selected groups', + groupScope: 'Audit Groups', + groupScopeHint: 'Switch on for all groups, or turn off to choose specific groups.', + selectedGroups: 'Selected Groups', + searchGroups: 'Search group name or platform', + noGroups: 'Нет доступных групп', + modelFilter: 'Model scope', + modelFilterHint: 'Moderate by the client-requested model name; channel model mappings do not change this match.', + modelFilterAll: 'All models', + modelFilterAllDesc: 'All model requests go through content moderation.', + modelFilterInclude: 'Only selected', + modelFilterIncludeDesc: 'Only listed models go through content moderation.', + modelFilterExclude: 'Exclude selected', + modelFilterExcludeDesc: 'Listed models skip content moderation; other models are moderated.', + modelFilterModels: 'Model list', + modelFilterModelCount: '{count} models configured', + modelFilterModelsRequired: 'This model scope requires at least 1 model', + modelFilterAllSummary: 'Applies to all models', + modelFilterIncludeSummary: 'Applies to {count} models', + modelFilterExcludeSummary: 'Excludes {count} models', + emptyLogs: 'No audit records', + workerStatus: 'Worker Runtime', + workerStatusHint: 'Queue and worker pool status for asynchronous observation tasks.', + workerPool: 'Worker Pool', + workerPoolMeta: '{active} processing, {idle} idle and ready, {total} total', + queueUsage: 'Queue Usage', + activeWorkers: 'Processing', + idleWorkers: 'Idle Ready', + workerActive: 'Processing an asynchronous audit task', + workerIdle: 'Started, idle and ready', + workerDisabled: 'Risk control or content audit is disabled', + processed: 'Processed', + droppedErrors: 'Dropped / Errors', + autoRefresh: 'Auto refresh every 15s', + lastCleanup: 'Last cleanup: {time}', + cleanupStats: 'Last cleanup deleted {hit} hits and {nonHit} non-hits', + riskSwitchOff: 'System switch off', + riskThresholds: 'Risk Thresholds', + riskThresholdsHint: 'Adjust hit thresholds by OpenAI Moderations category. Scores greater than or equal to the threshold count as hits.', + riskThresholdDefault: 'Default {value}', + riskThresholdReset: 'Restore defaults', + riskThresholdPercent: 'Threshold percentage', + tabs: { + basic: 'Basic', + scope: 'Scope', + runtime: 'Runtime', + response: 'Hit Notice', + riskThresholds: 'Risk Thresholds', + keywords: 'Keyword Block', + retention: 'Retention', + }, + blockedKeywords: 'Blocked keywords', + blockedKeywordsPlaceholder: 'One keyword per line, e.g.:\nbadword1\nbadword2', + blockedKeywordsDescription: 'Matching is case-insensitive. Whether the upstream moderation API is invoked after a hit depends on the strategy below.', + blockedKeywordsPreBlockHint: 'Keyword blocking only takes effect in "Pre-block" mode.', + blockedKeywordsModeWarning: 'Current mode is "{mode}". Keyword blocking will not run until you switch to "Pre-block" mode.', + blockedKeywordCount: '{count} keywords configured', + blockedKeywordsLimit: 'Up to {max} keywords, each no longer than 200 characters. Duplicates are removed automatically.', + keywordBlockingMode: 'Moderation strategy', + keywordModeKeywordAndApi: 'Keyword + API', + keywordModeKeywordAndApiDesc: 'Block on keyword hit; otherwise fall through to the upstream moderation API.', + keywordModeKeywordOnly: 'Keyword only', + keywordModeKeywordOnlyDesc: 'Decide using keywords only; misses are allowed without calling the API, saving upstream cost.', + keywordModeKeywordOnlyNotice: 'Keyword-only strategy: requests that do not match any keyword are allowed without calling the upstream moderation API.', + keywordModeApiOnly: 'API only', + keywordModeApiOnlyDesc: 'Use the upstream moderation API only; the keyword list configured here is not consulted.', + keywordModeApiOnlyNotice: 'API-only strategy: the keyword list is not consulted; all requests go through the upstream moderation API.', + overview: { + status: 'Статус', + enabled: 'Включено', + disabled: 'Отключено', + apiKey: 'API-ключ', + groupScope: 'Scope', + logs: 'Audit Records', + currentFilter: 'Current filter', + }, + filters: { + search: 'Search user/key/summary', + from: 'From', + to: 'To', + allGroups: 'Все группы', + allEndpoints: 'All Endpoints', + }, + table: { + time: 'Time', + group: 'Группа', + user: 'Пользователь', + apiKey: 'API-ключ', + endpoint: 'Endpoint', + result: 'Result', + highest: 'Highest', + actionMeta: 'Action', + latency: 'Latency', + input: 'Input Summary', + }, + result: { + all: 'All Results', + hit: 'Hit', + blocked: 'Blocked', + pass: 'Pass', + error: 'Ошибка', + }, + action: { + block: 'Blocked', + keywordBlock: 'Keyword Blocked', + error: 'Ошибка', + }, + }, + + // Channel Monitor + channelMonitor: { + title: 'Монитор каналов', + description: 'Monitor channel availability, latency and status', + searchPlaceholder: 'Search monitor name...', + allProviders: 'All Providers', + allStatus: 'Все статусы', + enabledFilter: 'Включено', + onlyEnabled: 'Enabled only', + onlyDisabled: 'Disabled only', + createButton: 'Create Monitor', + createTitle: 'Create Channel Monitor', + editTitle: 'Edit Channel Monitor', + runNow: 'Run Now', + runSuccess: 'Check completed', + runFailed: 'Check failed', + apiKeyDecryptFailed: 'API Key decryption failed. Please re-edit this monitor with a fresh key.', + createSuccess: 'Monitor created', + updateSuccess: 'Monitor updated', + deleteSuccess: 'Monitor deleted', + loadError: 'Failed to load monitors', + deleteConfirm: 'Are you sure you want to delete monitor "{name}"? This action cannot be undone.', + nameRequired: 'Please enter a monitor name', + primaryModelRequired: 'Please enter a primary model', + columns: { + name: 'Имя', + provider: 'Provider', + primaryModel: 'Primary Model', + availability7d: '7d Availability', + latency: 'Latency (ms)', + enabled: 'Включено', + actions: 'Действия' + }, + form: { + name: 'Имя', + namePlaceholder: 'Enter monitor name', + provider: 'Platform', + apiMode: 'OpenAI protocol', + apiModeChatCompletions: 'OpenAI Compatible', + apiModeChatCompletionsHint: 'Use /v1/chat/completions with messages; works for most compatible providers.', + apiModeResponses: 'Responses API', + apiModeResponsesHint: 'Use /v1/responses with default instructions + input; best for self-check/Codex paths.', + endpoint: 'Endpoint', + endpointPlaceholder: 'https://api.example.com', + useCurrentDomain: 'Use current service', + apiKey: 'API-ключ', + apiKeyPlaceholder: 'Enter API Key', + apiKeyEditPlaceholder: 'Leave blank to keep current key', + useMyKey: 'Use my key', + selectKeyTitle: 'Select my API Key', + selectKeyHint: 'Only your active, non-expired keys are listed.', + noActiveKey: 'No active API keys available', + primaryModel: 'Primary Model', + primaryModelPlaceholder: 'gpt-4o-mini', + extraModels: 'Extra Models', + extraModelsPlaceholder: 'Press Enter to add extra model', + groupName: 'Group Name', + groupNamePlaceholder: 'Optional, used to group rows in user view', + intervalSeconds: 'Interval (seconds)', + intervalSecondsHint: 'Range: 15 - 3600 seconds', + enabled: 'Enable monitor', + kindRequired: 'Please select a provider' + }, + runResultTitle: 'Check Result', + noMonitorsYet: 'No monitors yet', + createFirstMonitor: 'Create your first monitor to track channel availability', + advanced: { + section: 'Advanced (optional)', + sectionHint: 'Customize request headers and body to bypass upstream client-detection (e.g. "only Claude Code clients allowed").', + headers: 'Custom request headers', + headersPlaceholder: 'User-Agent: claude-cli/1.0.83 (external, cli)\nx-app: cli\nanthropic-beta: claude-code-20250219', + headerNamePlaceholder: 'Header name', + headerValuePlaceholder: 'Value', + headerAddRow: 'Add header', + headerNameInvalid: 'Header name cannot contain whitespace or colon: {name}', + headersHint: 'Merged on top of adapter defaults (user wins). Hop-by-hop headers (Host / Content-Length / ...) are ignored.', + headersParseError: 'Cannot parse line: {line}', + bodyMode: 'Body handling', + bodyModeOff: 'По умолчанию', + bodyModeMerge: 'Merge', + bodyModeReplace: 'Replace', + bodyModeHintOff: 'Use the adapter default body (includes challenge validation).', + bodyModeHintMerge: 'Shallow-merge with the default body; user fields win but model / messages / contents are protected (use Replace to change those).', + bodyModeHintReplace: 'Use the JSON below as the complete body. Challenge validation is skipped; HTTP 2xx + non-empty response text is treated as operational.', + bodyJson: 'Body JSON', + bodyJsonFormat: 'Format', + bodyJsonHint: 'Parsed on blur. Empty means no override.', + bodyJsonError: 'JSON parse failed', + bodyJsonObjectError: 'Body must be a JSON object (no arrays or primitives)' + }, + templateField: { + label: 'Request template', + none: 'No template', + placeholder: 'Pick a template (filtered by current provider)', + applyHint: 'Picking a template copies its headers and body to this monitor (snapshot). Later template edits are not auto-synced.' + }, + template: { + manageButton: 'Templates', + managerTitle: 'Request template manager', + createButton: 'New template', + emptyState: 'No templates for this provider yet', + missingName: 'Template name is required', + createSuccess: 'Template created', + updateSuccess: 'Template updated', + deleteSuccess: 'Template deleted', + applyButton: 'Apply to monitors', + applyTooltip: 'Overwrite snapshot fields on associated monitors', + applyTitle: 'Apply template', + applyConfirm: 'Применить', + applyConfirmMessage: 'Overwrite {n} associated monitor(s) with the current configuration of "{name}"? Any local customizations on those monitors will be discarded.', + applySuccess: 'Applied to {n} monitor(s)', + applyPickerTitle: 'Apply template "{name}"', + applyPickerHint: 'Select which monitors to overwrite (all selected by default). Any local customizations will be discarded.', + applyPickerEmpty: 'No monitors are currently associated to this template', + applyPickerConfirm: 'Apply to {n} monitor(s)', + selectNone: 'Select none', + selectedCount: 'Selected {n} / {total}', + deleteConfirm: 'Delete template "{name}"? {n} associated monitor(s) will be disassociated but keep their current snapshot and continue running.', + associatedCount: '{n} associated monitor(s)', + headersSummary: '{n} custom header(s)', + form: { + name: 'Template name', + namePlaceholder: 'e.g. Claude Code mimicry', + description: 'Description', + descriptionPlaceholder: 'Optional: what this template is for, capture date, etc.' + } + } + }, + + // Subscriptions + subscriptions: { + title: 'Subscription Management', + description: 'Manage user subscriptions and quota limits', + assignSubscription: 'Assign Subscription', + adjustSubscription: 'Adjust Subscription', + revokeSubscription: 'Revoke Subscription', + allStatus: 'Все статусы', + allGroups: 'Все группы', + allPlatforms: 'All Platforms', + daily: 'Daily', + weekly: 'Weekly', + monthly: 'Monthly', + noLimits: 'No limits configured', + unlimited: 'Безлимитно', + resetNow: 'Скоро сброс', + windowNotActive: 'Window not active', + resetInMinutes: 'Resets in {minutes}m', + resetInHoursMinutes: 'Resets in {hours}h {minutes}m', + resetInDaysHours: 'Resets in {days}d {hours}h', + quotaEndsInMinutes: 'Quota ends in {minutes}m', + quotaEndsInHoursMinutes: 'Quota ends in {hours}h {minutes}m', + quotaEndsInDaysHours: 'Quota ends in {days}d {hours}h', + daysRemaining: 'days remaining', + remainingDays: 'Remaining days', + noExpiration: 'Без срока', + status: { + active: 'Активен', + expired: 'Истекла', + revoked: 'Revoked' + }, + columns: { + user: 'Пользователь', + group: 'Группа', + usage: 'Расход', + expires: 'Expires', + status: 'Статус', + actions: 'Действия' + }, + form: { + user: 'Пользователь', + group: 'Subscription Group', + validityDays: 'Validity (Days)', + adjustDays: 'Adjust by (Days)' + }, + selectUser: 'Select a user', + selectGroup: 'Select a subscription group', + groupHint: 'Only groups with subscription billing type are shown', + validityHint: 'Number of days the subscription will be valid', + adjustingFor: 'Adjusting subscription for', + currentExpiration: 'Current expiration', + adjustDaysPlaceholder: 'Positive to extend, negative to shorten', + adjustHint: 'Enter positive number to extend, negative to shorten (remaining days must be > 0)', + assign: 'Assign', + assigning: 'Assigning...', + adjust: 'Adjust', + adjusting: 'Adjusting...', + revoke: 'Revoke', + resetQuota: 'Reset Quota', + resetQuotaTitle: 'Reset Usage Quota', + resetQuotaConfirm: "Reset the daily, weekly, and monthly usage quota for '{user}'? Usage will be zeroed and windows restarted from today.", + quotaResetSuccess: 'Quota reset successfully', + failedToResetQuota: 'Failed to reset quota', + noSubscriptionsYet: 'No subscriptions yet', + assignFirstSubscription: 'Assign a subscription to get started.', + subscriptionAssigned: 'Subscription assigned successfully', + subscriptionAdjusted: 'Subscription adjusted successfully', + subscriptionRevoked: 'Subscription revoked successfully', + failedToLoad: 'Failed to load subscriptions', + failedToAssign: 'Failed to assign subscription', + failedToAdjust: 'Failed to adjust subscription', + failedToRevoke: 'Failed to revoke subscription', + adjustWouldExpire: 'Remaining days after adjustment must be greater than 0', + adjustOutOfRange: 'Adjustment days must be between -36500 and 36500', + pleaseSelectUser: 'Please select a user', + pleaseSelectGroup: 'Please select a group', + validityDaysRequired: 'Please enter a valid number of days (at least 1)', + revokeConfirm: + "Are you sure you want to revoke the subscription for '{user}'? This action cannot be undone.", + guide: { + title: 'Subscription Management Guide', + subtitle: 'Subscription mode lets you assign time-based usage quotas to users, with daily/weekly/monthly limits. Follow these steps to get started.', + showGuide: 'Usage Guide', + step1: { + title: 'Create a Subscription Group', + line1: 'Go to "Group Management" page, click "Create Group"', + line2: 'Set billing type to "Subscription", configure daily/weekly/monthly quota limits', + line3: 'Save the group and ensure its status is "Активен"', + link: 'Go to Group Management' + }, + step2: { + title: 'Assign Subscription to User', + line1: 'Click the "Assign Subscription" button in the top right', + line2: 'Search for a user by email and select them', + line3: 'Choose a subscription group, set validity days, then click "Assign"' + }, + step3: { + title: 'Manage Existing Subscriptions' + }, + actions: { + adjust: 'Adjust', + adjustDesc: 'Extend or shorten the subscription validity period', + resetQuota: 'Reset Quota', + resetQuotaDesc: 'Reset daily/weekly/monthly usage to zero', + revoke: 'Revoke', + revokeDesc: 'Immediately terminate the subscription (irreversible)' + }, + tip: 'Tip: Only groups with billing type "Subscription" and status "Активен" appear in the group dropdown. If no options are available, create one in Group Management first.' + } + }, + + // Accounts + accounts: { + title: 'Управление аккаунтами', + description: 'Manage AI platform accounts and credentials', + createAccount: 'Создать аккаунт', + autoRefresh: 'Автообновление', + enableAutoRefresh: 'Включить автообновление', + refreshInterval5s: '5 seconds', + refreshInterval10s: '10 seconds', + refreshInterval15s: '15 seconds', + refreshInterval30s: '30 seconds', + autoRefreshCountdown: 'Автообновление: {seconds}с', + listPendingSyncHint: 'List changes are pending sync. Click sync to load latest rows.', + listPendingSyncAction: 'Sync now', + syncFromCrs: 'Sync from CRS', + dataExport: 'Экспорт', + dataExportSelected: 'Export Selected', + dataExportIncludeProxies: 'Include proxies linked to the exported accounts', + dataImport: 'Импорт', + moreActions: 'More Actions', + dataActions: 'Data', + toolActions: 'Tools', + viewColumns: 'Columns', + selectedCount: '{count} selected', + dataExportConfirmMessage: 'The exported data contains sensitive account and proxy information. Store it securely.', + dataExportConfirm: 'Confirm Export', + dataExported: 'Data exported successfully', + dataExportFailed: 'Failed to export data', + dataImportTitle: 'Import Data', + dataImportHint: 'Upload the exported JSON file to import accounts and proxies.', + dataImportWarning: 'Import will create new accounts/proxies; groups must be bound manually. Ensure existing data does not conflict.', + dataImportFile: 'Data file', + dataImportButton: 'Start Import', + dataImporting: 'Importing...', + dataImportSelectFile: 'Please select a data file', + dataImportParseFailed: 'Failed to parse data file', + dataImportFailed: 'Data import failed', + dataImportResult: 'Import Result', + dataImportResultSummary: 'Proxies created {proxy_created}, reused {proxy_reused}, failed {proxy_failed}; Accounts created {account_created}, failed {account_failed}', + dataImportErrors: 'Error Details', + dataImportSuccess: 'Import completed: accounts {account_created}, failed {account_failed}', + dataImportCompletedWithErrors: 'Import completed with errors: account failed {account_failed}, proxy failed {proxy_failed}', + syncFromCrsTitle: 'Sync Accounts from CRS', + syncFromCrsDesc: + 'Sync accounts from claude-relay-service (CRS) into this system (CRS is called server-to-server).', + crsVersionRequirement: '⚠️ Note: CRS version must be ≥ v1.1.240 to support this feature', + crsBaseUrl: 'CRS Base URL', + crsBaseUrlPlaceholder: 'e.g. http://127.0.0.1:3000', + crsUsername: 'Имя пользователя', + crsPassword: 'Пароль', + syncProxies: 'Also sync proxies (match by host/port/auth or create)', + syncNow: 'Sync Now', + syncing: 'Syncing...', + syncMissingFields: 'Please fill base URL, username and password', + syncResult: 'Sync Result', + syncResultSummary: 'Created {created}, updated {updated}, skipped {skipped}, failed {failed}', + syncErrors: 'Errors / Skipped Details', + syncCompleted: 'Sync completed: created {created}, updated {updated}, skipped {skipped}', + syncCompletedWithErrors: + 'Sync completed with errors: failed {failed} (created {created}, updated {updated}, skipped {skipped})', + syncFailed: 'Sync failed', + crsPreview: 'Preview', + crsPreviewing: 'Previewing...', + crsPreviewFailed: 'Preview failed', + crsExistingAccounts: 'Existing accounts (will be updated)', + crsNewAccounts: 'New accounts (select to sync)', + crsSelectAll: 'Выбрать все', + crsSelectNone: 'Select none', + crsNoNewAccounts: 'All CRS accounts are already synced.', + crsWillUpdate: 'Will update {count} existing accounts.', + crsSelectedCount: '{count} new accounts selected', + crsUpdateBehaviorNote: + 'Existing accounts only sync fields returned by CRS; missing fields keep their current values. Credentials are merged by key — keys not returned by CRS are preserved. Proxies are kept when "Sync proxies" is unchecked.', + crsBack: 'Назад', + editAccount: 'Edit Account', + deleteAccount: 'Delete Account', + searchAccounts: 'Search accounts...', + notes: 'Notes', + notesPlaceholder: 'Enter notes', + notesHint: 'Notes are optional', + allPlatforms: 'All Platforms', + allTypes: 'All Types', + allStatus: 'Все статусы', + allGroups: 'Все группы', + ungroupedGroup: 'Ungrouped', + oauthType: 'OAuth', + setupToken: 'Setup Token', + apiKey: 'API-ключ', + // Schedulable toggle + schedulable: 'Schedulable', + schedulableHint: 'Enable to include this account in API request scheduling', + schedulableEnabled: 'Scheduling enabled', + schedulableDisabled: 'Scheduling disabled', + failedToToggleSchedulable: 'Failed to toggle scheduling status', + groupCountTotal: '{count} groups total', + platforms: { + anthropic: 'Anthropic', + claude: 'Claude', + openai: 'OpenAI', + gemini: 'Gemini', + antigravity: 'Antigravity', + }, + types: { + oauth: 'OAuth', + chatgptOauth: 'ChatGPT OAuth', + responsesApi: 'Responses API', + googleOauth: 'Google OAuth', + codeAssist: 'Code Assist', + antigravityOauth: 'Antigravity OAuth', + antigravityApikey: 'Connect via Base URL + API Key', + upstream: 'Upstream', + upstreamDesc: 'Connect via Base URL + API Key' + }, + status: { + active: 'Активен', + inactive: 'Неактивен', + error: 'Ошибка', + cooldown: 'Cooldown', + paused: 'Paused', + limited: 'Limited', + rateLimited: 'Rate Limited', + overloaded: 'Overloaded', + tempUnschedulable: 'Temp Unschedulable', + quotaExceeded: 'Quota Exceeded', + unschedulable: 'Unschedulable', + rateLimitedUntil: 'Rate limited and removed from scheduling. Auto resumes at {time}', + rateLimitedAutoResume: 'Auto resumes in {time}', + modelRateLimitedUntil: '{model} rate limited until {time}', + modelCreditOveragesUntil: '{model} using AI Credits until {time}', + creditsExhausted: 'Credits Exhausted', + creditsExhaustedUntil: 'AI Credits exhausted, expected recovery at {time}', + overloadedUntil: 'Overloaded until {time}', + viewTempUnschedDetails: 'View temp unschedulable details' + }, + columns: { + name: 'Имя', + platformType: 'Platform/Type', + platform: 'Platform', + type: 'Type', + capacity: 'Capacity', + notes: 'Notes', + priority: 'Priority', + billingRateMultiplier: 'Billing Rate', + weight: 'Weight', + status: 'Статус', + schedulable: 'Schedulable', + todayStats: 'Today Stats', + groups: 'Группы', + usageWindows: 'Usage Windows', + proxy: 'Proxy', + lastUsed: 'Last Used', + createdAt: 'Создан', + expiresAt: 'Истекает', + actions: 'Действия' + }, + allPrivacyModes: 'All Privacy States', + privacyUnset: 'Unset', + privacyTrainingOff: 'Training data sharing disabled', + privacyCfBlocked: 'Blocked by Cloudflare, training may still be on', + privacyFailed: 'Failed to disable training', + privacyAntigravitySet: 'Telemetry and marketing emails disabled', + privacyAntigravityFailed: 'Privacy setting failed', + setPrivacy: 'Set Privacy', + subscriptionAbnormal: 'Abnormal', + subscriptionExpires: 'Expires', + // Capacity status tooltips + capacity: { + windowCost: { + blocked: '5h window cost exceeded, account scheduling paused', + stickyOnly: '5h window cost at threshold, only sticky sessions allowed', + normal: '5h window cost normal' + }, + sessions: { + full: 'Active sessions full, new sessions must wait (idle timeout: {idle} min)', + normal: 'Active sessions normal (idle timeout: {idle} min)' + }, + rpm: { + full: 'RPM limit reached', + warning: 'RPM approaching limit', + normal: 'RPM normal', + tieredNormal: 'RPM limit (Tiered) - Normal', + tieredWarning: 'RPM limit (Tiered) - Approaching limit', + tieredStickyOnly: 'RPM limit (Tiered) - Sticky only | Buffer: {buffer}', + tieredBlocked: 'RPM limit (Tiered) - Blocked | Buffer: {buffer}', + stickyExemptNormal: 'RPM limit (Sticky Exempt) - Normal', + stickyExemptWarning: 'RPM limit (Sticky Exempt) - Approaching limit', + stickyExemptOver: 'RPM limit (Sticky Exempt) - Over limit, sticky only' + }, + quota: { + exceeded: 'Quota exceeded, account paused', + normal: 'Quota normal' + }, + }, + tempUnschedulable: { + title: 'Temp Unschedulable', + statusTitle: 'Temp Unschedulable Status', + hint: 'Disable accounts temporarily when error code and keyword both match.', + notice: 'Rules are evaluated in order and require both error code and keyword match.', + addRule: 'Add Rule', + ruleOrder: 'Rule Order', + ruleIndex: 'Rule #{index}', + errorCode: 'Error Code', + errorCodePlaceholder: 'e.g. 429', + durationMinutes: 'Duration (minutes)', + durationPlaceholder: 'e.g. 30', + keywords: 'Keywords', + keywordsPlaceholder: 'e.g. overloaded, too many requests', + keywordsHint: 'Separate keywords with commas; any keyword match will trigger.', + description: 'Description', + descriptionPlaceholder: 'Optional note for this rule', + rulesInvalid: 'Add at least one rule with error code, keywords, and duration.', + viewDetails: 'View temp unschedulable details', + accountName: 'Account', + triggeredAt: 'Triggered At', + until: 'Until', + remaining: 'Remaining', + matchedKeyword: 'Matched Keyword', + errorMessage: 'Error Details', + reset: 'Recover State', + resetSuccess: 'Account state recovered successfully', + resetFailed: 'Failed to recover account state', + failedToLoad: 'Failed to load temp unschedulable status', + notActive: 'This account is not temporarily unschedulable.', + expired: 'Истекла', + remainingMinutes: 'About {minutes} minutes', + remainingHours: 'About {hours} hours', + remainingHoursMinutes: 'About {hours} hours {minutes} minutes', + presets: { + overloadLabel: '529 Overloaded', + overloadDesc: 'Overloaded - pause 60 minutes', + rateLimitLabel: '429 Rate Limit', + rateLimitDesc: 'Rate limited - pause 10 minutes', + unavailableLabel: '503 Unavailable', + unavailableDesc: 'Unavailable - pause 30 minutes' + } + }, + clearRateLimit: 'Clear Rate Limit', + resetQuota: 'Reset Quota', + quotaLimit: 'Quota Limit', + quotaLimitPlaceholder: '0 means unlimited', + quotaLimitHint: 'Set daily/weekly/total spending limits (USD). Anthropic API key accounts can also configure client affinity. Changing limits won\'t reset usage.', + quotaLimitToggle: 'Enable Quota Limit', + quotaLimitToggleHint: 'When enabled, account will be paused when usage reaches the set limit', + quotaDailyLimit: 'Дневной лимит', + quotaDailyLimitHint: 'Automatically resets every 24 hours from first usage.', + quotaWeeklyLimit: 'Недельный лимит', + quotaWeeklyLimitHint: 'Automatically resets every 7 days from first usage.', + quotaTotalLimit: 'Total Limit', + quotaTotalLimitHint: 'Cumulative spending limit. Does not auto-reset — use "Reset Quota" to clear.', + quotaResetMode: 'Reset Mode', + quotaResetModeRolling: 'Rolling Window', + quotaResetModeFixed: 'Fixed Time', + quotaResetHour: 'Reset Hour', + quotaWeeklyResetDay: 'Reset Day', + quotaResetTimezone: 'Reset Timezone', + quotaDailyLimitHintFixed: 'Resets daily at {hour}:00 ({timezone}).', + quotaWeeklyLimitHintFixed: 'Resets every {day} at {hour}:00 ({timezone}).', + dayOfWeek: { + monday: 'Monday', + tuesday: 'Tuesday', + wednesday: 'Wednesday', + thursday: 'Thursday', + friday: 'Friday', + saturday: 'Saturday', + sunday: 'Sunday', + }, + quotaLimitAmount: 'Total Limit', + quotaLimitAmountHint: 'Cumulative spending limit. Does not auto-reset.', + quotaNotify: { + alert: 'Alert', + enabled: 'Enable Alert', + threshold: 'Alert Amount', + thresholdPlaceholder: 'Enter percentage', + }, + testConnection: 'Test Connection', + reAuthorize: 'Re-Authorize', + refreshToken: 'Refresh Token', + noAccountsYet: 'No accounts yet', + createFirstAccount: 'Create your first account to start using AI services.', + tokenRefreshed: 'Token refreshed successfully', + accountDeleted: 'Account deleted successfully', + rateLimitCleared: 'Rate limit cleared successfully', + bulkSchedulableEnabled: 'Successfully enabled scheduling for {count} account(s)', + bulkSchedulableDisabled: 'Successfully disabled scheduling for {count} account(s)', + bulkSchedulablePartial: 'Scheduling updated partially: {success} succeeded, {failed} failed', + bulkSchedulableResultUnknown: 'Bulk scheduling result incomplete. Please retry or refresh.', + bulkActions: { + selected: '{count} account(s) selected', + selectCurrentPage: 'Select this page', + clear: 'Clear selection', + edit: 'Bulk Edit', + delete: 'Bulk Delete', + enableScheduling: 'Enable Scheduling', + disableScheduling: 'Disable Scheduling', + resetStatus: 'Reset Status', + refreshToken: 'Refresh Token', + resetStatusSuccess: 'Successfully reset {count} account(s) status', + refreshTokenSuccess: 'Successfully refreshed {count} account(s) token', + partialSuccess: 'Partially completed: {success} succeeded, {failed} failed' + }, + bulkEdit: { + title: 'Bulk Edit Accounts', + selectionInfo: + '{count} account(s) selected. Only checked or filled fields will be updated; others stay unchanged.', + baseUrlPlaceholder: 'https://api.anthropic.com or https://api.openai.com', + baseUrlNotice: 'Applies to API Key accounts only; leave empty to keep existing value', + submit: 'Update Accounts', + updating: 'Updating...', + success: 'Updated {count} account(s)', + partialSuccess: 'Partially updated: {success} succeeded, {failed} failed', + failed: 'Bulk update failed', + noSelection: 'Please select accounts to edit', + noFieldsSelected: 'Select at least one field to update', + mixedPlatformWarning: 'Selected accounts span multiple platforms ({platforms}). Model mapping presets shown are combined — ensure mappings are appropriate for each platform.' + }, + bulkDeleteTitle: 'Bulk Delete Accounts', + bulkDeleteConfirm: 'Delete the selected {count} account(s)? This action cannot be undone.', + bulkDeleteSuccess: 'Deleted {count} account(s)', + bulkDeletePartial: 'Partially deleted: {success} succeeded, {failed} failed', + bulkDeleteFailed: 'Bulk delete failed', + recoverState: 'Recover State', + recoverStateHint: 'Used to recover error, rate-limit, and temporary unschedulable runtime state.', + recoverStateSuccess: 'Account state recovered successfully', + recoverStateFailed: 'Failed to recover account state', + resetStatus: 'Reset Status', + statusReset: 'Account status reset successfully', + failedToResetStatus: 'Failed to reset account status', + failedToLoad: 'Failed to load accounts', + failedToRefresh: 'Failed to refresh token', + failedToDelete: 'Failed to delete account', + failedToClearRateLimit: 'Failed to clear rate limit', + deleteConfirm: "Are you sure you want to delete '{name}'? This action cannot be undone.", + // Create/Edit Account Modal + platform: 'Platform', + accountName: 'Account Name', + enterAccountName: 'Enter account name', + accountType: 'Account Type', + claudeCode: 'Claude Code', + claudeConsole: 'Claude Console', + bedrockLabel: 'AWS Bedrock', + bedrockDesc: 'SigV4 / API Key', + vertexLabel: 'Vertex', + vertexDesc: 'Service Account', + vertexAnthropicHint: 'Use a Google Cloud Service Account JSON to call Anthropic Claude via Vertex AI. It is recommended to configure model mapping to map client Claude model names to Vertex model IDs.', + vertexGeminiHint: 'Use a Google Cloud Service Account JSON to access Vertex AI Gemini. It is recommended to place Vertex accounts in a separate group to avoid mixing with AI Studio/Gemini OAuth on the same models.', + vertexSaJsonLabel: 'Service Account JSON', + vertexSaJsonLoaded: 'Service Account JSON loaded', + vertexSaJsonDrop: 'Drop Service Account JSON here', + vertexSaJsonKeyHidden: 'Key content is not displayed in the form.', + vertexSaJsonDropHint: 'Drag a .json file here, or click the button to select one.', + vertexSaJsonSelectBtn: 'Select JSON', + vertexSaJsonUploadHint: 'After uploading or dropping a JSON file, the project_id will be auto-extracted. Key content is only used for account creation.', + vertexSaJsonEditHint: 'Service Account JSON is not shown on the edit page; to change the JSON, delete the account and recreate it.', + vertexProjectIdPlaceholder: 'Auto-extracted from JSON', + vertexLocationHint: 'Available locations vary by Vertex model. Select the default endpoint location for this account.', + vertexLocationRequired: 'Please enter a Vertex location', + vertexSaJsonMissingFields: 'Service Account JSON is missing project_id, client_email, or private_key', + vertexSaJsonMissingProjectId: 'Service Account JSON is missing project_id', + vertexSaJsonMissingClientEmail: 'Service Account JSON is missing client_email', + vertexSaJsonInvalid: 'Service Account JSON format is invalid', + vertexSaJsonRequired: 'Please upload a Service Account JSON', + oauthSetupToken: 'OAuth / Setup Token', + addMethod: 'Add Method', + setupTokenLongLived: 'Setup Token (Long-lived)', + baseUrl: 'Base URL', + baseUrlHint: 'Leave default for official Anthropic API', + apiKeyRequired: 'API Key *', + apiKeyPlaceholder: 'sk-ant-api03-...', + apiKeyHint: 'Your Claude Console API Key', + // OpenAI specific hints + openai: { + baseUrlHint: 'Leave default for official OpenAI API', + apiKeyHint: 'Your OpenAI API Key', + oauthPassthrough: 'Auto passthrough (auth only)', + oauthPassthroughDesc: + 'When enabled, this OpenAI account uses automatic passthrough: the gateway forwards request/response as-is and only swaps auth, while keeping billing/concurrency/audit and necessary safety filtering.', + responsesWebsocketsV2: 'Responses WebSocket v2', + responsesWebsocketsV2Desc: + 'Disabled by default. Enable to allow responses_websockets_v2 capability (still gated by global and account-type switches).', + wsMode: 'WS mode', + wsModeDesc: 'Only applies to the current OpenAI account type.', + wsModeOff: 'Off (off)', + wsModeCtxPool: 'Context Pool (ctx_pool)', + wsModePassthrough: 'Passthrough (passthrough)', + wsModeShared: 'Shared (shared)', + wsModeDedicated: 'Dedicated (dedicated)', + wsModeConcurrencyHint: + 'When WS mode is enabled, account concurrency becomes the WS connection pool limit for this account.', + wsModePassthroughHint: 'Passthrough mode does not use the WS connection pool.', + oauthResponsesWebsocketsV2: 'OAuth WebSocket Mode', + oauthResponsesWebsocketsV2Desc: + 'Only applies to OpenAI OAuth. This account can use OpenAI WebSocket Mode only when enabled.', + apiKeyResponsesWebsocketsV2: 'API Key WebSocket Mode', + apiKeyResponsesWebsocketsV2Desc: + 'Only applies to OpenAI API Key. This account can use OpenAI WebSocket Mode only when enabled.', + responsesWebsocketsV2PassthroughHint: + 'Automatic passthrough is currently enabled: it only affects HTTP passthrough and does not disable WS mode.', + responsesMode: 'Responses API support', + responsesModeDesc: + 'Only applies to OpenAI API Key accounts. Auto follows probe results; force modes override probing.', + responsesModeAuto: 'Auto', + responsesModeForceResponses: 'Force Responses', + responsesModeForceChatCompletions: 'Force Chat Completions', + responsesStatusAutoSupported: 'Auto probe: Responses', + responsesStatusAutoUnsupported: 'Auto probe: Chat Completions', + responsesStatusAutoUnknown: 'Auto probe: unknown', + responsesStatusForcedResponses: 'Forced Responses', + responsesStatusForcedChatCompletions: 'Forced Chat Completions', + codexCLIOnly: 'Codex official clients only', + codexCLIOnlyDesc: + 'Only applies to OpenAI OAuth. When enabled, only Codex official client families are allowed; when disabled, the gateway bypasses this restriction and keeps existing behavior.', + codexImageGenerationBridge: 'Codex image-generation bridge', + codexImageGenerationBridgeDesc: + 'Account policy takes precedence over channel and global settings. Only controls whether Codex requests through the /responses text endpoint receive the image_generation tool; standalone image-generation endpoints are unaffected.', + codexImageGenerationBridgeInherit: 'Follow channel', + codexImageGenerationBridgeInheritDesc: 'Do not write an account override; use the channel or global policy.', + codexImageGenerationBridgeEnabled: 'Force on', + codexImageGenerationBridgeEnabledDesc: 'Allow image tool injection for Codex /responses requests.', + codexImageGenerationBridgeDisabled: 'Force off', + codexImageGenerationBridgeDisabledDesc: 'Block image tool injection for Codex /responses requests.', + codexImageGenerationBridgeBadgeInherit: 'Channel policy', + codexImageGenerationBridgeBadgeEnabled: 'Account on', + codexImageGenerationBridgeBadgeDisabled: 'Account off', + compactMode: 'Compact mode', + compactModeDesc: + 'Controls how this account participates in /responses/compact routing. Auto follows probe results, Force On always allows, Force Off always excludes.', + compactModeAuto: 'Auto', + compactModeForceOn: 'Force On', + compactModeForceOff: 'Force Off', + compactModelMapping: 'Compact-only model mapping', + compactModelMappingDesc: + 'Only applies to /responses/compact. Use this when the upstream compact endpoint requires a special compact model.', + compactSupported: 'Compact supported', + compactUnsupported: 'Compact unsupported', + compactAuto: 'Compact Auto', + compactUnknown: 'Compact Auto', + compactLastChecked: 'Last compact probe', + testMode: 'Test mode', + testModeDefault: 'Default request', + testModeCompact: 'Compact probe', + modelRestrictionDisabledByPassthrough: 'Automatic passthrough is enabled: model whitelist/mapping will not take effect.', + }, + anthropic: { + apiKeyPassthrough: 'Auto passthrough (auth only)', + apiKeyPassthroughDesc: + 'Only applies to Anthropic API Key accounts. When enabled, messages/count_tokens are forwarded in passthrough mode with auth replacement only, while billing/concurrency/audit and safety filtering are preserved. Disable to roll back immediately.', + webSearchEmulation: 'Web Search Emulation', + webSearchEmulationDesc: + 'Enable web search emulation for this API Key account. When a pure web_search request is detected, the gateway calls a third-party search API and constructs the response locally. Default follows channel config.', + webSearchDefault: 'По умолчанию', + webSearchEnabled: 'Включено', + webSearchDisabled: 'Отключено', + }, + modelRestriction: 'Model Restriction (Optional)', + modelWhitelist: 'Model Whitelist', + modelMapping: 'Model Mapping', + selectAllowedModels: 'Select allowed models. Leave empty to support all models.', + mapRequestModels: + 'Map request models to actual models. Left is the requested model, right is the actual model sent to API.', + selectedModels: 'Selected {count} model(s)', + supportsAllModels: '(supports all models)', + requestModel: 'Request model', + actualModel: 'Actual model', + addMapping: 'Add Mapping', + mappingExists: 'Mapping for {model} already exists', + wildcardOnlyAtEnd: 'Wildcard * can only be at the end', + targetNoWildcard: 'Target model cannot contain wildcard *', + searchModels: 'Search models...', + noMatchingModels: 'No matching models', + fillRelatedModels: 'Sync latest supported models', + syncUpstreamModels: 'Sync upstream supported models', + syncUpstreamModelsLoading: 'Syncing upstream...', + syncUpstreamModelsSuccess: 'Synced {count} new model(s) from upstream ({total} upstream total)', + syncUpstreamModelsNoChanges: 'All {count} upstream model(s) are already in the whitelist', + syncUpstreamModelsEmpty: 'Upstream returned no models to sync', + syncUpstreamModelsFailed: 'Failed to sync upstream models', + syncUpstreamModelsError: 'Failed to sync upstream models: {message}', + clearAllModels: 'Clear all models', + customModelName: 'Custom model name', + enterCustomModelName: 'Enter custom model name', + addModel: 'Добавить', + modelExists: 'Model already exists', + modelCount: '{count} models', + poolMode: 'Pool Mode', + poolModeHint: 'Enable when upstream is an account pool; errors won\'t mark local account status', + poolModeInfo: + 'When enabled, upstream 429/403/401 errors will auto-retry without marking the account as rate-limited or errored. Suitable for upstream pointing to another sub2api instance.', + poolModeRetryCount: 'Same-Account Retries', + poolModeRetryCountHint: + 'Only applies in pool mode. Use 0 to disable in-place retry. Default {default}, maximum {max}.', + poolModeRetryStatusCodes: 'Retry Status Codes', + poolModeRetryStatusCodesHint: + 'Comma-separated HTTP status codes (100-599) that trigger same-account retry in pool mode. Leave blank to use defaults ({default}).', + customErrorCodes: 'Custom Error Codes', + customErrorCodesHint: 'Only stop scheduling for selected error codes', + customErrorCodesWarning: + 'Only selected error codes will stop scheduling. Other errors will return 500.', + customErrorCodes429Warning: + '429 already has built-in rate limit handling. Adding it to custom error codes will disable the account instead of temporary rate limiting. Are you sure?', + customErrorCodes529Warning: + '529 already has built-in overload handling. Adding it to custom error codes will disable the account instead of temporary overload marking. Are you sure?', + selectedErrorCodes: 'Selected', + noneSelectedUsesDefault: 'None selected (uses default policy)', + enterErrorCode: 'Enter error code (100-599)', + invalidErrorCode: 'Please enter a valid HTTP error code (100-599)', + errorCodeExists: 'This error code is already selected', + interceptWarmupRequests: 'Intercept Warmup Requests', + interceptWarmupRequestsDesc: + 'When enabled, warmup requests like title generation will return mock responses without consuming upstream tokens', + autoPauseOnExpired: 'Auto Pause On Expired', + autoPauseOnExpiredDesc: 'When enabled, the account will auto pause scheduling after it expires', + // Quota control (Anthropic OAuth/SetupToken only) + quotaControl: { + title: 'Quota Control', + hint: 'Configure cost window, session limits, client affinity and other scheduling controls.', + windowCost: { + label: '5h Window Cost Limit', + hint: 'Limit account cost usage within the 5-hour window', + limit: 'Cost Threshold', + limitPlaceholder: '50', + limitHint: 'Account will not participate in new scheduling after reaching threshold', + stickyReserve: 'Sticky Reserve', + stickyReservePlaceholder: '10', + stickyReserveHint: 'Additional reserve for sticky sessions' + }, + sessionLimit: { + label: 'Session Count Limit', + hint: 'Limit the number of active concurrent sessions', + maxSessions: 'Max Sessions', + maxSessionsPlaceholder: '3', + maxSessionsHint: 'Maximum number of active concurrent sessions', + idleTimeout: 'Idle Timeout', + idleTimeoutPlaceholder: '5', + idleTimeoutHint: 'Sessions will be released after idle timeout' + }, + rpmLimit: { + label: 'RPM Limit', + hint: 'Limit requests per minute to protect upstream accounts', + baseRpm: 'Base RPM', + baseRpmPlaceholder: '15', + baseRpmHint: 'Max requests per minute, 0 or empty means no limit', + strategy: 'RPM Strategy', + strategyTiered: 'Tiered Model', + strategyStickyExempt: 'Sticky Exempt', + strategyTieredHint: 'Green → Yellow → Sticky only → Blocked, progressive throttling', + strategyStickyExemptHint: 'Only sticky sessions allowed when over limit', + strategyHint: 'Tiered: gradually restrict when exceeded; Sticky Exempt: existing sessions unrestricted', + stickyBuffer: 'Sticky Buffer', + stickyBufferPlaceholder: 'Default: 20% of base RPM', + stickyBufferHint: 'Extra requests allowed for sticky sessions after exceeding base RPM. Leave empty to use default (20% of base RPM, min 1)', + userMsgQueue: 'User Message Rate Control', + userMsgQueueHint: 'Rate-limit user messages to avoid triggering upstream RPM limits', + umqModeOff: 'Off', + umqModeThrottle: 'Throttle', + umqModeSerialize: 'Serialize', + }, + tlsFingerprint: { + label: 'TLS Fingerprint Simulation', + hint: 'Simulate Node.js/Claude Code client TLS fingerprint', + defaultProfile: 'Built-in Default', + randomProfile: 'Random' + }, + sessionIdMasking: { + label: 'Session ID Masking', + hint: 'When enabled, fixes the session ID in metadata.user_id for 15 minutes, making upstream think requests come from the same session' + }, + cacheTTLOverride: { + label: 'Cache TTL Override', + hint: 'Force all cache creation tokens to be billed as the selected TTL tier (5m or 1h)', + target: 'Target TTL', + targetHint: 'Select the TTL tier for billing' + }, + customBaseUrl: { + label: 'Custom Relay URL', + hint: 'Forward requests to a custom relay service. Proxy URL will be passed as a query parameter.', + urlHint: 'Relay service URL (e.g., https://relay.example.com)', + }, + clientAffinity: { + label: 'Client Affinity Scheduling', + hint: 'When enabled, new sessions prefer accounts previously used by this client to reduce account switching' + } + }, + affinityNoClients: 'No affinity clients', + affinityClients: '{count} affinity clients:', + affinitySection: 'Client Affinity', + affinitySectionHint: 'Control how clients are distributed across accounts. Configure zone thresholds to balance load.', + affinityToggle: 'Enable Client Affinity', + affinityToggleHint: 'New sessions prefer accounts previously used by this client', + affinityBase: 'Base Limit (Green Zone)', + affinityBasePlaceholder: 'Empty = no limit', + affinityBaseHint: 'Max clients in green zone (full priority scheduling)', + affinityBaseOffHint: 'No green zone limit. All clients receive full priority scheduling.', + affinityBuffer: 'Buffer (Yellow Zone)', + affinityBufferPlaceholder: 'e.g. 3', + affinityBufferHint: 'Additional clients allowed in the yellow zone (degraded priority)', + affinityBufferInfinite: 'Безлимитно', + expired: 'Истекла', + proxy: 'Proxy', + noProxy: 'No Proxy', + concurrency: 'Параллелизм', + loadFactor: 'Load Factor', + loadFactorHint: 'Higher load factor increases scheduling frequency', + priority: 'Priority', + priorityHint: 'Lower value accounts are used first', + billingRateMultiplier: 'Billing Rate Multiplier', + billingRateMultiplierHint: '0 = free, affects account billing only', + expiresAt: 'Истекает', + expiresAtHint: 'Leave empty for no expiration', + higherPriorityFirst: 'Lower value means higher priority', + mixedScheduling: 'Use in /v1/messages', + mixedSchedulingHint: 'Enable to participate in Anthropic/Gemini group scheduling', + mixedSchedulingTooltip: + '!! WARNING !! Antigravity Claude and Anthropic Claude cannot be used in the same context. If you have both Anthropic and Antigravity accounts, enabling this option will cause frequent 400 errors. When enabled, please use the group feature to isolate Antigravity accounts from Anthropic accounts. Make sure you understand this before enabling!!', + aiCreditsBalance: 'AI Credits', + allowOverages: 'Allow Overages (AI Credits)', + allowOveragesTooltip: + 'Only use AI Credits after free quota is explicitly exhausted. Ordinary concurrent 429 rate limits will not switch to overages.', + creating: 'Creating...', + updating: 'Updating...', + accountCreated: 'Account created successfully', + accountUpdated: 'Account updated successfully', + failedToCreate: 'Failed to create account', + failedToUpdate: 'Failed to update account', + pleaseSelectStatus: 'Please select a valid account status', + mixedChannelWarningTitle: 'Mixed Channel Warning', + mixedChannelWarning: 'Warning: Group "{groupName}" contains both {currentPlatform} and {otherPlatform} accounts. Mixing different channels may cause thinking block signature validation issues, which will fallback to non-thinking mode. Are you sure you want to continue?', + pleaseEnterAccountName: 'Please enter account name', + pleaseEnterApiKey: 'Please enter API Key', + bedrockAccessKeyId: 'AWS Access Key ID', + bedrockSecretAccessKey: 'AWS Secret Access Key', + bedrockSessionToken: 'AWS Session Token', + bedrockRegion: 'AWS Region', + bedrockRegionHint: 'e.g. us-east-1, us-west-2, eu-west-1', + bedrockForceGlobal: 'Force Global cross-region inference', + bedrockForceGlobalHint: 'When enabled, model IDs use the global. prefix (e.g. global.anthropic.claude-...), routing requests to any supported region worldwide for higher availability', + bedrockAccessKeyIdRequired: 'Please enter AWS Access Key ID', + bedrockSecretAccessKeyRequired: 'Please enter AWS Secret Access Key', + bedrockRegionRequired: 'Please select AWS Region', + bedrockSessionTokenHint: 'Optional, for temporary credentials', + bedrockSecretKeyLeaveEmpty: 'Leave empty to keep current key', + bedrockAuthMode: 'Authentication Mode', + bedrockAuthModeSigv4: 'SigV4 Signing', + bedrockAuthModeApikey: 'Bedrock API Key', + bedrockApiKeyLabel: 'Bedrock API Key', + bedrockApiKeyDesc: 'Bearer Token', + bedrockApiKeyInput: 'API-ключ', + bedrockApiKeyRequired: 'Please enter Bedrock API Key', + bedrockApiKeyLeaveEmpty: 'Leave empty to keep current key', + apiKeyIsRequired: 'API Key is required', + leaveEmptyToKeep: 'Leave empty to keep current key', + // Upstream type + upstream: { + baseUrl: 'Upstream Base URL', + baseUrlHint: 'The address of the upstream Antigravity service, e.g., https://cloudcode-pa.googleapis.com', + apiKey: 'Upstream API Key', + apiKeyHint: 'API Key for the upstream service', + pleaseEnterBaseUrl: 'Please enter upstream Base URL', + pleaseEnterApiKey: 'Please enter upstream API Key' + }, + // OAuth flow + oauth: { + title: 'Claude Account Authorization', + authMethod: 'Authorization Method', + manualAuth: 'Manual Authorization', + cookieAutoAuth: 'Cookie Auto-Auth', + cookieAutoAuthDesc: + 'Use claude.ai sessionKey to automatically complete OAuth authorization without manually opening browser.', + sessionKey: 'sessionKey', + keysCount: '{count} keys', + batchCreateAccounts: 'Will batch create {count} accounts', + sessionKeyPlaceholder: + 'One sessionKey per line, e.g.:\nsk-ant-sid01-xxxxx...\nsk-ant-sid01-yyyyy...', + sessionKeyPlaceholderSingle: 'sk-ant-sid01-xxxxx...', + howToGetSessionKey: 'How to get sessionKey', + step1: 'Login to claude.ai in your browser', + step2: 'Press F12 to open Developer Tools', + step3: 'Go to Application tab', + step4: 'Find Cookies → https://claude.ai', + step5: 'Find the row with key sessionKey', + step6: 'Copy the Value', + sessionKeyFormat: 'sessionKey usually starts with sk-ant-sid01-', + startAutoAuth: 'Start Auto-Auth', + authorizing: 'Authorizing...', + followSteps: 'Follow these steps to authorize your Claude account:', + step1GenerateUrl: 'Click the button below to generate the authorization URL', + generateAuthUrl: 'Generate Auth URL', + generating: 'Generating...', + regenerate: 'Regenerate', + step2OpenUrl: 'Open the URL in your browser and complete authorization', + openUrlDesc: + 'Open the authorization URL in a new tab, log in to your Claude account and authorize.', + proxyWarning: + 'Note: If you configured a proxy, make sure your browser uses the same proxy to access the authorization page.', + step3EnterCode: 'Enter the Authorization Code', + authCodeDesc: + 'After authorization is complete, the page will display an Authorization Code. Copy and paste it below:', + authCode: 'Authorization Code', + authCodePlaceholder: 'Paste the Authorization Code from Claude page...', + authCodeHint: 'Paste the Authorization Code copied from the Claude page', + completeAuth: 'Complete Authorization', + verifying: 'Проверка...', + pleaseEnterSessionKey: 'Please enter at least one valid sessionKey', + authFailed: 'Authorization failed', + cookieAuthFailed: 'Cookie authorization failed', + keyAuthFailed: 'Key {index}: {error}', + successCreated: 'Successfully created {count} account(s)', + batchSuccess: 'Successfully created {count} account(s)', + batchPartialSuccess: 'Partial success: {success} succeeded, {failed} failed', + batchFailed: 'Batch creation failed', + // OpenAI specific + openai: { + title: 'OpenAI Account Authorization', + followSteps: 'Follow these steps to complete OpenAI account authorization:', + step1GenerateUrl: 'Click the button below to generate the authorization URL', + generateAuthUrl: 'Generate Auth URL', + step2OpenUrl: 'Open the URL in your browser and complete authorization', + openUrlDesc: + 'Open the authorization URL in a new tab, log in to your OpenAI account and authorize.', + importantNotice: + 'Important: The page may take a while to load after authorization. Please wait patiently. When the browser address bar changes to http://localhost..., the authorization is complete.', + step3EnterCode: 'Enter Authorization URL or Code', + authCodeDesc: + 'After authorization is complete, when the page URL becomes http://localhost:xxx/auth/callback?code=...:', + authCode: 'Authorization URL or Code', + authCodePlaceholder: + 'Option 1: Copy the complete URL\n(http://localhost:xxx/auth/callback?code=...)\nOption 2: Copy only the code parameter value', + authCodeHint: + 'You can copy the entire URL or just the code parameter value, the system will auto-detect', + failedToGenerateUrl: 'Failed to generate OpenAI auth URL', + failedToExchangeCode: 'Failed to exchange OpenAI auth code', + failedToValidateRT: 'Failed to validate refresh token', + errors: { + OPENAI_OAUTH_PROXY_REQUIRED: + 'No proxy is configured and this server could not reach OpenAI directly, so the OpenAI OAuth request failed. Select a proxy that can access OpenAI and retry; if the authorization code has expired, regenerate the authorization URL.' + }, + // Refresh Token auth + refreshTokenAuth: 'Manual RT Input', + refreshTokenDesc: 'Enter your existing OpenAI Refresh Token(s). Supports batch input (one per line). The system will automatically validate and create accounts.', + refreshTokenPlaceholder: 'Paste your OpenAI Refresh Token...\nSupports multiple, one per line', + codexSessionAuth: 'Codex JSON / AT Batch Input', + codexSessionDesc: 'Paste Codex JSON or an accessToken. Accounts use the step 1 settings.', + codexSessionInputLabel: 'Codex JSON or accessToken', + codexSessionPlaceholder: 'Multiple lines supported, one token or JSON per line', + codexSessionHint: 'sessionToken will not be saved as refresh_token. Without refresh_token, the account expires with the accessToken expiry; import is rejected if the expiry cannot be parsed and step 1 has no expiration.', + codexSessionImportAndCreate: 'Import & Create Account', + codexSessionEmpty: 'Please enter Codex JSON or accessToken', + codexSessionImportFailed: 'Failed to import Codex account', + codexSessionImportSuccess: 'Import completed: created {created}, updated {updated}, skipped {skipped}', + codexSessionImportPartial: 'Partial success: created {created}, updated {updated}, skipped {skipped}, failed {failed}', + sessionTokenAuth: 'Manual ST Input', + sessionTokenDesc: 'Enter your existing Session Token(s). Supports batch input (one per line). The system will automatically validate and create accounts.', + sessionTokenPlaceholder: 'Paste your Session Token...\nSupports multiple, one per line', + sessionTokenRawLabel: 'Raw Input', + sessionTokenRawPlaceholder: 'Paste /api/auth/session raw payload or Session Token...', + sessionTokenRawHint: 'You can paste full JSON. The system will auto-parse ST and AT.', + openSessionUrl: 'Open Fetch URL', + copySessionUrl: 'Copy URL', + sessionUrlHint: 'This URL usually returns AT. If sessionToken is absent, copy __Secure-next-auth.session-token from browser cookies as ST.', + parsedSessionTokensLabel: 'Parsed ST', + parsedSessionTokensEmpty: 'No ST parsed. Please check your input.', + parsedAccessTokensLabel: 'Parsed AT', + validating: 'Validating...', + validateAndCreate: 'Validate & Create Account', + pleaseEnterRefreshToken: 'Please enter Refresh Token', + pleaseEnterSessionToken: 'Please enter Session Token' + }, + // Gemini specific + gemini: { + title: 'Gemini Account Authorization', + followSteps: 'Follow these steps to authorize your Gemini account:', + step1GenerateUrl: 'Generate the authorization URL', + generateAuthUrl: 'Generate Auth URL', + projectIdLabel: 'Project ID (optional)', + projectIdPlaceholder: 'e.g. my-gcp-project or cloud-ai-companion-xxxxx', + projectIdHint: + 'Leave empty to auto-detect after code exchange. If auto-detection fails, fill it in and re-generate the auth URL to try again.', + howToGetProjectId: 'How to get', + step2OpenUrl: 'Open the URL in your browser and complete authorization', + openUrlDesc: + 'Open the authorization URL in a new tab, log in to your Google account and authorize.', + step3EnterCode: 'Enter Authorization URL or Code', + authCodeDesc: + 'After authorization, copy the callback URL (recommended) or just the code and paste it below.', + authCode: 'Callback URL or Code', + authCodePlaceholder: + 'Option 1 (recommended): Paste the callback URL\nOption 2: Paste only the code value', + authCodeHint: 'The system will auto-extract code/state from the URL.', + redirectUri: 'Redirect URI', + redirectUriHint: + 'This must be configured in your Google OAuth client and must match exactly.', + confirmRedirectUri: + 'I have configured this Redirect URI in the Google OAuth client (must match exactly)', + invalidRedirectUri: 'Redirect URI must be a valid http(s) URL', + redirectUriNotConfirmed: 'Please confirm the Redirect URI is configured correctly', + missingRedirectUri: 'Missing redirect URI', + failedToGenerateUrl: 'Failed to generate Gemini auth URL', + missingExchangeParams: 'Missing auth code, session ID, or state', + failedToExchangeCode: 'Failed to exchange Gemini auth code', + missingProjectId: 'GCP Project ID retrieval failed: Your Google account is not linked to an active GCP project. Please activate GCP and bind a credit card in Google Cloud Console, or manually enter the Project ID during authorization.', + modelPassthrough: 'Gemini Model Passthrough', + modelPassthroughDesc: + 'All model requests are forwarded directly to the Gemini API without model restrictions or mappings.', + stateWarningTitle: 'Note', + stateWarningDesc: 'Recommended: paste the full callback URL (includes code & state).', + oauthTypeLabel: 'OAuth Type', + needsProjectId: 'Built-in OAuth (Code Assist)', + needsProjectIdDesc: 'Requires GCP project and Project ID', + noProjectIdNeeded: 'Custom OAuth (AI Studio)', + noProjectIdNeededDesc: 'Requires admin-configured OAuth client', + aiStudioNotConfiguredShort: 'Not configured', + aiStudioNotConfiguredTip: + 'AI Studio OAuth is not configured: set GEMINI_OAUTH_CLIENT_ID / GEMINI_OAUTH_CLIENT_SECRET and add Redirect URI: http://localhost:1455/auth/callback (Consent screen scopes must include https://www.googleapis.com/auth/generative-language.retriever)', + aiStudioNotConfigured: + 'AI Studio OAuth is not configured: set GEMINI_OAUTH_CLIENT_ID / GEMINI_OAUTH_CLIENT_SECRET and add Redirect URI: http://localhost:1455/auth/callback' + }, + // Antigravity specific + antigravity: { + title: 'Antigravity Account Authorization', + followSteps: 'Follow these steps to authorize your Antigravity account:', + step1GenerateUrl: 'Generate the authorization URL', + generateAuthUrl: 'Generate Auth URL', + step2OpenUrl: 'Open the URL in your browser and complete authorization', + openUrlDesc: 'Open the authorization URL in a new tab, log in to your Google account and authorize.', + importantNotice: + 'Important: The page may take a while to load after authorization. Please wait patiently. When the browser address bar shows http://localhost..., authorization is complete.', + step3EnterCode: 'Enter Authorization URL or Code', + authCodeDesc: + 'After authorization, when the page URL becomes http://localhost:xxx/auth/callback?code=...:', + authCode: 'Authorization URL or Code', + authCodePlaceholder: + 'Option 1: Copy the complete URL\n(http://localhost:xxx/auth/callback?code=...)\nOption 2: Copy only the code parameter value', + authCodeHint: 'You can copy the entire URL or just the code parameter value, the system will auto-detect', + failedToGenerateUrl: 'Failed to generate Antigravity auth URL', + missingExchangeParams: 'Missing code, session ID, or state', + failedToExchangeCode: 'Failed to exchange Antigravity auth code', + // Refresh Token auth + refreshTokenAuth: 'Manual RT', + refreshTokenDesc: 'Enter your existing Antigravity Refresh Token. Supports batch input (one per line). The system will automatically validate and create accounts.', + refreshTokenPlaceholder: 'Paste your Antigravity Refresh Token...\nSupports multiple tokens, one per line', + validating: 'Validating...', + validateAndCreate: 'Validate & Create', + pleaseEnterRefreshToken: 'Please enter Refresh Token', + failedToValidateRT: 'Failed to validate Refresh Token' + } + }, // Gemini specific (platform-wide) + gemini: { + helpButton: 'Help', + helpDialog: { + title: 'Gemini Usage Guide', + apiKeySection: 'API Key Links' + }, + modelPassthrough: 'Gemini Model Passthrough', + modelPassthroughDesc: + 'All model requests are forwarded directly to the Gemini API without model restrictions or mappings.', + baseUrlHint: 'Leave default for official Gemini API', + apiKeyHint: 'Your Gemini API Key (starts with AIza)', + tier: { + label: 'Account Tier', + hint: 'Tip: The system will try to auto-detect the tier first; if auto-detection is unavailable or fails, your selected tier is used as a fallback (simulated quota).', + aiStudioHint: + 'AI Studio quotas are per-model (Pro/Flash are limited independently). If billing is enabled, choose Pay-as-you-go.', + googleOne: { + free: 'Google One Free', + pro: 'Google One Pro', + ultra: 'Google One Ultra' + }, + gcp: { + standard: 'GCP Standard', + enterprise: 'GCP Enterprise' + }, + aiStudio: { + free: 'Google AI Free', + paid: 'Google AI Pay-as-you-go' + } + }, + accountType: { + oauthTitle: 'OAuth (Gemini)', + oauthDesc: 'Authorize with your Google account and choose an OAuth type.', + apiKeyTitle: 'API Key (AI Studio)', + apiKeyDesc: 'Fastest setup. Use an AIza API key.', + apiKeyNote: + 'Best for light testing. Free tier has strict rate limits and data may be used for training.', + apiKeyLink: 'Get API Key', + quotaLink: 'Quota guide' + }, + oauthType: { + builtInTitle: 'Built-in OAuth (Gemini CLI / Code Assist)', + builtInDesc: 'Uses Google built-in client ID. No admin configuration required.', + builtInRequirement: 'Requires a GCP project and Project ID.', + gcpProjectLink: 'Create project', + customTitle: 'Custom OAuth (AI Studio OAuth)', + customDesc: 'Uses admin-configured OAuth client for org management.', + customRequirement: 'Admin must configure Client ID and add you as a test user.', + badges: { + recommended: 'Recommended', + highConcurrency: 'High concurrency', + noAdmin: 'No admin setup', + orgManaged: 'Org managed', + adminRequired: 'Admin required' + } + }, + setupGuide: { + title: 'Gemini Setup Checklist', + checklistTitle: 'Checklist', + checklistItems: { + usIp: 'Use a US IP and ensure your account country is set to US.', + age: 'Account must be 18+.' + }, + activationTitle: 'One-click Activation', + activationItems: { + geminiWeb: 'Activate Gemini Web to avoid User not initialized.', + gcpProject: 'Activate a GCP project and get the Project ID for Code Assist.' + }, + links: { + countryCheck: 'Check country association', + geminiWebActivation: 'Activate Gemini Web', + gcpProject: 'Open GCP Console' + } + }, + quotaPolicy: { + title: 'Gemini Quota & Limit Policy (Reference)', + note: 'Note: Gemini does not provide an official quota inquiry API. The "Daily Quota" shown here is an estimate simulated by the system based on account tiers for scheduling reference only. Please refer to official Google errors for actual limits.', + columns: { + channel: 'Auth Channel', + account: 'Account Status', + limits: 'Limit Policy', + docs: 'Official Docs' + }, + docs: { + codeAssist: 'Code Assist Quotas', + aiStudio: 'AI Studio Pricing', + vertex: 'Vertex AI Quotas' + }, + simulatedNote: 'Simulated quota, for reference only', + rows: { + googleOne: { + channel: 'Google One OAuth (Individuals / Code Assist for Individuals)', + limitsFree: 'Shared pool: 1000 RPD / 60 RPM', + limitsPro: 'Shared pool: 1500 RPD / 120 RPM', + limitsUltra: 'Shared pool: 2000 RPD / 120 RPM' + }, + gcp: { + channel: 'GCP Code Assist OAuth (Enterprise)', + limitsStandard: 'Shared pool: 1500 RPD / 120 RPM', + limitsEnterprise: 'Shared pool: 2000 RPD / 120 RPM' + }, + cli: { + channel: 'Gemini CLI (Official Google Login / Code Assist)', + free: 'Free Google Account', + premium: 'Google One AI Premium', + limitsFree: 'RPD ~1000; RPM ~60 (soft)', + limitsPremium: 'RPD ~1500+; RPM ~60+ (priority queue)' + }, + gcloud: { + channel: 'GCP Code Assist (gcloud auth)', + account: 'No Code Assist subscription', + limits: 'RPD ~1000; RPM ~60 (preview)' + }, + aiStudio: { + channel: 'AI Studio API Key / OAuth', + free: 'No billing (free tier)', + paid: 'Billing enabled (pay-as-you-go)', + limitsFree: 'RPD 50; RPM 2 (Pro) / 15 (Flash)', + limitsPaid: 'RPD unlimited; RPM 1000 (Pro) / 2000 (Flash) (per model)' + }, + customOAuth: { + channel: 'Custom OAuth Client (GCP)', + free: 'Project not billed', + paid: 'Project billed', + limitsFree: 'RPD 50; RPM 2 (project quota)', + limitsPaid: 'RPD unlimited; RPM 1000+ (project quota)' + } + } + }, + rateLimit: { + ok: 'Not rate limited', + unlimited: 'Безлимитно', + limited: 'Rate limited {time}', + now: 'now' + } + }, + // Re-Auth Modal + reAuthorizeAccount: 'Re-Authorize Account', + claudeCodeAccount: 'Claude Code Account', + openaiAccount: 'OpenAI Account', + geminiAccount: 'Gemini Account', + antigravityAccount: 'Antigravity Account', + inputMethod: 'Input Method', + reAuthorizedSuccess: 'Account re-authorized successfully', + // Test Modal + testAccountConnection: 'Test Account Connection', + account: 'Account', + readyToTest: 'Ready to test. Click "Start Test" to begin...', + connectingToApi: 'Connecting to API...', + testCompleted: 'Test completed successfully!', + testFailed: 'Test failed', + connectedToApi: 'Connected to API', + usingModel: 'Using model: {model}', + sendingTestMessage: 'Sending test message: "hi"', + sendingImageRequest: 'Sending image generation test request...', + response: 'Response:', + startTest: 'Start Test', + testing: 'Testing...', + retry: 'Retry', + copyOutput: 'Copy output', + outputCopied: 'Output copied', + startingTestForAccount: 'Starting test for account: {name}', + testAccountTypeLabel: 'Account type: {type}', + selectTestModel: 'Select Test Model', + testModel: 'Test model', + testPrompt: 'Prompt: "hi"', + imagePromptLabel: 'Image prompt', + imagePromptPlaceholder: 'Example: Generate an orange cat astronaut sticker in pixel-art style on a solid background.', + imagePromptDefault: 'Generate a cute orange cat astronaut sticker on a clean pastel background.', + imageTestHint: 'When an image model is selected, this test sends a real image-generation request and previews the returned image below.', + imageTestMode: 'Mode: Image generation test', + imagePreview: 'Generated images:', + imageReceived: 'Received test image #{count}', + // Stats Modal + viewStats: 'View Stats', + usageStatistics: 'Usage Statistics', + last30DaysUsage: 'Last 30 days usage statistics (based on actual usage days)', + stats: { + totalCost: '30-Day Total Cost', + accumulatedCost: 'Accumulated cost', + standardCost: 'Standard', + totalRequests: '30-Day Total Requests', + totalCalls: 'Total API calls', + avgDailyCost: 'Daily Avg Cost', + basedOnActualDays: 'Based on {days} actual usage days', + avgDailyRequests: 'Daily Avg Requests', + avgDailyUsage: 'Average daily usage', + todayOverview: 'Today Overview', + cost: 'Стоимость', + requests: 'Запросы', + tokens: 'Tokens', + highestCostDay: 'Highest Cost Day', + highestRequestDay: 'Highest Request Day', + date: 'Дата', + accumulatedTokens: 'Accumulated Tokens', + totalTokens: '30-Day Total', + dailyAvgTokens: 'Daily Average', + performance: 'Performance', + avgResponseTime: 'Средний ответ', + daysActive: 'Days Active', + recentActivity: 'Recent Activity', + todayRequests: 'Запросов сегодня', + todayTokens: 'Токенов сегодня', + todayCost: 'Расход сегодня', + usageTrend: '30-Day Cost & Request Trend', + noData: 'No usage data available for this account' + }, + usageWindow: { + statsTitle: '5-Hour Window Usage Statistics', + statsTitleDaily: 'Daily Usage Statistics', + geminiProDaily: 'Pro', + geminiFlashDaily: 'Flash', + gemini3Pro: 'G3P', + gemini3Flash: 'G3F', + gemini3Image: 'G31FI', + claude: 'Claude', + passiveSampled: 'Passive', + activeQuery: 'Запросить' + }, + tier: { + free: 'Free', + pro: 'Pro', + ultra: 'Ultra', + aiPremium: 'AI Premium', + standard: 'Standard', + basic: 'Basic', + personal: 'Personal', + unlimited: 'Безлимитно' + }, + ineligibleWarning: + 'This account is not eligible for Antigravity, but API forwarding still works. Use at your own risk.', + forbidden: 'Доступ запрещён', + forbiddenValidation: 'Verification Required', + forbiddenViolation: 'Violation Ban', + openVerification: 'Open Verification Link', + copyLink: 'Copy Link', + linkCopied: 'Link Copied', + needsReauth: 'Re-auth Required', + rateLimited: 'Rate Limited', + usageError: 'Fetch Error' + }, + + // Scheduled Tests + scheduledTests: { + title: 'Scheduled Tests', + addPlan: 'Add Plan', + editPlan: 'Edit Plan', + deletePlan: 'Delete Plan', + model: 'Модель', + cronExpression: 'Cron Expression', + enabled: 'Включено', + lastRun: 'Last Run', + nextRun: 'Next Run', + maxResults: 'Max Results', + noPlans: 'No scheduled test plans', + confirmDelete: 'Are you sure you want to delete this plan?', + createSuccess: 'Plan created successfully', + updateSuccess: 'Plan updated successfully', + deleteSuccess: 'Plan deleted successfully', + results: 'Test Results', + noResults: 'No test results yet', + responseText: 'Response', + errorMessage: 'Ошибка', + success: 'Успешно', + failed: 'Ошибка', + running: 'Running', + schedule: 'Schedule', + cronHelp: 'Standard 5-field cron expression (e.g., */30 * * * *)', + cronTooltipTitle: 'Cron expression examples:', + cronTooltipMeaning: 'Defines when the test runs automatically. The 5 fields are: minute, hour, day, month, and weekday.', + cronTooltipExampleEvery30Min: '*/30 * * * *: run every 30 minutes', + cronTooltipExampleHourly: '0 * * * *: run at the start of every hour', + cronTooltipExampleDaily: '0 9 * * *: run every day at 09:00', + cronTooltipExampleWeekly: '0 9 * * 1: run every Monday at 09:00', + cronTooltipRange: 'Recommended range: use standard 5-field cron. For health checks, start with a moderate frequency such as every 30 minutes, every hour, or once a day instead of running too often.', + maxResultsTooltipTitle: 'What Max Results means:', + maxResultsTooltipMeaning: 'Sets how many historical test results are kept for a single plan so the result list does not grow without limit.', + maxResultsTooltipBody: 'Only the newest test results are kept. Once the number of saved results exceeds this value, older records are pruned automatically so the history list and storage stay under control.', + maxResultsTooltipExample: 'For example, 100 means keeping at most the latest 100 test results. When the 101st result is saved, the oldest one is removed.', + maxResultsTooltipRange: 'Recommended range: usually 20 to 200. Use 20-50 when you only care about recent health status, or 100-200 if you want a longer trend history.', + autoRecover: 'Auto Recover', + autoRecoverHelp: 'Automatically recover account from error/rate-limited state on successful test' + }, + + // Proxies + proxies: { + title: 'Proxy Management', + description: 'Manage proxy servers for accounts', + createProxy: 'Create Proxy', + editProxy: 'Edit Proxy', + deleteProxy: 'Delete Proxy', + ad: { + inline: 'Need proxy IP?' + }, + dataImport: 'Импорт', + dataExportSelected: 'Export Selected', + dataImportTitle: 'Import Proxies', + dataImportHint: 'Upload the exported proxy JSON file to import proxies in bulk.', + dataImportWarning: 'Import will create or reuse proxies, keep their status, and trigger latency checks after completion.', + dataImportFile: 'Data File', + dataImportButton: 'Start Import', + dataImporting: 'Importing...', + dataImportSelectFile: 'Please select a data file', + dataImportParseFailed: 'Failed to parse data', + dataImportFailed: 'Failed to import data', + dataImportResult: 'Import Result', + dataImportResultSummary: 'Created {proxy_created}, reused {proxy_reused}, failed {proxy_failed}', + dataImportErrors: 'Failure Details', + dataImportSuccess: 'Import completed: created {proxy_created}, reused {proxy_reused}', + dataImportCompletedWithErrors: 'Import completed with errors: failed {proxy_failed}', + dataExport: 'Экспорт', + dataExportConfirmMessage: 'The exported data contains sensitive proxy information. Store it securely.', + dataExportConfirm: 'Confirm Export', + dataExported: 'Data exported successfully', + dataExportFailed: 'Failed to export data', + copyProxyUrl: 'Copy Proxy URL', + urlCopied: 'Proxy URL copied', + searchProxies: 'Search proxies...', + allProtocols: 'All Protocols', + allStatus: 'Все статусы', + protocols: { + http: 'HTTP', + https: 'HTTPS', + socks5: 'SOCKS5', + socks5h: 'SOCKS5H (Remote DNS)' + }, + columns: { + name: 'Имя', + protocol: 'Protocol', + address: 'Address', + auth: 'Auth', + location: 'Location', + status: 'Статус', + accounts: 'Аккаунты', + latency: 'Latency', + actions: 'Действия' + }, + testConnection: 'Test Connection', + qualityCheck: 'Quality Check', + batchQualityCheck: 'Batch Quality Check', + batchTest: 'Test All Proxies', + testFailed: 'Ошибка', + latencyFailed: 'Connection failed', + batchTestEmpty: 'No proxies available for testing', + batchTestDone: 'Batch test completed for {count} proxies', + batchTestFailed: 'Batch test failed', + batchDeleteAction: 'Удалить', + batchDelete: 'Batch delete', + batchDeleteConfirm: 'Delete {count} selected proxies? In-use ones will be skipped.', + batchDeleteDone: 'Deleted {deleted} proxies, skipped {skipped}', + batchDeleteSkipped: 'Skipped {skipped} proxies', + batchDeleteFailed: 'Batch delete failed', + deleteBlockedInUse: 'This proxy is in use and cannot be deleted', + accountsTitle: 'Accounts using this IP', + accountsEmpty: 'No accounts are using this proxy', + accountsFailed: 'Failed to load accounts list', + accountName: 'Account', + accountPlatform: 'Platform', + accountNotes: 'Notes', + name: 'Имя', + protocol: 'Protocol', + host: 'Host', + port: 'Port', + username: 'Username (Optional)', + password: 'Password (Optional)', + status: 'Статус', + enterProxyName: 'Enter proxy name', + leaveEmptyToKeep: 'Leave empty to keep current', + optionalAuth: 'Optional authentication', + form: { + hostPlaceholder: 'proxy.example.com', + portPlaceholder: '8080' + }, + noProxiesYet: 'No proxies yet', + createFirstProxy: 'Create your first proxy to route traffic through it.', + // Batch import + standardAdd: 'Standard Add', + batchAdd: 'Quick Add', + batchInput: 'Proxy List', + batchInputPlaceholder: + "Enter one proxy per line in the following formats:\nsocks5://user:pass{'@'}192.168.1.1:1080\nhttp://192.168.1.1:8080\nhttps://user:pass{'@'}proxy.example.com:443", + batchInputHint: + "Supports http, https, socks5 protocols. Format: protocol://[user:pass{'@'}]host:port", + parsedCount: '{count} valid', + invalidCount: '{count} invalid', + duplicateCount: '{count} duplicate', + importing: 'Importing...', + importProxies: 'Import {count} proxies', + batchImportSuccess: 'Successfully imported {created} proxies, skipped {skipped} duplicates', + batchImportAllSkipped: 'All {skipped} proxies already exist, skipped import', + failedToImport: 'Failed to batch import', + // Other messages + creating: 'Creating...', + updating: 'Updating...', + proxyCreated: 'Proxy created successfully', + proxyUpdated: 'Proxy updated successfully', + proxyDeleted: 'Proxy deleted successfully', + proxyWorking: 'Proxy is working!', + proxyWorkingWithLatency: 'Proxy is working! Latency: {latency}ms', + proxyTestFailed: 'Proxy test failed', + qualityCheckDone: 'Quality check completed: score {score} ({grade})', + qualityCheckFailed: 'Failed to run proxy quality check', + batchQualityDone: + 'Batch quality check completed for {count} proxies: healthy {healthy}, warn {warn}, challenge {challenge}, abnormal {failed}', + batchQualityFailed: 'Batch quality check failed', + batchQualityEmpty: 'No proxies available for quality check', + qualityReportTitle: 'Proxy Quality Report', + qualityGrade: 'Grade {grade}', + qualityExitIP: 'Exit IP', + qualityCountry: 'Exit Region', + qualityBaseLatency: 'Base Latency', + qualityCheckedAt: 'Checked At', + qualityTableTarget: 'Target', + qualityTableStatus: 'Статус', + qualityTableLatency: 'Latency', + qualityTableMessage: 'Message', + qualityInline: 'Quality {grade}/{score}', + qualityStatusHealthy: 'Healthy', + qualityStatusPass: 'Pass', + qualityStatusWarn: 'Warn', + qualityStatusFail: 'Fail', + qualityStatusChallenge: 'Challenge', + qualityTargetBase: 'Base Connectivity', + failedToLoad: 'Failed to load proxies', + failedToCreate: 'Failed to create proxy', + failedToUpdate: 'Failed to update proxy', + failedToDelete: 'Failed to delete proxy', + failedToTest: 'Failed to test proxy', + nameRequired: 'Please enter proxy name', + hostRequired: 'Please enter host address', + portInvalid: 'Port must be between 1-65535', + deleteConfirm: + "Are you sure you want to delete '{name}'? Accounts using this proxy will have their proxy removed." + }, + + // Redeem Codes + redeem: { + title: 'Redeem Code Management', + description: 'Generate and manage redeem codes', + generateCodes: 'Generate Codes', + searchCodes: 'Search codes or email...', + allTypes: 'All Types', + allStatus: 'Все статусы', + balance: 'Баланс', + concurrency: 'Параллелизм', + subscription: 'Subscription', + invitation: 'Invitation', + invitationHint: 'Invitation codes are used to restrict user registration. They are automatically marked as used after use.', + unused: 'Unused', + used: 'Использовано', + columns: { + code: 'Code', + type: 'Type', + value: 'Value', + status: 'Статус', + usedBy: 'Used By', + usedAt: 'Used At', + expiresAt: 'Истекает', + actions: 'Действия' + }, + userPrefix: 'User #{id}', + exportCsv: 'Экспорт CSV', + batchUpdate: 'Batch Update', + batchUpdateTitle: 'Batch Update Redeem Codes', + selectedCount: '{count} redeem code(s) selected', + clearSelection: 'Clear selection', + selectCodesFirst: 'Select redeem codes first', + noBatchFieldsSelected: 'Select at least one field to update', + batchUpdateSuccess: 'Updated {count} redeem code(s)', + failedToBatchUpdate: 'Failed to batch update redeem codes', + batchFields: { + status: 'Статус', + expiresAt: 'Истекает', + notes: 'Notes', + group: 'Группа' + }, + batchNotesPlaceholder: 'Enter the new note, or leave blank to clear it', + clearGroup: 'Clear group', + deleteAllUnused: 'Delete All Unused Codes', + deleteCode: 'Delete Redeem Code', + deleteCodeConfirm: + 'Are you sure you want to delete this redeem code? This action cannot be undone.', + deleteAllUnusedConfirm: + 'Are you sure you want to delete all unused (active) redeem codes? This action cannot be undone.', + deleteAll: 'Delete All', + generateCodesTitle: 'Generate Redeem Codes', + generatedSuccessfully: 'Generated Successfully', + codesCreated: '{count} redeem code(s) created', + codeType: 'Code Type', + amount: 'Amount ($)', + value: 'Value', + count: 'Count', + generating: 'Generating...', + generate: 'Generate', + copyAll: 'Copy All', + copied: 'Copied!', + download: 'Download', + codesExported: 'Codes exported successfully', + codeDeleted: 'Redeem code deleted successfully', + codesDeleted: 'Successfully deleted {count} unused code(s)', + noUnusedCodes: 'No unused codes to delete', + failedToLoad: 'Failed to load redeem codes', + failedToGenerate: 'Failed to generate codes', + failedToExport: 'Failed to export codes', + failedToDelete: 'Failed to delete code', + failedToDeleteUnused: 'Failed to delete unused codes', + failedToCopy: 'Failed to copy codes', + types: { + balance: 'Баланс', + concurrency: 'Параллелизм', + subscription: 'Subscription', + invitation: 'Invitation', + // Admin adjustment types (created when admin modifies user balance/concurrency) + admin_balance: 'Balance (Admin)', + admin_concurrency: 'Concurrency (Admin)' + }, + selectGroup: 'Select Group', + selectGroupPlaceholder: 'Choose a subscription group', + validityDays: 'Validity Days', + codeExpiry: 'Code Expiry', + neverExpires: 'Never expires', + expiryPresetDays: '{days} days', + customExpiry: 'Свой', + customExpiryDays: 'Custom days', + expiryDaysRequired: 'Please enter a valid expiry day count', + groupRequired: 'Please select a subscription group', + days: ' days', + status: { + unused: 'Unused', + used: 'Использовано', + expired: 'Истекла', + disabled: 'Отключено' + } + }, + + // Announcements + announcements: { + title: 'Объявления', + description: 'Create announcements and target by conditions', + createAnnouncement: 'Create Announcement', + editAnnouncement: 'Edit Announcement', + deleteAnnouncement: 'Delete Announcement', + searchAnnouncements: 'Search announcements...', + status: 'Статус', + allStatus: 'Все статусы', + columns: { + title: 'Title', + status: 'Статус', + notifyMode: 'Notify Mode', + targeting: 'Targeting', + timeRange: 'Schedule', + createdAt: 'Created At', + actions: 'Действия' + }, + statusLabels: { + draft: 'Draft', + active: 'Активен', + archived: 'Archived' + }, + notifyModeLabels: { + silent: 'Silent', + popup: 'Popup' + }, + form: { + title: 'Title', + content: 'Content (Markdown supported)', + status: 'Статус', + notifyMode: 'Notify Mode', + notifyModeHint: 'Popup mode will show a popup notification to users', + startsAt: 'Starts At', + endsAt: 'Ends At', + startsAtHint: 'Leave empty to start immediately', + endsAtHint: 'Leave empty to never expire', + targetingMode: 'Targeting', + targetingAll: 'All users', + targetingCustom: 'Custom rules', + addOrGroup: 'Add OR group', + addAndCondition: 'Add AND condition', + conditionType: 'Condition type', + conditionSubscription: 'Subscription', + conditionBalance: 'Баланс', + operator: 'Operator', + balanceValue: 'Balance threshold', + selectPackages: 'Select packages' + }, + operators: { + gt: '>', + gte: '≥', + lt: '<', + lte: '≤', + eq: '=' + }, + targetingSummaryAll: 'All users', + targetingSummaryCustom: 'Custom ({groups} groups)', + timeImmediate: 'Immediate', + timeNever: 'Никогда', + readStatus: 'Read Status', + eligible: 'Eligible', + readAt: 'Read at', + unread: 'Unread', + searchUsers: 'Search users...', + failedToLoad: 'Failed to load announcements', + failedToCreate: 'Failed to create announcement', + failedToUpdate: 'Failed to update announcement', + failedToDelete: 'Failed to delete announcement', + failedToLoadReadStatus: 'Failed to load read status', + deleteConfirm: 'Are you sure you want to delete this announcement? This action cannot be undone.' + }, + + // Promo Codes + promo: { + title: 'Promo Code Management', + description: 'Create and manage registration promo codes', + createCode: 'Create Promo Code', + editCode: 'Edit Promo Code', + deleteCode: 'Delete Promo Code', + searchCodes: 'Search codes...', + allStatus: 'Все статусы', + columns: { + code: 'Code', + bonusAmount: 'Bonus Amount', + maxUses: 'Max Uses', + usedCount: 'Использовано', + usage: 'Расход', + status: 'Статус', + expiresAt: 'Истекает', + createdAt: 'Created At', + actions: 'Действия' + }, + // Form labels (flat structure for template usage) + code: 'Промокод', + autoGenerate: 'auto-generate if empty', + codePlaceholder: 'Enter promo code or leave empty', + bonusAmount: 'Bonus Amount ($)', + maxUses: 'Max Uses', + zeroUnlimited: '0 = unlimited', + expiresAt: 'Истекает', + notes: 'Notes', + notesPlaceholder: 'Optional notes for this code', + status: 'Статус', + neverExpires: 'Never expires', + // Status labels + statusActive: 'Активен', + statusDisabled: 'Отключено', + statusExpired: 'Истекла', + statusMaxUsed: 'Used Up', + // Usage records + usageRecords: 'История расхода', + viewUsages: 'View Usages', + noUsages: 'No usage records yet', + userPrefix: 'User #{id}', + copied: 'Copied!', + // Messages + noCodesYet: 'No promo codes yet', + createFirstCode: 'Create your first promo code to offer registration bonuses.', + codeCreated: 'Promo code created successfully', + codeUpdated: 'Promo code updated successfully', + codeDeleted: 'Promo code deleted successfully', + deleteCodeConfirm: 'Are you sure you want to delete this promo code? This action cannot be undone.', + copyRegisterLink: 'Copy register link', + registerLinkCopied: 'Register link copied to clipboard', + failedToLoad: 'Failed to load promo codes', + failedToCreate: 'Failed to create promo code', + failedToUpdate: 'Failed to update promo code', + failedToDelete: 'Failed to delete promo code', + failedToLoadUsages: 'Failed to load usage records' + }, + + // Usage Records + usage: { + title: 'История расхода', + description: 'View and manage all user usage records', + userFilter: 'Пользователь', + searchUserPlaceholder: 'Search user by email...', + searchApiKeyPlaceholder: 'Search API key by name...', + searchAccountPlaceholder: 'Search account by name...', + selectedUser: 'Selected', + user: 'Пользователь', + account: 'Account', + group: 'Группа', + requestId: 'Request ID', + requestIdCopied: 'Request ID copied', + allModels: 'All Models', + allAccounts: 'All Accounts', + allGroups: 'Все группы', + allTypes: 'All Types', + inputCost: 'Input Cost', + outputCost: 'Output Cost', + cacheCreationCost: 'Cache Creation Cost', + cacheReadCost: 'Cache Read Cost', + inputTokens: 'Входные токены', + outputTokens: 'Выходные токены', + cacheCreationTokens: 'Cache Creation Tokens', + cacheCreation5mTokens: 'Cache Write', + cacheCreation1hTokens: 'Cache Write', + cacheReadTokens: 'Cache Read Tokens', + failedToLoad: 'Failed to load usage records', + billingType: 'Billing Type', + allBillingTypes: 'All Billing Types', + billingTypeBalance: 'Баланс', + billingTypeSubscription: 'Subscription', + billingMode: 'Billing Mode', + billingModeToken: 'Token', + billingModePerRequest: 'Per Request', + billingModeImage: 'Image', + allBillingModes: 'All Billing Modes', + ipAddress: 'IP', + clickToViewBalance: 'Click to view balance history', + failedToLoadUser: 'Failed to load user info', + cleanup: { + button: 'Cleanup', + title: 'Cleanup Usage Records', + warning: 'Cleanup is irreversible and will affect historical stats.', + submit: 'Submit Cleanup', + submitting: 'Отправка...', + confirmTitle: 'Confirm Cleanup', + confirmMessage: 'Are you sure you want to submit this cleanup task? This action cannot be undone.', + confirmSubmit: 'Confirm Cleanup', + cancel: 'Отмена', + cancelConfirmTitle: 'Confirm Cancel', + cancelConfirmMessage: 'Are you sure you want to cancel this cleanup task?', + cancelConfirm: 'Confirm Cancel', + cancelSuccess: 'Cleanup task canceled', + cancelFailed: 'Failed to cancel cleanup task', + recentTasks: 'Recent Cleanup Tasks', + loadingTasks: 'Loading tasks...', + noTasks: 'No cleanup tasks yet', + range: 'Range', + deletedRows: 'Deleted', + missingRange: 'Please select a date range', + submitSuccess: 'Cleanup task created', + submitFailed: 'Failed to create cleanup task', + loadFailed: 'Failed to load cleanup tasks', + status: { + pending: 'Ожидает', + running: 'Running', + succeeded: 'Succeeded', + failed: 'Ошибка', + canceled: 'Canceled' + } + } + }, + + // Ops Monitoring + ops: { + title: 'Ops Monitoring', + description: 'Operational monitoring and troubleshooting', + // Dashboard + systemHealth: 'System Health', + overview: 'Overview', + noSystemMetrics: 'No system metrics collected yet.', + collectedAt: 'Collected at:', + window: 'window', + memory: 'Memory', + db: 'DB', + goroutines: 'Goroutines', + jobs: 'Jobs', + jobsHelp: 'Click “Details” to view job heartbeats and recent errors', + active: 'active', + idle: 'idle', + waiting: 'waiting', + conns: 'conns', + queue: 'queue', + accountSwitches: 'Account switches', + ok: 'ok', + lastRun: 'last_run:', + lastSuccess: 'last_success:', + lastError: 'last_error:', + noData: 'No data.', + loadingText: 'loading', + ready: 'ready', + requestsTotal: 'Requests (total)', + slaScope: 'SLA scope:', + tokens: 'Tokens', + tps: 'TPS:', + current: 'current', + peak: 'peak', + average: 'average', + totalRequests: 'Всего запросов', + avgQps: 'Avg QPS', + avgTps: 'Avg TPS', + avgLatency: 'Avg Request Duration', + avgTtft: 'Avg TTFT', + exceptions: 'Exceptions', + requestErrors: 'Request Errors', + errorCount: 'Error Count', + upstreamErrors: 'Upstream Errors', + errorCountExcl429529: 'Error Count (excl 429/529)', + sla: 'SLA (excl business limits)', + businessLimited: 'business_limited:', + errors: 'Errors', + errorRate: 'error_rate:', + upstreamRate: 'upstream_rate:', + latencyDuration: 'Request Duration', + ttftLabel: 'TTFT (first_token_ms)', + p50: 'p50:', + p90: 'p90:', + p95: 'p95:', + p99: 'p99:', + avg: 'avg:', + max: 'max:', + requests: 'Запросы', + requestsTitle: 'Запросы', + upstream: 'Upstream', + client: 'Client', + system: 'System', + other: 'Other', + errorsSla: 'Errors (SLA scope)', + upstreamExcl429529: 'Upstream (excl 429/529)', + failedToLoadData: 'Failed to load ops data.', + failedToLoadOverview: 'Failed to load overview', + failedToLoadThroughputTrend: 'Failed to load throughput trend', + failedToLoadSwitchTrend: 'Failed to load avg account switches trend', + failedToLoadLatencyHistogram: 'Failed to load request duration histogram', + failedToLoadErrorTrend: 'Failed to load error trend', + failedToLoadErrorDistribution: 'Failed to load error distribution', + failedToLoadErrorDetail: 'Failed to load error detail', + retryFailed: 'Retry failed', + tpsK: 'TPS (K)', + top: 'Top:', + throughputTrend: 'Throughput Trend', + switchRateTrend: 'Avg Account Switches', + latencyHistogram: 'Request Duration Histogram', + errorTrend: 'Error Trend', + errorDistribution: 'Error Distribution', + switchRate: 'Avg switches', + // Health Score & Diagnosis + health: 'Health', + healthCondition: 'Health Condition', + healthHelp: 'Overall system health score based on SLA, error rate, and resource usage', + healthyStatus: 'Healthy', + riskyStatus: 'At Risk', + idleStatus: 'Idle', + timeRange: { + '5m': 'Last 5 minutes', + '30m': 'Last 30 minutes', + '1h': 'Last 1 hour', + '1d': 'Last 1 day', + '15d': 'Last 15 days', + '6h': 'Last 6 hours', + '24h': 'Last 24 hours', + '7d': 'Последние 7 дней', + '30d': 'Last 30 days' + }, + openaiTokenStats: { + title: 'OpenAI Token Request Stats', + viewModeTopN: 'TopN', + viewModePagination: 'Pagination', + prevPage: 'Назад', + nextPage: 'Далее', + pageInfo: 'Page {page}/{total}', + totalModels: 'Total models: {total}', + failedToLoad: 'Failed to load OpenAI token stats', + empty: 'No OpenAI token stats for the current filters', + table: { + model: 'Модель', + requestCount: 'Запросы', + avgTokensPerSec: 'Avg Tokens/sec', + avgFirstTokenMs: 'Avg First Token Latency (ms)', + totalOutputTokens: 'Total Output Tokens', + avgDurationMs: 'Avg Duration (ms)', + requestsWithFirstToken: 'Requests With First Token' + } + }, + fullscreen: { + enter: 'Enter Fullscreen' + }, + diagnosis: { + title: 'Smart Diagnosis', + footer: 'Automated diagnostic suggestions based on current metrics', + idle: 'System is currently idle', + idleImpact: 'No active traffic', + // Resource diagnostics + dbDown: 'Database connection failed', + dbDownImpact: 'All database operations will fail', + dbDownAction: 'Check database service status, network connectivity, and connection configuration', + redisDown: 'Redis connection failed', + redisDownImpact: 'Cache functionality degraded, performance may decline', + redisDownAction: 'Check Redis service status and network connectivity', + cpuCritical: 'CPU usage critically high ({usage}%)', + cpuCriticalImpact: 'System response slowing, may affect all requests', + cpuCriticalAction: 'Check CPU-intensive tasks, consider scaling or code optimization', + cpuHigh: 'CPU usage elevated ({usage}%)', + cpuHighImpact: 'System load is high, needs attention', + cpuHighAction: 'Monitor CPU trends, prepare scaling plan', + memoryCritical: 'Memory usage critically high ({usage}%)', + memoryCriticalImpact: 'May trigger OOM, system stability threatened', + memoryCriticalAction: 'Check for memory leaks, consider increasing memory or optimizing usage', + memoryHigh: 'Memory usage elevated ({usage}%)', + memoryHighImpact: 'Memory pressure is high, needs attention', + memoryHighAction: 'Monitor memory trends, check for memory leaks', + ttftHigh: 'Time to first token elevated ({ttft}ms)', + ttftHighImpact: 'User perceived latency increased', + ttftHighAction: 'Optimize request processing flow, reduce pre-processing time', + // Error rate diagnostics + upstreamCritical: 'Upstream error rate critically high ({rate}%)', + upstreamCriticalImpact: 'May affect many user requests', + upstreamCriticalAction: 'Check upstream service health, enable fallback strategies', + upstreamHigh: 'Upstream error rate elevated ({rate}%)', + upstreamHighImpact: 'Recommend checking upstream service status', + upstreamHighAction: 'Contact upstream service team, prepare fallback plan', + errorHigh: 'Error rate too high ({rate}%)', + errorHighImpact: 'Many requests failing', + errorHighAction: 'Check error logs, identify root cause, urgent fix required', + errorElevated: 'Error rate elevated ({rate}%)', + errorElevatedImpact: 'Recommend checking error logs', + errorElevatedAction: 'Analyze error types and distribution, create fix plan', + // SLA diagnostics + slaCritical: 'SLA critically below target ({sla}%)', + slaCriticalImpact: 'User experience severely degraded', + slaCriticalAction: 'Urgently investigate errors and latency, consider rate limiting', + slaLow: 'SLA below target ({sla}%)', + slaLowImpact: 'Service quality needs attention', + slaLowAction: 'Analyze SLA decline causes, optimize system performance', + // Health score diagnostics + healthCritical: 'Overall health score critically low ({score})', + healthCriticalImpact: 'Multiple metrics may be degraded; prioritize error rate and latency investigation', + healthCriticalAction: 'Comprehensive system check, prioritize critical-level issues', + healthLow: 'Overall health score low ({score})', + healthLowImpact: 'May indicate minor instability; monitor SLA and error rates', + healthLowAction: 'Monitor metric trends, prevent issue escalation', + healthy: 'All system metrics normal', + healthyImpact: 'Service running stable' + }, + // Error Log + errorLog: { + timeId: 'Time / ID', + commonErrors: { + contextDeadlineExceeded: 'context deadline exceeded', + connectionRefused: 'connection refused', + rateLimit: 'rate limit' + }, + time: 'Time', + type: 'Type', + context: 'Context', + platform: 'Platform', + model: 'Модель', + group: 'Группа', + user: 'Пользователь', + userId: 'User ID', + account: 'Account', + accountId: 'Account ID', + status: 'Статус', + message: 'Message', + latency: 'Request Duration', + action: 'Action', + noErrors: 'No errors in this window.', + grp: 'GRP:', + acc: 'ACC:', + details: 'Details', + phase: 'Phase', + id: 'ID:', + typeUpstream: 'Upstream', + typeRequest: 'Request', + typeAuth: 'Auth', + typeRouting: 'Routing', + typeInternal: 'Internal', + endpoint: 'Endpoint', + requestType: 'Type', + requestTypeSync: 'Sync', + requestTypeStream: 'Stream', + requestTypeWs: 'WS' + }, + // Error Details Modal + errorDetails: { + upstreamErrors: 'Upstream Errors', + requestErrors: 'Request Errors', + unresolved: 'Unresolved', + resolved: 'Resolved', + viewErrors: 'Errors', + viewExcluded: 'Excluded', + statusCodeOther: 'Other', + owner: { + provider: 'Provider', + client: 'Client', + platform: 'Platform' + }, + phase: { + request: 'Request', + auth: 'Auth', + routing: 'Routing', + upstream: 'Upstream', + network: 'Network', + internal: 'Internal' + }, + total: 'Total:', + searchPlaceholder: 'Search request_id / client_request_id / message', + }, + // Error Detail Modal + errorDetail: { + title: 'Error Detail', + titleWithId: 'Error #{id}', + noErrorSelected: 'No error selected.', + resolution: 'Resolved:', + failedToUpdateResolvedStatus: 'Failed to update resolved status', + classificationKeys: { + phase: 'Phase', + owner: 'Owner', + source: 'Source', + resolvedAt: 'Resolved At', + resolvedBy: 'Resolved By' + }, + source: { + upstream_http: 'Upstream HTTP' + }, + upstreamKeys: { + status: 'Статус', + message: 'Message', + detail: 'Detail', + upstreamErrors: 'Upstream Errors' + }, + upstreamEvent: { + account: 'Account', + status: 'Статус', + requestId: 'Request ID' + }, + responsePreview: { + expand: 'Response (click to expand)', + collapse: 'Response (click to collapse)' + }, + loading: 'Loading…', + requestId: 'Request ID', + time: 'Time', + phase: 'Phase', + status: 'Статус', + message: 'Message', + basicInfo: 'Basic Info', + platform: 'Platform', + model: 'Модель', + group: 'Группа', + user: 'Пользователь', + account: 'Account', + latency: 'Request Duration', + businessLimited: 'Business Limited', + requestPath: 'Request Path', + inboundEndpoint: 'Inbound Endpoint', + upstreamEndpoint: 'Upstream Endpoint', + requestedModel: 'Requested Model', + upstreamModel: 'Upstream Model', + requestType: 'Request Type', + requestTypeUnknown: 'Неизвестно', + requestTypeSync: 'Sync', + requestTypeStream: 'Stream', + requestTypeWs: 'WebSocket', + modelMapping: 'Model Mapping', + timings: 'Timings', + auth: 'Auth', + routing: 'Routing', + upstream: 'Upstream', + response: 'Response', + classification: 'Classification', + errorBody: 'Error Body', + trimmed: 'trimmed', + markResolved: 'Mark resolved', + markUnresolved: 'Mark unresolved', + tabOverview: 'Overview', + tabRequest: 'Request', + tabResponse: 'Response', + responseBody: 'Response', + compareA: 'Compare A', + compareB: 'Compare B', + suggestion: 'Suggestion', + suggestUpstream: 'Upstream instability: check account status or consider switching accounts', + suggestRequest: 'Client request error: ask customer to fix request parameters', + suggestAuth: 'Auth failed: verify API key/credentials', + suggestPlatform: 'Platform error: prioritize investigation and fix', + suggestGeneric: 'See details for more context' + }, + requestDetails: { + title: 'Request Details', + details: 'Details', + rangeLabel: 'Window: {range}', + rangeMinutes: '{n} minutes', + rangeHours: '{n} hours', + empty: 'No requests in this window.', + emptyHint: 'Try a different time range or remove filters.', + failedToLoad: 'Failed to load request details', + requestIdCopied: 'Request ID copied', + copyFailed: 'Copy failed', + copy: 'Копировать', + viewError: 'View Error', + kind: { + success: 'SUCCESS', + error: 'ERROR' + }, + table: { + time: 'Time', + kind: 'Kind', + platform: 'Platform', + model: 'Модель', + duration: 'Duration', + status: 'Статус', + requestId: 'Request ID', + actions: 'Действия' + } + }, + alertEvents: { + title: 'Alert Events', + description: 'Recent alert firing/resolution records (email-only)', + loading: 'Загрузка...', + empty: 'No alert events', + loadFailed: 'Failed to load alert events', + status: { + firing: 'FIRING', + resolved: 'RESOLVED', + manualResolved: 'MANUAL RESOLVED' + }, + detail: { + title: 'Alert Detail', + loading: 'Loading detail...', + empty: 'No detail', + loadFailed: 'Failed to load alert detail', + manualResolve: 'Mark as Resolved', + manualResolvedSuccess: 'Marked as manually resolved', + manualResolvedFailed: 'Failed to mark as manually resolved', + silence: 'Ignore Alert', + silenceSuccess: 'Alert silenced', + silenceFailed: 'Failed to silence alert', + viewRule: 'View Rule', + viewLogs: 'View Logs', + firedAt: 'Fired At', + resolvedAt: 'Resolved At', + ruleId: 'Rule ID', + dimensions: 'Dimensions', + historyTitle: 'History', + historyHint: 'Recent events with same rule + dimensions', + historyLoading: 'Loading history...', + historyEmpty: 'No history' + }, + table: { + time: 'Time', + status: 'Статус', + severity: 'Severity', + platform: 'Platform', + ruleId: 'Rule ID', + title: 'Title', + duration: 'Duration', + metric: 'Metric / Threshold', + dimensions: 'Dimensions', + email: 'Email Sent', + emailSent: 'Sent', + emailIgnored: 'Ignored' + } + }, + alertRules: { + title: 'Alert Rules', + description: 'Create and manage threshold-based system alerts (email-only)', + loading: 'Загрузка...', + empty: 'No alert rules', + loadFailed: 'Failed to load alert rules', + saveFailed: 'Failed to save alert rule', + saveSuccess: 'Alert rule saved successfully', + deleteFailed: 'Failed to delete alert rule', + deleteSuccess: 'Alert rule deleted successfully', + manage: 'Manage Alert Rules', + create: 'Create Rule', + createTitle: 'Create Alert Rule', + editTitle: 'Edit Alert Rule', + deleteConfirmTitle: 'Delete this rule?', + deleteConfirmMessage: 'This will remove the rule and its related events. Continue?', + metricGroups: { + system: 'System Metrics', + group: 'Group-level Metrics (requires group_id)', + account: 'Account-level Metrics' + }, + metrics: { + successRate: 'Success Rate (%)', + errorRate: 'Error Rate (%)', + upstreamErrorRate: 'Upstream Error Rate (%)', + p95: 'P95 Latency (ms)', + p99: 'P99 Latency (ms)', + cpu: 'CPU Usage (%)', + memory: 'Memory Usage (%)', + queueDepth: 'Concurrency Queue Depth', + groupAvailableAccounts: 'Group Available Accounts', + groupAvailableRatio: 'Group Available Ratio (%)', + groupRateLimitRatio: 'Group Rate Limit Ratio (%)', + accountRateLimitedCount: 'Rate-limited Accounts', + accountErrorCount: 'Error Accounts (excluding temporarily unschedulable)', + accountErrorRatio: 'Error Account Ratio (%)', + overloadAccountCount: 'Overloaded Accounts' + }, + metricDescriptions: { + successRate: 'Percentage of successful requests in the window (0-100).', + errorRate: 'Percentage of failed requests in the window (0-100).', + upstreamErrorRate: 'Percentage of upstream failures in the window (0-100).', + p95: 'P95 request latency within the window (ms).', + p99: 'P99 request latency within the window (ms).', + cpu: 'Current instance CPU usage (0-100).', + memory: 'Current instance memory usage (0-100).', + queueDepth: 'Concurrency queue depth within the window (queued requests).', + groupAvailableAccounts: 'Number of available accounts in the selected group (requires group_id).', + groupAvailableRatio: 'Available account ratio in the selected group (0-100, requires group_id).', + groupRateLimitRatio: 'Rate-limited account ratio in the selected group (0-100, requires group_id).', + accountRateLimitedCount: 'Number of rate-limited accounts within the window.', + accountErrorCount: 'Number of error accounts within the window (excluding temporarily unschedulable).', + accountErrorRatio: 'Error account ratio within the window (0-100).', + overloadAccountCount: 'Number of overloaded accounts within the window.' + }, + hints: { + recommended: 'Recommended: operator {operator}, threshold {threshold}{unit}', + groupRequired: 'This is a group-level metric; selecting a group (group_id) is required.', + groupOptional: 'Optional: limit the rule to a specific group via group_id.' + }, + table: { + name: 'Имя', + metric: 'Metric', + severity: 'Severity', + enabled: 'Включено', + actions: 'Действия' + }, + form: { + name: 'Имя', + description: 'Description', + metric: 'Metric', + operator: 'Operator', + groupId: 'Group (group_id)', + groupPlaceholder: 'Выберите группу', + allGroups: 'All groups', + threshold: 'Threshold', + severity: 'Severity', + window: 'Window (minutes)', + sustained: 'Sustained (samples)', + cooldown: 'Cooldown (minutes)', + enabled: 'Включено', + notifyEmail: 'Send email notifications' + }, + validation: { + title: 'Please fix the following issues', + invalid: 'Invalid rule', + nameRequired: 'Name is required', + metricRequired: 'Metric is required', + groupIdRequired: 'group_id is required for group-level metrics', + operatorRequired: 'Operator is required', + thresholdRequired: 'Threshold must be a number', + windowRange: 'Window must be one of: 1, 5, 60 minutes', + sustainedRange: 'Sustained must be between 1 and 1440 samples', + cooldownRange: 'Cooldown must be between 0 and 1440 minutes' + } + }, + runtime: { + title: 'Ops Runtime Settings', + description: 'Stored in database; changes take effect without editing config files.', + loading: 'Загрузка...', + noData: 'No runtime settings available', + loadFailed: 'Failed to load runtime settings', + saveSuccess: 'Runtime settings saved', + saveFailed: 'Failed to save runtime settings', + alertTitle: 'Alert Evaluator', + groupAvailabilityTitle: 'Group Availability Monitor', + evalIntervalSeconds: 'Evaluation Interval (seconds)', + silencing: { + title: 'Alert Silencing (Maintenance Mode)', + enabled: 'Enable silencing', + globalUntil: 'Silence until (RFC3339)', + untilHint: 'Leave empty to only toggle silencing without an expiry (not recommended).', + reason: 'Reason', + reasonPlaceholder: 'e.g., planned maintenance', + entries: { + title: 'Advanced: targeted silencing', + hint: 'Optional: silence only certain rules or severities. Leave fields empty to match all.', + add: 'Add Entry', + empty: 'No targeted entries', + entryTitle: 'Entry #{n}', + ruleId: 'Rule ID (optional)', + ruleIdPlaceholder: 'e.g., 1', + severities: 'Severities (optional)', + severitiesPlaceholder: 'e.g., P0,P1 (empty = all)', + until: 'Until (RFC3339)', + reason: 'Reason', + validation: { + untilRequired: 'Entry until time is required', + untilFormat: 'Entry until time must be a valid RFC3339 timestamp', + ruleIdPositive: 'Entry rule_id must be a positive integer', + severitiesFormat: 'Entry severities must be a comma-separated list of P0..P3' + } + }, + validation: { + timeFormat: 'Silence time must be a valid RFC3339 timestamp' + } + }, + lockEnabled: 'Distributed Lock Enabled', + lockKey: 'Distributed Lock Key', + lockTTLSeconds: 'Distributed Lock TTL (seconds)', + showAdvancedDeveloperSettings: 'Show advanced developer settings (Distributed Lock)', + advancedSettingsSummary: 'Advanced settings (Distributed Lock)', + evalIntervalHint: 'How often the evaluator runs. Keeping the default is recommended.', + validation: { + title: 'Please fix the following issues', + invalid: 'Invalid settings', + evalIntervalRange: 'Evaluation interval must be between 1 and 86400 seconds', + lockKeyRequired: 'Distributed lock key is required when lock is enabled', + lockKeyPrefix: 'Distributed lock key must start with "{prefix}"', + lockKeyHint: 'Recommended: start with "{prefix}" to avoid conflicts', + lockTtlRange: 'Distributed lock TTL must be between 1 and 86400 seconds', + slaMinPercentRange: 'SLA minimum percentage must be between 0 and 100', + ttftP99MaxRange: 'TTFT P99 maximum must be a number ≥ 0', + requestErrorRateMaxRange: 'Request error rate maximum must be between 0 and 100', + upstreamErrorRateMaxRange: 'Upstream error rate maximum must be between 0 and 100' + } + }, + email: { + title: 'Email Notification', + description: 'Configure alert/report email notifications (stored in database).', + loading: 'Загрузка...', + noData: 'No email notification config', + loadFailed: 'Failed to load email notification config', + saveSuccess: 'Email notification config saved', + saveFailed: 'Failed to save email notification config', + alertTitle: 'Alert Emails', + reportTitle: 'Report Emails', + recipients: 'Recipients', + recipientsHint: 'If empty, the system may fallback to the first admin email.', + minSeverity: 'Min Severity', + minSeverityAll: 'All severities', + rateLimitPerHour: 'Rate limit per hour', + batchWindowSeconds: 'Batch window (seconds)', + includeResolved: 'Include resolved alerts', + dailySummary: 'Daily summary', + weeklySummary: 'Weekly summary', + errorDigest: 'Error digest', + errorDigestMinCount: 'Min errors for digest', + accountHealth: 'Account health', + accountHealthThreshold: 'Error rate threshold (%)', + cronPlaceholder: 'Cron expression', + reportHint: 'Schedules use cron syntax; leave empty to use defaults.', + validation: { + title: 'Please fix the following issues', + invalid: 'Invalid email notification config', + alertRecipientsRequired: 'Alert emails are enabled but no recipients are configured', + reportRecipientsRequired: 'Report emails are enabled but no recipients are configured', + invalidRecipients: 'One or more recipient emails are invalid', + rateLimitRange: 'Rate limit per hour must be a number ≥ 0', + batchWindowRange: 'Batch window must be between 0 and 86400 seconds', + cronRequired: 'A cron expression is required when schedule is enabled', + cronFormat: 'Cron expression format looks invalid (expected at least 5 parts)', + digestMinCountRange: 'Min errors for digest must be a number ≥ 0', + accountHealthThresholdRange: 'Account health threshold must be between 0 and 100' + } + }, + settings: { + title: 'Ops Monitoring Settings', + loadFailed: 'Failed to load settings', + saveSuccess: 'Ops monitoring settings saved successfully', + saveFailed: 'Failed to save settings', + dataCollection: 'Data Collection', + evaluationInterval: 'Evaluation Interval (seconds)', + evaluationIntervalHint: 'Frequency of detection tasks, recommended to keep default', + alertConfig: 'Alert Configuration', + enableAlert: 'Enable Alerts', + alertRecipients: 'Alert Recipient Emails', + emailPlaceholder: 'Enter email address', + recipientsHint: 'If empty, the system will use the first admin email as default recipient', + minSeverity: 'Minimum Severity', + reportConfig: 'Report Configuration', + enableReport: 'Enable Reports', + reportRecipients: 'Report Recipient Emails', + dailySummary: 'Daily Summary', + weeklySummary: 'Weekly Summary', + metricThresholds: 'Metric Thresholds', + metricThresholdsHint: 'Configure alert thresholds for metrics, values exceeding thresholds will be displayed in red', + slaMinPercent: 'SLA Minimum Percentage', + slaMinPercentHint: 'SLA below this value will be displayed in red (default: 99.5%)', + ttftP99MaxMs: 'TTFT P99 Maximum (ms)', + ttftP99MaxMsHint: 'TTFT P99 above this value will be displayed in red (default: 500ms)', + requestErrorRateMaxPercent: 'Request Error Rate Maximum (%)', + requestErrorRateMaxPercentHint: 'Request error rate above this value will be displayed in red (default: 5%)', + upstreamErrorRateMaxPercent: 'Upstream Error Rate Maximum (%)', + upstreamErrorRateMaxPercentHint: 'Upstream error rate above this value will be displayed in red (default: 5%)', + advancedSettings: 'Advanced Settings', + dataRetention: 'Data Retention Policy', + enableCleanup: 'Enable Data Cleanup', + cleanupSchedule: 'Cleanup Schedule (Cron)', + cleanupScheduleHint: 'Example: 0 2 * * * means 2 AM daily', + errorLogRetentionDays: 'Error Log Retention Days', + minuteMetricsRetentionDays: 'Minute Metrics Retention Days', + hourlyMetricsRetentionDays: 'Hourly Metrics Retention Days', + retentionDaysHint: 'Recommended 7-90 days; longer periods consume more storage. Set to 0 to wipe all history on every scheduled cleanup', + aggregation: 'Pre-aggregation Tasks', + enableAggregation: 'Enable Pre-aggregation', + aggregationHint: 'Pre-aggregation improves query performance for long time windows', + errorFiltering: 'Error Filtering', + ignoreCountTokensErrors: 'Ignore count_tokens errors', + ignoreCountTokensErrorsHint: 'When enabled, errors from count_tokens requests will not be written to the error log.', + ignoreContextCanceled: 'Ignore client disconnect errors', + ignoreContextCanceledHint: 'When enabled, client disconnect (context canceled) errors will not be written to the error log.', + ignoreNoAvailableAccounts: 'Ignore no available accounts errors', + ignoreNoAvailableAccountsHint: 'When enabled, "No available accounts" errors will not be written to the error log (not recommended; usually a config issue).', + ignoreInvalidApiKeyErrors: 'Ignore invalid API key errors', + ignoreInvalidApiKeyErrorsHint: 'When enabled, invalid or missing API key errors (INVALID_API_KEY, API_KEY_REQUIRED) will not be written to the error log.', + ignoreInsufficientBalanceErrors: 'Ignore Insufficient Balance Errors', + ignoreInsufficientBalanceErrorsHint: 'When enabled, insufficient account balance errors will not be written to the error log.', + autoRefresh: 'Автообновление', + enableAutoRefresh: 'Включить автообновление', + enableAutoRefreshHint: 'Automatically refresh dashboard data at a fixed interval.', + refreshInterval: 'Refresh Interval', + refreshInterval15s: '15 seconds', + refreshInterval30s: '30 seconds', + refreshInterval60s: '60 seconds', + dashboardCards: 'Dashboard Cards', + displayAlertEvents: 'Display alert events', + displayAlertEventsHint: 'Show or hide the recent alert events card on the ops dashboard. Enabled by default.', + displayOpenAITokenStats: 'Display OpenAI token request stats', + displayOpenAITokenStatsHint: 'Show or hide the OpenAI token request stats card on the ops dashboard. Hidden by default.', + autoRefreshCountdown: 'Автообновление: {seconds}с', + validation: { + title: 'Please fix the following issues', + retentionDaysRange: 'Retention days must be between 0 and 365 (0 = wipe all on every cleanup)', + slaMinPercentRange: 'SLA minimum percentage must be between 0 and 100', + ttftP99MaxRange: 'TTFT P99 maximum must be a number ≥ 0', + requestErrorRateMaxRange: 'Request error rate maximum must be between 0 and 100', + upstreamErrorRateMaxRange: 'Upstream error rate maximum must be between 0 and 100' + } + }, + concurrency: { + title: 'Concurrency / Queue', + byPlatform: 'By Platform', + byGroup: 'By Group', + byAccount: 'By Account', + byUser: 'By User', + showByUserTooltip: 'Switch to user view to see concurrency usage per user', + switchToUser: 'Switch to user view', + switchToPlatform: 'Switch to platform view', + totalRows: '{count} rows', + disabledHint: 'Realtime monitoring is disabled in settings.', + empty: 'Нет данных', + queued: 'Queue {count}', + rateLimited: 'Rate-limited {count}', + errorAccounts: 'Errors {count}', + loadFailed: 'Failed to load concurrency data' + }, + realtime: { + title: 'Realtime', + connected: 'Realtime connected', + connecting: 'Realtime connecting', + reconnecting: 'Realtime reconnecting', + offline: 'Realtime offline', + closed: 'Realtime closed', + reconnectIn: 'retry in {seconds}s' + }, + queryMode: { + auto: 'Auto', + raw: 'Raw', + preagg: 'Preagg' + }, + accountAvailability: { + available: 'Доступно', + unavailable: 'Unavailable', + accountError: 'Ошибка' + }, + tooltips: { + totalRequests: 'Total number of requests (including both successful and failed requests) in the selected time window.', + throughputTrend: 'Requests/QPS + Tokens/TPS in the selected window.', + switchRateTrend: 'Trend of account switches / total requests over the last 5 hours (avg switches).', + latencyHistogram: 'Request duration distribution (ms) for successful requests.', + errorTrend: 'Error counts over time (SLA scope excludes business limits; upstream excludes 429/529).', + errorDistribution: 'Error distribution by status code (SLA scope, excluding business limits).', + goroutines: + 'Number of Go runtime goroutines (lightweight threads). There is no absolute "safe" number—use your historical baseline. Heuristic: <2k is common; 2k–8k watch; >8k plus rising queue/latency often suggests blocking/leaks.', + cpu: 'CPU usage percentage, showing system processor load.', + memory: 'Memory usage, including used and total available memory.', + db: 'Database connection pool status, including active, idle, and waiting connections.', + redis: 'Redis connection pool status, showing active and idle connections.', + jobs: 'Background job execution status, including last run time, success time, and error information.', + qps: 'Queries Per Second (QPS) and Tokens Per Second (TPS), real-time system throughput.', + tokens: 'Total number of tokens processed in the current time window.', + sla: 'Service Level Agreement success rate, excluding business limits (e.g., insufficient balance, quota exceeded).', + errors: 'Error statistics, including total errors, error rate, and upstream error rate.', + upstreamErrors: 'Upstream error statistics, excluding rate limit errors (429/529).', + latency: 'Request duration statistics, including p50, p90, p95, p99 percentiles.', + ttft: 'Time To First Token, measuring the speed of first token return in streaming responses.', + health: 'System health score (0-100), considering SLA, error rate, and resource usage.' + }, + charts: { + emptyRequest: 'No requests in this window.', + emptyError: 'No errors in this window.', + resetZoom: 'Сбросить', + resetZoomHint: 'Reset zoom (if enabled)', + downloadChart: 'Download', + downloadChartHint: 'Download chart as image' + } + }, + + // Settings + settings: { + title: 'System Settings', + description: 'Manage registration, email verification, default values, and SMTP settings', + tabs: { + general: 'General', + agreement: 'Agreement', + features: 'Feature Switches', + security: 'Security', + users: 'Пользователи', + gateway: 'Gateway', + email: 'Email', + backup: 'Backup', + payment: 'Payment', + }, + features: { + channelMonitor: { + title: 'Монитор каналов', + description: 'Periodically probe configured channels and surface availability / latency to users. Turning it off stops the scheduler and returns an empty list on the user page.', + configureLink: 'Configure monitors in Channel Management > Channel Monitor', + enabled: 'Enable Channel Monitor', + enabledHint: 'Disabling stops background checks; existing history is preserved.', + defaultInterval: 'Default check interval (seconds)', + defaultIntervalHint: 'Pre-fills the interval when creating a new monitor; each monitor can override it. Range 15 – 3600.', + }, + availableChannels: { + title: 'Доступные каналы', + description: 'Show logged-in users an aggregate view of the channels, models and pricing they can access. Disabled by default.', + configureLink: 'Configure model pricing in Channel Management > Channel Pricing', + enabled: 'Enable Available Channels', + enabledHint: 'When off, the sidebar entry is hidden and the endpoint returns an empty list.', + }, + riskControl: { + title: 'Риск-контроль', + description: 'Enable the content moderation menu and gateway audit entry point. Disabled by default.', + configureLink: 'Configure content moderation in Risk Control', + enabled: 'Enable Risk Control', + enabledHint: 'When off, the admin sidebar entry is hidden and gateway moderation is skipped.', + }, + affiliate: { + title: 'Affiliate (Invite Rebate)', + description: 'Existing users invite new ones; the inviter earns a percentage rebate on the invitee’s recharges. Disabled by default.', + enabled: 'Enable Affiliate', + enabledHint: 'When off, the affiliate menu is hidden, the aff parameter is ignored at signup, and new recharges generate no rebate. Existing rebate balances can still be transferred.', + rebateRate: 'Global Rebate Rate', + rebateRateHint: 'Default percentage given back to the inviter on recharges (0-100, e.g. 10 = 10%).', + freezeHours: 'Rebate Freeze Period (hours)', + freezeHoursDesc: 'New rebates will be frozen for this period before becoming available for withdrawal. 0 = no freeze.', + durationDays: 'Rebate Duration (days)', + durationDaysDesc: 'Rebate relationship expires after this many days since invitee registration. 0 = permanent.', + perInviteeCap: 'Per-Invitee Rebate Cap', + perInviteeCapDesc: 'Maximum total rebate from a single invitee. 0 = no limit.', + customUsers: { + title: 'Per-User Overrides', + description: 'Set a custom invite code or exclusive rebate rate for specific users. Lists only users that have an override applied.', + addButton: 'Add Custom User', + searchPlaceholder: 'Search by email or username', + batchButton: 'Batch Set Rate ({count} selected)', + empty: 'No users with custom affiliate settings yet', + customBadge: 'custom', + useGlobal: 'use global', + resetTitle: 'Reset Custom Settings', + resetMessage: 'Reset all custom settings for {email}?\n• The exclusive rebate rate will be cleared (fall back to the global rate)\n• The invite code will be regenerated as a new system code (previously shared links will stop working)', + totalLabel: '{total} total', + col: { + email: 'Email', + username: 'Имя пользователя', + code: 'Invite Code', + rate: 'Custom Rate', + actions: 'Действия', + }, + }, + modal: { + addTitle: 'Add Custom User', + editTitle: 'Edit Custom Settings', + userLabel: 'Пользователь', + userPlaceholder: 'Search by email or username', + changeUser: 'Change user', + codeLabel: 'Custom Invite Code (optional)', + codePlaceholder: 'e.g. VIP2026', + codeHint: '4-32 characters; A-Z, 0-9, underscore, dash. Leave empty to keep current. Input is upper-cased.', + rateLabel: 'Exclusive Rebate Rate (optional)', + ratePlaceholder: 'e.g. 30', + rateHint: '0-100. Leave empty (in edit mode) to clear and fall back to the global rate.', + errorBadRate: 'Please enter a number between 0 and 100', + errorEmpty: 'Fill at least one: custom invite code or exclusive rebate rate', + }, + batchModal: { + title: 'Batch Set Rate ({count} users selected)', + hint: 'Apply the same exclusive rebate rate to all selected users.', + placeholder: 'e.g. 30', + clearHint: 'Submitting empty will clear the exclusive rate for selected users.', + }, + }, + }, + emailTabDisabledTitle: 'Email Verification Not Enabled', + emailTabDisabledHint: 'Enable email verification in the Security tab to configure SMTP settings.', + registration: { + title: 'Registration Settings', + description: 'Control user registration and verification', + enableRegistration: 'Enable Registration', + enableRegistrationHint: 'Allow new users to register', + emailVerification: 'Email Verification', + emailVerificationHint: 'Require email verification for new registrations', + emailSuffixWhitelist: 'Email Domain Whitelist', + emailSuffixWhitelistHint: + "Only email addresses from the specified domains can register (for example, {'@'}qq.com, {'@'}gmail.com, *.edu.cn)", + emailSuffixWhitelistPlaceholder: "{'@'}example.com, *.edu.cn", + emailSuffixWhitelistInputHint: 'Leave empty for no restriction. Use *.edu.cn to match edu.cn and its subdomains.', + promoCode: 'Промокод', + promoCodeHint: 'Allow users to use promo codes during registration', + invitationCode: 'Invitation Code Registration', + invitationCodeHint: 'When enabled, users must enter a valid invitation code to register', + passwordReset: 'Password Reset', + passwordResetHint: 'Allow users to reset their password via email', + frontendUrl: 'Frontend URL', + frontendUrlPlaceholder: 'https://example.com', + frontendUrlHint: 'Used to generate password reset links in emails. Example: https://example.com', + totp: 'Двухфакторная аутентификация (2FA)', + totpHint: 'Allow users to use authenticator apps like Google Authenticator', + totpKeyNotConfigured: + 'Please configure TOTP_ENCRYPTION_KEY in environment variables first. Generate a key with: openssl rand -hex 32' + }, + turnstile: { + title: 'Cloudflare Turnstile', + description: 'Bot protection for login and registration', + enableTurnstile: 'Enable Turnstile', + enableTurnstileHint: 'Require Cloudflare Turnstile verification', + siteKey: 'Site Key', + secretKey: 'Secret Key', + siteKeyHint: 'Get this from your Cloudflare Dashboard', + cloudflareDashboard: 'Cloudflare Dashboard', + secretKeyHint: 'Server-side verification key (keep this secret)', + secretKeyConfiguredHint: 'Secret key configured. Leave empty to keep the current value.' + }, + apiKeyAcl: { + title: 'API Key IP Access Control', + description: 'Choose which client IP is used by API Key allowlists and denylists', + trustForwardedIp: 'Trust forwarded client IP', + trustForwardedIpHint: + 'Disabled by default. Enable only when the origin is reachable only through Cloudflare or Nginx reverse proxy. When enabled, API Key IP allowlists and denylists use CF-Connecting-IP, X-Real-IP, or X-Forwarded-For, matching the request IP shown in usage records.' + }, + linuxdo: { + title: 'LinuxDo Connect Login', + description: 'Configure LinuxDo Connect OAuth for Sub2API end-user login', + enable: 'Enable LinuxDo Login', + enableHint: 'Show LinuxDo login on the login/register pages', + clientId: 'Client ID', + clientIdPlaceholder: 'e.g., hprJ5pC3...', + clientIdHint: 'Get this from Connect.Linux.Do', + clientSecret: 'Client Secret', + clientSecretPlaceholder: '********', + clientSecretHint: 'Used by backend to exchange tokens (keep it secret)', + clientSecretConfiguredPlaceholder: '********', + clientSecretConfiguredHint: 'Secret configured. Leave empty to keep the current value.', + redirectUrl: 'Redirect URL', + redirectUrlPlaceholder: 'https://your-domain.com/api/v1/auth/oauth/linuxdo/callback', + redirectUrlHint: + 'Must match the redirect URL configured in Connect.Linux.Do (must be an absolute http(s) URL)', + quickSetCopy: 'Generate & Copy (current site)', + redirectUrlSetAndCopied: 'Redirect URL generated and copied to clipboard' + }, + dingtalk: { + title: 'DingTalk Login', + description: 'Configure DingTalk OAuth for Sub2API end-user login', + enable: 'Enable DingTalk Login (Internal Corporate App)', + enableHint: 'Show DingTalk login on the login/register pages', + clientId: 'Client ID (AppKey)', + clientIdPlaceholder: 'e.g., dingxxxxxxxxxxxxxxxx', + clientIdHint: 'Get this from the DingTalk Open Platform app details', + clientSecret: 'Client Secret (AppSecret)', + clientSecretPlaceholder: '********', + clientSecretHint: 'Used by backend to exchange tokens (keep it secret)', + clientSecretConfiguredPlaceholder: '********', + clientSecretConfiguredHint: 'Secret configured. Leave empty to keep the current value.', + redirectUrl: 'Redirect URL', + redirectUrlPlaceholder: 'https://your-domain.com/api/v1/auth/oauth/dingtalk/callback', + redirectUrlHint: + 'Must match the redirect URL configured in DingTalk Open Platform (must be an absolute http(s) URL)', + corpPolicy: { + label: 'Corp Restriction Policy', + hint: 'Control which DingTalk accounts (orgs) are allowed to sign in', + none: 'No restriction (all DingTalk accounts allowed)', + internalOnly: 'Internal only (single corp)' + }, + bypassRegistration: 'Enable DingTalk signup', + bypassRegistrationHint: 'Allow new users to register via DingTalk even when public registration is disabled.', + syncDisplayName: 'Sync DingTalk display name', + syncDisplayNameHint: 'Overwrite username with the DingTalk staff name on each login (also stored in the dingtalk_name attribute).', + syncCorpEmail: 'Sync corporate email', + syncCorpEmailHint: 'Write the DingTalk corporate email to the dingtalk_email attribute on each login (does not change the login email).', + syncCorpEmailPermissionHint: 'Requires the OAPI permission "Personal info incl. email (fieldEmail)" to be granted to the app on the DingTalk open platform, otherwise OAPI will not return the email field.', + syncDept: 'Sync department', + syncDeptHint: 'Write the full DingTalk department path to the dingtalk_department attribute on each login (fetched live each time).', + syncDeptPermissionHint: 'Requires the OAPI "Department info read (qyapi_get_department_list)" permission to be granted to the app on the DingTalk open platform, otherwise the department path cannot be resolved.', + syncDisplayNameTarget: 'Attribute key', + syncDisplayNameTargetHint: 'Defaults to dingtalk_name / DingTalk Name. Saving settings auto-creates the user attribute by the key and display name above (existing definition only has its display name synced).', + syncCorpEmailTarget: 'Attribute key', + syncCorpEmailTargetHint: 'Defaults to dingtalk_email / DingTalk Corporate Email. Saving settings auto-creates the user attribute by the key and display name above (existing definition only has its display name synced).', + syncDeptTarget: 'Attribute key', + syncDeptTargetHint: 'Defaults to dingtalk_department / DingTalk Department. Saving settings auto-creates the user attribute by the key and display name above (existing definition only has its display name synced).', + syncAttrDisplayName: 'Display name' + }, + oidc: { + title: 'OIDC Login', + description: 'Configure a standard OIDC provider (for example Keycloak)', + enable: 'Enable OIDC Login', + enableHint: 'Show OIDC login on the login/register pages', + providerName: 'Provider Name', + providerNamePlaceholder: 'for example Keycloak', + clientId: 'Client ID', + clientIdPlaceholder: 'OIDC client id', + clientSecret: 'Client Secret', + clientSecretPlaceholder: '********', + clientSecretHint: 'Used by backend to exchange tokens (keep it secret)', + clientSecretConfiguredPlaceholder: '********', + clientSecretConfiguredHint: 'Secret configured. Leave empty to keep the current value.', + issuerUrl: 'Issuer URL', + issuerUrlPlaceholder: 'https://id.example.com/realms/main', + discoveryUrl: 'Discovery URL', + discoveryUrlPlaceholder: 'Optional, leave empty to auto-derive from issuer', + authorizeUrl: 'Authorize URL', + authorizeUrlPlaceholder: 'Optional, can be discovered automatically', + tokenUrl: 'Token URL', + tokenUrlPlaceholder: 'Optional, can be discovered automatically', + userinfoUrl: 'UserInfo URL', + userinfoUrlPlaceholder: 'Optional, can be discovered automatically', + jwksUrl: 'JWKS URL', + jwksUrlPlaceholder: 'Optional, required when strict ID token validation is enabled', + scopes: 'Scopes', + scopesPlaceholder: 'openid email profile', + scopesHint: 'Must include openid', + redirectUrl: 'Backend Redirect URL', + redirectUrlPlaceholder: 'https://your-domain.com/api/v1/auth/oauth/oidc/callback', + redirectUrlHint: 'Must match the callback URL configured in the OIDC provider', + quickSetCopy: 'Generate & Copy (current site)', + redirectUrlSetAndCopied: 'Redirect URL generated and copied to clipboard', + frontendRedirectUrl: 'Frontend Callback Path', + frontendRedirectUrlPlaceholder: '/auth/oidc/callback', + frontendRedirectUrlHint: 'Frontend route used after backend callback', + tokenAuthMethod: 'Token Auth Method', + clockSkewSeconds: 'Clock Skew (seconds)', + allowedSigningAlgs: 'Allowed Signing Algs', + allowedSigningAlgsPlaceholder: 'RS256,ES256,PS256', + usePkce: 'Use PKCE', + validateIdToken: 'Validate ID Token', + requireEmailVerified: 'Require Email Verified', + userinfoEmailPath: 'UserInfo Email Path', + userinfoEmailPathPlaceholder: 'for example data.email', + userinfoIdPath: 'UserInfo ID Path', + userinfoIdPathPlaceholder: 'for example data.id', + userinfoUsernamePath: 'UserInfo Username Path', + userinfoUsernamePathPlaceholder: 'for example data.username' + }, + defaults: { + title: 'Default User Settings', + description: 'Default values for new users', + defaultBalance: 'Default Balance', + defaultBalanceHint: 'Initial balance for new users', + affiliateRebateRate: 'Affiliate Rebate Rate', + affiliateRebateRateHint: + 'Rebate percentage credited to inviter after recharge (0-100%, e.g. 10 means 10%)', + defaultConcurrency: 'Default Concurrency', + defaultConcurrencyHint: 'Maximum concurrent requests for new users', + defaultUserRpmLimit: 'Default User RPM Limit', + defaultUserRpmLimitHint: 'Default max requests per minute for new users; 0 = unlimited. Only applied at new user creation.', + defaultSubscriptions: 'Default Subscriptions', + defaultSubscriptionsHint: 'Auto-assign these subscriptions when a new user is created or registered', + addDefaultSubscription: 'Add Default Subscription', + defaultSubscriptionsEmpty: 'No default subscriptions configured.', + defaultSubscriptionsDuplicate: + 'Duplicate subscription group: {groupId}. Each group can only appear once.', + subscriptionGroup: 'Subscription Group', + subscriptionValidityDays: 'Validity (days)', + defaultPlatformQuotas: 'Default Platform Quotas (on signup)', + defaultPlatformQuotasHint: 'Automatically assigned to new users on signup; existing users are not affected. Leave blank = unlimited.', + platformQuotaNotice: 'Monthly quota uses a 30-day rolling window, not a calendar month.', + }, + platformQuota: { + platform: 'Platform', + daily: 'Daily (USD)', + weekly: 'Weekly (USD)', + monthly: 'Monthly (USD, 30d rolling)', + placeholder: 'Безлимитно', + }, + claudeCode: { + title: 'Claude Code Settings', + description: 'Control Claude Code client access requirements', + minVersion: 'Minimum Version', + minVersionPlaceholder: 'e.g. 2.1.63', + minVersionHint: + 'Reject Claude Code clients below this version (semver format). Leave empty to disable version check.', + maxVersion: 'Maximum Version', + maxVersionPlaceholder: 'e.g. 2.5.0', + maxVersionHint: + 'Reject Claude Code clients above this version (semver format). Leave empty to allow any version.' + }, + scheduling: { + title: 'Gateway Scheduling Settings', + description: 'Control API Key scheduling behavior', + allowUngroupedKey: 'Allow Ungrouped Key Scheduling', + allowUngroupedKeyHint: 'When disabled, API Keys not assigned to any group cannot make requests (403 Forbidden). Keep disabled to ensure all Keys belong to a specific group.' + }, + gatewayForwarding: { + title: 'Request Forwarding', + description: 'Control how requests are forwarded to upstream OAuth accounts', + fingerprintUnification: 'Fingerprint Unification', + fingerprintUnificationHint: 'Unify X-Stainless-* headers across users sharing the same OAuth account. Disabling passes through each client\'s original headers.', + metadataPassthrough: 'Metadata Passthrough', + metadataPassthroughHint: 'Pass through client\'s original metadata.user_id without rewriting. May improve upstream cache hit rates.', + cchSigning: 'CCH Signing', + cchSigningHint: 'Sign the billing header in forwarded requests with CCH hash. When disabled, the placeholder is preserved.', + anthropicCacheTTL1hInjection: 'Anthropic Cache TTL Injection', + anthropicCacheTTL1hInjectionHint: 'When enabled, existing ephemeral cache_control blocks in Anthropic OAuth/Setup Token request bodies are forced to 1h; response usage is billed back as 5m by default, with account-level TTL billing override taking priority.', + rewriteMessageCacheControl: 'Rewrite Message Cache Breakpoints', + rewriteMessageCacheControlHint: 'Default off: preserve client cache_control on message content blocks. When enabled, client breakpoints are stripped and proxy breakpoints are injected for clients that do not manage caching themselves.', + antigravityUserAgentVersion: 'Antigravity UA Version', + antigravityUserAgentVersionPlaceholder: '1.23.2', + antigravityUserAgentVersionHint: 'Leave empty to use ANTIGRAVITY_USER_AGENT_VERSION or the built-in default 1.23.2; when set, the admin setting takes precedence.', + openaiCodexUserAgent: 'OpenAI Codex UA', + openaiCodexUserAgentPlaceholder: 'codex-tui/0.125.0 (Ubuntu 22.4.0; x86_64) xterm-256color (codex-tui; 0.125.0)', + openaiCodexUserAgentHint: 'Used to bypass Cloudflare browser-UA challenges on the OpenAI upstream. Only applies when the client User-Agent is detected as a browser (Mozilla/...). Leave empty to use the built-in default.', + }, + webSearchEmulation: { + title: 'Web Search Emulation', + description: 'Inject web search capability for Anthropic API Key accounts that don\'t natively support it', + enabled: 'Enable Web Search Emulation', + enabledHint: 'Global switch. When disabled, web search emulation is inactive for all channels and accounts.', + providers: 'Search Providers', + addProvider: 'Add Provider', + providerType: 'Provider Type', + apiKey: 'API-ключ', + apiKeyPlaceholder: 'Enter API Key', + apiKeyConfigured: 'Configured', + showApiKey: 'Show', + hideApiKey: 'Hide', + copyApiKey: 'Копировать', + copied: 'Скопировано', + quotaLimit: 'Quota Limit', + quotaLimitHint: 'Leave empty for unlimited; must be > 0 if set', + quotaLimitMustBePositive: 'Quota limit must be greater than 0', + subscribedAt: 'Subscribed At', + subscribedAtHint: 'Quota resets monthly from this date; leave empty to disable auto-reset', + quotaUsage: 'Расход', + resetUsage: 'Сбросить', + resetUsageConfirm: 'Reset usage counter for this provider?', + resetUsageSuccess: 'Usage counter reset', + proxy: 'Proxy', + removeProvider: 'Remove', + noProviders: 'No search providers configured', + test: 'Test', + testDefaultQuery: 'Major world events this year', + testing: 'Searching...', + testResultTitle: 'Search Results', + testResultProvider: 'Provider', + testNoResults: 'No results found', + }, + site: { + title: 'Site Settings', + description: 'Customize site branding', + backendMode: 'Backend Mode', + backendModeDescription: + 'Disables user registration, public site, and self-service features. Only admin can log in and manage the platform.', + siteName: 'Site Name', + siteNamePlaceholder: 'Sub2API', + siteNameHint: 'Displayed in emails and page titles', + siteSubtitle: 'Site Subtitle', + siteSubtitlePlaceholder: 'Subscription to API Conversion Platform', + siteSubtitleHint: 'Displayed on login and register pages', + apiBaseUrl: 'API Base URL', + apiBaseUrlPlaceholder: 'https://api.example.com', + apiBaseUrlHint: + 'Used for "Use Key" and "Import to CC Switch" features. Leave empty to use current site URL.', + tablePreferencesTitle: 'Global Table Preferences', + tablePreferencesDescription: 'Configure default pagination behavior for shared table components', + tableDefaultPageSize: 'Default Rows Per Page', + tableDefaultPageSizeHint: 'Must be an integer between 5 and 1000', + tablePageSizeOptions: 'Rows Per Page Options', + tablePageSizeOptionsPlaceholder: '10, 20, 50, 100', + tablePageSizeOptionsHint: 'Use commas to separate integers between 5 and 1000; values are deduplicated and sorted on save', + tableDefaultPageSizeRangeError: 'Default rows per page must be between {min} and {max}', + tablePageSizeOptionsFormatError: 'Invalid options format. Enter comma-separated integers between {min} and {max}', + customEndpoints: { + title: 'Custom Endpoints', + description: 'Add additional API endpoint URLs for users to quickly copy on the API Keys page', + itemLabel: 'Endpoint #{n}', + name: 'Имя', + namePlaceholder: 'e.g., OpenAI Compatible', + endpointUrl: 'Endpoint URL', + endpointUrlPlaceholder: 'https://api2.example.com', + descriptionLabel: 'Description', + descriptionPlaceholder: 'e.g., Supports OpenAI format requests', + add: 'Add Endpoint', + }, + contactInfo: 'Contact Info', + contactInfoPlaceholder: 'e.g., QQ: 123456789', + contactInfoHint: 'Customer support contact info, displayed on redeem page, profile, etc.', + docUrl: 'Documentation URL', + docUrlPlaceholder: 'https://docs.example.com', + docUrlHint: 'Link to your documentation site. Leave empty to hide the documentation link.', + siteLogo: 'Site Logo', + uploadImage: 'Upload Image', + remove: 'Remove', + logoHint: 'PNG, JPG, or SVG. Max 300KB. Recommended: 80x80px square image.', + logoSizeError: 'Image size exceeds 300KB limit ({size}KB)', + logoTypeError: 'Please select an image file', + logoReadError: 'Failed to read the image file', + homeContent: 'Home Page Content', + homeContentPlaceholder: 'Enter custom content for the home page. Supports Markdown & HTML. If a URL is entered, it will be displayed as an iframe.', + homeContentHint: 'Customize the home page content. Supports Markdown/HTML. If you enter a URL (starting with http:// or https://), it will be used as an iframe src to embed an external page. When set, the default status information will no longer be displayed.', + homeContentIframeWarning: '⚠️ iframe mode note: Some websites have X-Frame-Options or CSP security policies that prevent embedding in iframes. If the page appears blank or shows an error, please verify the target website allows embedding, or consider using HTML mode to build your own content.', + hideCcsImportButton: 'Hide CCS Import Button', + hideCcsImportButtonHint: 'When enabled, the "Import to CCS" button will be hidden on the API Keys page' + }, + purchase: { + title: 'Recharge / Subscription Page', + description: 'Show a "Пополнение / подписка" entry in the sidebar and open the configured URL in an iframe', + enabled: 'Show Recharge / Subscription Entry', + enabledHint: 'Only shown in standard mode (not simple mode)', + url: 'Recharge / Subscription URL', + urlPlaceholder: 'https://example.com/purchase', + urlHint: 'Must be an absolute http(s) URL', + iframeWarning: + '⚠️ iframe note: Some websites block embedding via X-Frame-Options or CSP (frame-ancestors). If the page is blank, provide an "Открыть в новой вкладке" alternative.', + integrationDoc: 'Payment Integration Docs', + integrationDocHint: 'Covers endpoint specs, idempotency semantics, and code samples' + }, + soraClient: { + title: 'Sora Client', + description: 'Control whether to show the Sora client entry in the sidebar', + enabled: 'Enable Sora Client', + enabledHint: 'When enabled, the Sora entry will be shown in the sidebar for users to access Sora features' + }, + customMenu: { + title: 'Custom Menu Pages', + description: 'Add custom iframe pages to the sidebar navigation. Each page can be visible to regular users or administrators.', + itemLabel: 'Menu Item #{n}', + name: 'Menu Name', + namePlaceholder: 'e.g. Help Center', + url: 'Page URL', + urlPlaceholder: 'https://example.com/page', + iconSvg: 'SVG Icon', + iconSvgPlaceholder: '...', + iconPreview: 'Icon Preview', + uploadSvg: 'Upload SVG', + removeSvg: 'Remove', + visibility: 'Visible To', + visibilityUser: 'Regular Users', + visibilityAdmin: 'Administrators', + add: 'Add Menu Item', + remove: 'Remove', + moveUp: 'Move Up', + moveDown: 'Move Down', + }, + payment: { + title: 'Payment Settings', + description: 'Configure payment system options', + configGuide: 'Configuration Guide', + enabled: 'Enable Payment', + enabledHint: 'Enable or disable the payment system', + enabledPaymentTypes: 'Enabled Providers', + enabledPaymentTypesHint: 'Disabling a provider will also disable its instances.', + findProvider: 'Looking for a suitable EasyPay provider?', + minAmount: 'Minimum Amount', + maxAmount: 'Maximum Amount', + dailyLimit: 'Дневной лимит', + balanceRechargeMultiplier: 'Balance Recharge Multiplier', + balanceRechargeMultiplierHint: 'How many USD balance the user receives for each 1 CNY paid', + balanceRechargePreview: 'Preview: 1 CNY = {usd} USD', + rechargeFeeRate: 'Recharge Fee Rate', + rechargeFeeRateHint: 'Percentage of service fee charged on top of recharge amount, 0 means no fee', + rechargeFeePreview: 'Preview: Recharge 100, fee {fee}', + orderTimeout: 'Order Timeout', + orderTimeoutHint: 'In minutes, minimum 1', + maxPendingOrders: 'Max Pending Orders', + cancelRateLimit: 'Limit Cancel Rate', + cancelRateLimitHint: 'When enabled, users who exceed the cancel limit within the time window cannot create new orders', + cancelRateLimitEvery: 'Every', + cancelRateLimitAllowMax: 'allow max', + cancelRateLimitTimes: 'cancels', + cancelRateLimitWindow: 'Window', + cancelRateLimitUnit: 'Unit', + cancelRateLimitMax: 'Max Cancels', + cancelRateLimitUnitMinute: 'Minutes', + cancelRateLimitUnitHour: 'Hours', + cancelRateLimitUnitDay: 'Days', + cancelRateLimitWindowMode: 'Window Mode', + cancelRateLimitWindowModeRolling: 'Rolling', + cancelRateLimitWindowModeFixed: 'Fixed', + alipayForceQRCode: 'Force Alipay QR Code', + alipayForceQRCodeHint: 'When enabled, mobile Alipay users always see a QR code instead of being redirected to the mobile payment page', + helpText: 'Help Text', + helpImageUrl: 'Help Image URL', + manageProviders: 'Manage Providers', + balancePaymentDisabled: 'Disable Balance Recharge', + noLimit: 'Empty = no limit', + helpImage: 'Help Image', + helpImagePlaceholder: 'Upload or enter image URL', + helpTextPlaceholder: 'Enter help text...', + providerEasypay: 'EasyPay', + providerAlipay: 'Alipay (Direct)', + providerWxpay: 'WeChat Pay (Direct)', + providerStripe: 'Stripe', + providerAirwallex: 'Airwallex', + typeDisabled: 'type disabled', + enableTypesFirst: 'Enable at least one payment type above first', + easypayRedirect: 'Redirect', + paymentMode: 'Payment Mode', + modeRedirect: 'Redirect', + modeQRCode: 'QR Code', + modePopup: 'Popup', + validationNameRequired: 'Provider name is required', + validationTypesRequired: 'Please select at least one supported payment type', + validationFieldRequired: '{field} is required', + field_apiBase: 'API Base URL', + field_notifyUrl: 'Notify URL', + field_returnUrl: 'Return URL', + callbackBaseUrl: 'Callback Base URL', + field_privateKey: 'Private Key', + field_publicKey: 'Public Key', + field_mpAppId: 'MP App ID', + field_mchId: 'Merchant ID', + field_apiV3Key: 'API v3 Key', + field_publicKeyId: 'Public Key ID', + field_certSerial: 'Certificate Serial', + field_h5AppName: 'H5 App Name', + field_h5AppUrl: 'H5 App URL', + wxpayConfigHint: 'WeChat Pay usually only needs App ID. Fill MP App ID, H5 App Name, and H5 App URL only when your Official Account or H5 flow specifically requires them.', + wxpayAdvancedOptions: 'WeChat Pay Advanced Options', + field_secretKey: 'Secret Key', + field_clientId: 'Client ID', + field_apiKey: 'API-ключ', + field_publishableKey: 'Publishable Key', + field_webhookSecret: 'Webhook Secret', + field_countryCode: 'Country/region code', + field_currency: 'Payment currency', + field_accountId: 'Airwallex Account ID', + field_airwallexApiBaseHint: 'Must match the API key environment: use https://api-demo.airwallex.com/api/v1 for sandbox/demo keys, and https://api.airwallex.com/api/v1 for production keys. Mixed environments return credentials_invalid / Access Denied.', + field_paymentCurrencyHint: 'Default is CNY. Stripe and Airwallex can choose HKD, USD, or another listed currency supported by the account; WeChat Pay, Alipay, and EasyPay remain CNY.', + field_accountIdHint: 'Leave this empty unless you use multiple accounts, an organization-level key, or connected-account payments. A single-account scoped API key uses the selected account by default.', + field_cid: 'Channel ID', + field_cidAlipay: 'Alipay Channel ID', + field_cidWxpay: 'WeChat Channel ID', + stripeWebhookHint: 'Configure the following URL as a Webhook endpoint in Stripe Dashboard:', + stripeWebhookApiVersionHint: 'Set this Webhook endpoint API version to match the integrated Stripe SDK. Recommended: {version}. A mismatch can cause webhook parsing errors.', + airwallexWebhookHint: 'Configure the following URL as a Webhook endpoint in Airwallex. Select at least Payment Intent -> Succeeded (payment_intent.succeeded), preferably also Payment Intent -> Cancelled (payment_intent.cancelled). Use the account default or latest stable API version.', + airwallexGuideSummary: 'When creating an Airwallex scoped API key, select Read and Write for Payment Acceptance under account-level permissions.', + airwallexGuideNote: 'Do not grant unrelated permissions such as Spend, Payouts, Transfers, Funds Splits, or POS Terminals unless you explicitly need them. For webhooks, select at least payment_intent.succeeded, preferably also payment_intent.cancelled, and use the account default or latest stable API version.', + limitsTitle: 'Limits', + limitSingleMin: 'Min per order', + limitSingleMax: 'Max per order', + limitDaily: 'Daily limit', + limitsHint: 'All empty = use global config; partially filled = empty means no limit', + limitsUseGlobal: 'Use global', + limitsNoLimit: 'No limit', + productNamePrefix: 'Product Name Prefix', + productNameSuffix: 'Product Name Suffix', + preview: 'Preview', + loadBalanceStrategy: 'Load Balance Strategy', + strategyRoundRobin: 'Round Robin', + strategyLeastAmount: 'Least Daily Amount', + providerManagement: 'Provider Management', + providerManagementDesc: 'Manage payment provider instances', + createProvider: 'Add Provider', + editProvider: 'Edit Provider', + deleteProvider: 'Delete Provider', + deleteProviderConfirm: 'Are you sure you want to delete this provider?', + providerName: 'Provider Name', + providerKey: 'Provider Type', + selectProviderKey: 'Select Provider Type', + providerConfig: 'Credentials', + paymentGuideTrigger: 'View payment guide', + guideOpenLabel: 'Enable: ', + guideCallLabel: 'Call: ', + guideFallbackLabel: 'Fallback: ', + alipayGuideSummary: 'Desktop prefers QR precreate and falls back to cashier; mobile prefers WAP checkout.', + alipayGuideFaceToFaceTitle: 'Face-to-face / QR Payment', + alipayGuideFaceToFaceOpen: 'Enable face-to-face or QR payment capability.', + alipayGuideFaceToFaceCall: 'Desktop orders call alipay.trade.precreate first and render the QR code directly.', + alipayGuideFaceToFaceFallback: 'If unavailable or failed, the flow falls back to website checkout automatically.', + alipayGuidePagePayTitle: 'Website Payment', + alipayGuidePagePayOpen: 'Enable website payment.', + alipayGuidePagePayCall: 'When face-to-face is unavailable on desktop, the flow calls alipay.trade.page.pay and still renders the returned link as a QR code.', + alipayGuidePagePayFallback: 'The cashier link stays available so users can reopen the checkout page manually.', + alipayGuideWapTitle: 'WAP Payment', + alipayGuideWapOpen: 'Enable mobile website payment.', + alipayGuideWapCall: 'Mobile orders call alipay.trade.wap.pay first and jump to Alipay checkout.', + alipayGuideWapFallback: 'If mobile payment is unavailable or fails, the frontend switches to QR payment and shows a notice.', + wxpayGuideSummary: 'Desktop prefers Native QR; mobile routes to JSAPI or H5 based on browser context.', + wxpayGuideNote: 'The current form defaults to one shared App ID, which fits the common single-subject web, mobile, and Official Account setup.', + wxpayGuideNativeTitle: 'Native / QR Payment', + wxpayGuideNativeOpen: 'Enable Native or QR payment capability.', + wxpayGuideNativeCall: 'Desktop orders use Native by default and the frontend renders the QR payload.', + wxpayGuideNativeFallback: 'Mobile flows also fall back here when JSAPI or H5 cannot be used.', + wxpayGuideJsapiTitle: 'JSAPI / Official Account', + wxpayGuideJsapiOpen: 'Enable Official Account payment and ensure the browser is inside WeChat with an available OpenID.', + wxpayGuideJsapiCall: 'Inside WeChat, the app calls JSAPI after authorization and launches WeChat Pay directly.', + wxpayGuideJsapiFallback: 'If configuration is missing, the bridge is unavailable, or launch fails, the flow falls back to QR payment.', + wxpayGuideH5Title: 'H5 Payment', + wxpayGuideH5Open: 'Enable H5 payment.', + wxpayGuideH5Call: 'On mobile browsers outside WeChat, the app calls H5 payment when a client IP is available.', + wxpayGuideH5Fallback: 'If H5 is unavailable or order creation fails, the flow falls back to QR payment.', + noProviders: 'No provider instances configured', + supportedTypes: 'Supported Payment Types', + supportedTypesHint: 'Comma-separated, e.g. alipay,wxpay', + refundEnabled: 'Allow Refund', + allowUserRefund: 'Allow User Refund', + enableConflict: '{method} already has an enabled provider instance: {provider}. Disable the existing instance before switching.', + }, + balanceNotify: { + title: 'Balance Low Notification', + description: 'Send email notification when user balance falls below threshold', + enabled: 'Enable Balance Low Notification', + threshold: 'Default Threshold', + thresholdHint: 'Used when user has not set a custom value', + thresholdPlaceholder: 'Введите сумму', + rechargeUrl: 'Recharge Page URL', + rechargeUrlPlaceholder: 'https://example.com/payment', + rechargeUrlHint: 'A top-up button will appear in the email when set', + }, + quotaNotify: { + title: 'Account Quota Notification', + description: 'Notify admins when account quota usage reaches alert threshold', + enabled: 'Enable Account Quota Notification', + emails: 'Notification Emails', + emailsHint: 'Leave empty to disable notifications', + addEmail: 'Add Email', + emailPlaceholder: 'Enter email address', + }, + subscriptionExpiryNotify: { + title: 'Subscription Expiry Reminder', + description: 'Control whether users receive subscription expiry reminder emails.', + enabled: 'Enable Subscription Expiry Reminder', + enabledHint: 'When enabled, the system sends reminders 7, 3, and 1 day before expiry.' + }, + smtp: { + title: 'SMTP Settings', + description: 'Configure email sending for verification codes', + testConnection: 'Test Connection', + testing: 'Testing...', + host: 'SMTP Host', + hostPlaceholder: 'smtp.gmail.com', + port: 'SMTP Port', + portPlaceholder: '587', + username: 'SMTP Username', + usernamePlaceholder: "your-email{'@'}gmail.com", + password: 'SMTP Password', + passwordPlaceholder: '********', + passwordHint: 'Leave empty to keep existing password', + passwordConfiguredPlaceholder: '********', + passwordConfiguredHint: 'Password configured. Leave empty to keep the current value.', + fromEmail: 'From Email', + fromEmailPlaceholder: "noreply{'@'}example.com", + fromName: 'From Name', + fromNamePlaceholder: 'Sub2API', + useTls: 'Use TLS', + useTlsHint: 'Enable TLS encryption for SMTP connection' + }, + testEmail: { + title: 'Send Test Email', + description: 'Send a test email to verify your SMTP configuration', + recipientEmail: 'Recipient Email', + recipientEmailPlaceholder: "test{'@'}example.com", + sendTestEmail: 'Send Test Email', + sending: 'Sending...', + enterRecipientHint: 'Please enter a recipient email address' + }, + emailTemplates: { + title: 'Email Templates', + description: 'Customize notification email subjects and HTML content for each event and locale.', + event: 'Event', + locale: 'Locale', + localeEn: 'English', + localeZh: 'Chinese', + subject: 'Subject', + subjectPlaceholder: 'Enter the email subject', + html: 'HTML Template', + htmlPlaceholder: 'Edit the email HTML template', + placeholders: 'Available Placeholders', + placeholdersHelp: 'Click a placeholder to copy it. The backend replaces these values when sending emails.', + livePreview: 'Live Preview', + previewSecurityHint: 'Preview HTML is generated by the backend preview endpoint and displayed in a sandboxed iframe with scripts disabled.', + preview: 'Preview / Refresh', + previewing: 'Previewing...', + save: 'Save Template', + saving: 'Сохранение...', + restoreOfficial: 'Restore Official', + restoring: 'Restoring...', + restoreConfirm: 'Restore the official template for this event and locale? Your custom version will be replaced.', + restoreSuccess: 'Official template restored', + saveSuccess: 'Email template saved', + placeholderCopied: 'Placeholder copied', + validationRequired: 'Subject and HTML template are required', + empty: 'No email template events or locales are available yet.', + noPreview: 'Refresh the preview to see the rendered email subject.', + customized: 'Customized' + }, + opsMonitoring: { + title: 'Ops Monitoring', + description: 'Enable ops monitoring for troubleshooting and health visibility', + disabled: 'Ops monitoring is disabled', + enabled: 'Enable Ops Monitoring', + enabledHint: 'Enable the ops monitoring module (admin only)', + realtimeEnabled: 'Enable Realtime Monitoring', + realtimeEnabledHint: 'Enable realtime QPS/metrics push (WebSocket)', + queryMode: 'Default Query Mode', + queryModeHint: 'Default query mode for Ops Dashboard (auto/raw/preagg)', + queryModeAuto: 'Auto (recommended)', + queryModeRaw: 'Raw (most accurate, slower)', + queryModePreagg: 'Preagg (fastest, requires aggregation)', + metricsInterval: 'Metrics Collection Interval (seconds)', + metricsIntervalHint: 'How often to collect system/request metrics (60-3600 seconds)' + }, + adminApiKey: { + title: 'Admin API Key', + description: 'Global API key for external system integration with full admin access', + notConfigured: 'Admin API key not configured', + configured: 'Admin API key is active', + currentKey: 'Current Key', + regenerate: 'Regenerate', + regenerating: 'Regenerating...', + delete: 'Удалить', + deleting: 'Deleting...', + create: 'Create Key', + creating: 'Creating...', + regenerateConfirm: 'Are you sure? The current key will be immediately invalidated.', + deleteConfirm: + 'Are you sure you want to delete the admin API key? External integrations will stop working.', + keyGenerated: 'New admin API key generated', + keyDeleted: 'Admin API key deleted', + copyKey: 'Copy Key', + keyCopied: 'Key copied to clipboard', + keyWarning: 'This key will only be shown once. Please copy it now.', + securityWarning: 'Warning: This key provides full admin access. Keep it secure.', + usage: 'Usage: Add to request header - x-api-key: ' + }, + soraS3: { + title: 'Sora Storage', + description: 'Manage Sora media storage profiles with S3 and Google Drive support', + newProfile: 'New Profile', + reloadProfiles: 'Reload Profiles', + empty: 'No storage profiles yet, create one first', + createTitle: 'Create Storage Profile', + editTitle: 'Edit Storage Profile', + selectProvider: 'Select Storage Type', + providerS3Desc: 'S3-compatible object storage', + providerGDriveDesc: 'Google Drive cloud storage', + profileID: 'Profile ID', + profileName: 'Profile Name', + setActive: 'Set as active after creation', + saveProfile: 'Save Profile', + activateProfile: 'Activate', + profileCreated: 'Storage profile created', + profileSaved: 'Storage profile saved', + profileDeleted: 'Storage profile deleted', + profileActivated: 'Active storage profile switched', + profileIDRequired: 'Profile ID is required', + profileNameRequired: 'Profile name is required', + profileSelectRequired: 'Please select a profile first', + endpointRequired: 'S3 endpoint is required when enabled', + bucketRequired: 'Bucket is required when enabled', + accessKeyRequired: 'Access Key ID is required when enabled', + deleteConfirm: 'Delete storage profile {profileID}?', + columns: { + profile: 'Профиль', + profileId: 'Profile ID', + name: 'Имя', + provider: 'Type', + active: 'Активен', + endpoint: 'Endpoint', + bucket: 'Bucket', + storagePath: 'Storage Path', + capacityUsage: 'Capacity / Used', + capacityUnlimited: 'Безлимитно', + videoCount: 'Videos', + videoCompleted: 'completed', + videoInProgress: 'in progress', + quota: 'Default Quota', + updatedAt: 'Updated At', + actions: 'Действия', + rootFolder: 'Root folder', + testInTable: 'Test', + testingInTable: 'Testing...', + testTimeout: 'Test timed out (15s)' + }, + enabled: 'Enable Storage', + enabledHint: 'When enabled, Sora generated media files will be automatically uploaded', + endpoint: 'S3 Endpoint', + region: 'Region', + bucket: 'Bucket', + prefix: 'Object Prefix', + accessKeyId: 'Access Key ID', + secretAccessKey: 'Secret Access Key', + secretConfigured: '(Configured, leave blank to keep)', + cdnUrl: 'CDN URL', + cdnUrlHint: 'Optional. When configured, files are accessed via CDN URL', + forcePathStyle: 'Force Path Style', + defaultQuota: 'Default Storage Quota', + defaultQuotaHint: 'Default quota when not specified at user or group level. 0 means unlimited', + testConnection: 'Test Connection', + testing: 'Testing...', + testSuccess: 'Connection test successful', + testFailed: 'Connection test failed', + saved: 'Storage settings saved successfully', + saveFailed: 'Failed to save storage settings', + gdrive: { + authType: 'Authentication Method', + serviceAccount: 'Service Account', + clientId: 'Client ID', + clientSecret: 'Client Secret', + clientSecretConfigured: '(Configured, leave blank to keep)', + refreshToken: 'Refresh Token', + refreshTokenConfigured: '(Configured, leave blank to keep)', + serviceAccountJson: 'Service Account JSON', + serviceAccountConfigured: '(Configured, leave blank to keep)', + folderId: 'Folder ID (optional)', + authorize: 'Authorize Google Drive', + authorizeHint: 'Get Refresh Token via OAuth2', + oauthFieldsRequired: 'Please fill in Client ID and Client Secret first', + oauthSuccess: 'Google Drive authorization successful', + oauthFailed: 'Google Drive authorization failed', + closeWindow: 'This window will close automatically', + processing: 'Processing authorization...', + testStorage: 'Test Storage', + testSuccess: 'Google Drive storage test passed (upload, access, delete all OK)', + testFailed: 'Google Drive storage test failed' + } + }, + overloadCooldown: { + title: '529 Overload Cooldown', + description: 'Configure account scheduling pause strategy when upstream returns 529 (overloaded)', + enabled: 'Enable Overload Cooldown', + enabledHint: 'Pause account scheduling on 529 errors, auto-recover after cooldown', + cooldownMinutes: 'Cooldown Duration (minutes)', + cooldownMinutesHint: 'Duration to pause account scheduling (1-120 minutes)', + saved: 'Overload cooldown settings saved', + saveFailed: 'Failed to save overload cooldown settings' + }, + rateLimit429Cooldown: { + title: '429 Default Cooldown', + description: 'Configure the default account cooldown when upstream returns 429 without an explicit reset time', + enabled: 'Enable 429 Default Cooldown', + enabledHint: 'Pause account scheduling when a 429 has no reset time, then auto-recover after cooldown', + cooldownSeconds: 'Cooldown Duration (seconds)', + cooldownSecondsHint: 'Default cooldown duration (1-7200 seconds); explicit upstream reset times still take precedence', + saved: '429 default cooldown settings saved', + saveFailed: 'Failed to save 429 default cooldown settings' + }, + streamTimeout: { + title: 'Stream Timeout Handling', + description: 'Configure account handling strategy when upstream response times out', + enabled: 'Enable Stream Timeout Handling', + enabledHint: 'Automatically handle problematic accounts when upstream times out', + timeoutSeconds: 'Timeout Threshold (seconds)', + timeoutSecondsHint: 'Stream data interval exceeding this time is considered timeout (30-300s)', + action: 'Action', + actionTempUnsched: 'Temporarily Unschedulable', + actionError: 'Mark as Error', + actionNone: 'No Action', + actionHint: 'Action to take on the account after timeout', + tempUnschedMinutes: 'Pause Duration (minutes)', + tempUnschedMinutesHint: 'Duration of temporary unschedulable state (1-60 minutes)', + thresholdCount: 'Trigger Threshold (count)', + thresholdCountHint: 'Number of timeouts before triggering action (1-10)', + thresholdWindowMinutes: 'Threshold Window (minutes)', + thresholdWindowMinutesHint: 'Time window for counting timeouts (1-60 minutes)', + saved: 'Stream timeout settings saved', + saveFailed: 'Failed to save stream timeout settings' + }, + rectifier: { + title: 'Request Rectifier', + description: 'Automatically fix request parameters and retry when upstream returns specific errors', + enabled: 'Enable Request Rectifier', + enabledHint: 'Master switch - disabling turns off all rectification features', + thinkingSignature: 'Thinking Signature Rectifier', + thinkingSignatureHint: 'Automatically strip signatures and retry when upstream returns thinking block signature validation errors', + thinkingBudget: 'Thinking Budget Rectifier', + thinkingBudgetHint: 'Automatically set budget to 32000 and retry when upstream returns budget_tokens constraint error (≥1024)', + apikeySignature: 'API Key Signature Rectifier', + apikeySignatureHint: + 'Automatically strip signatures and retry when API Key accounts receive signature-related errors (built-in patterns always apply)', + apikeyPatterns: 'Custom Match Patterns', + apikeyPatternsHint: + 'Additional keywords matched against the response body (case-insensitive). Built-in patterns always apply; use these for supplementary matching.', + apikeyPatternPlaceholder: 'e.g., thinking_error', + addPattern: 'Add Pattern', + saved: 'Rectifier settings saved', + saveFailed: 'Failed to save rectifier settings' + }, + betaPolicy: { + title: 'Beta Policy', + description: 'How to handle Beta features when configuring the forwarding of Anthropic API requests. Applicable only to the /v1/messages endpoint.', + action: 'Action', + actionPass: 'Pass (transparent)', + actionFilter: 'Filter (remove)', + actionBlock: 'Block (reject)', + scope: 'Scope', + scopeAll: 'All accounts', + scopeOAuth: 'OAuth only', + scopeAPIKey: 'API Key only', + scopeBedrock: 'Bedrock only', + errorMessage: 'Error message', + errorMessagePlaceholder: 'Custom error message when blocked', + errorMessageHint: 'Leave empty for default message', + saved: 'Beta policy settings saved', + saveFailed: 'Failed to save beta policy settings', + modelWhitelist: 'Model Whitelist', + modelWhitelistHint: 'Leave empty to apply to all models. Supports exact match and wildcard prefix (e.g., claude-opus-*)', + modelPatternPlaceholder: 'e.g., claude-opus-* or claude-opus-4-6', + addModelPattern: 'Add model pattern', + removePattern: 'Remove', + fallbackAction: 'Fallback Action', + fallbackActionHint: 'Action for models not matching the whitelist', + fallbackErrorMessagePlaceholder: 'Custom error message when non-whitelisted models are blocked', + quickPresets: 'Quick Presets', + presetOpusOnly: 'Opus only for 1M', + presetOpusOnlyDesc: 'Pass for Opus, filter others', + commonPatterns: 'Common patterns' + }, + openaiFastPolicy: { + title: 'OpenAI Fast/Flex Policy', + description: 'Intercept, filter, or pass OpenAI fast(priority) / flex requests based on the request body service_tier field. Applies to the OpenAI gateway only.', + empty: 'No rules configured. Click the button below to add one.', + ruleHeader: 'Rule #{index}', + removeRule: 'Remove rule', + addRule: 'Add rule', + saveHint: 'Saved together with system settings (click the global Save button at the bottom of the page).', + serviceTier: 'service_tier match', + tierAll: 'All tiers', + tierPriority: 'priority (fast)', + tierFlex: 'flex', + action: 'Action', + actionPass: 'Pass (keep service_tier)', + actionFilter: 'Filter (remove service_tier)', + actionBlock: 'Block (reject request)', + scope: 'Scope', + scopeAll: 'All accounts', + scopeOAuth: 'OAuth only', + scopeAPIKey: 'API Key only', + scopeBedrock: 'Bedrock only', + errorMessage: 'Error message', + errorMessagePlaceholder: 'Custom error message when blocked', + errorMessageHint: 'Leave empty for the default message.', + modelWhitelist: 'Model whitelist', + modelWhitelistHint: 'Leave empty to apply to all models. Supports exact match and wildcard prefix (e.g., gpt-5.5*).', + modelPatternPlaceholder: 'e.g., gpt-5.5 or gpt-5.5*', + addModelPattern: 'Add model pattern', + fallbackAction: 'Fallback action', + fallbackActionHint: 'Action for models not matching the whitelist.', + fallbackErrorMessagePlaceholder: 'Custom error message when non-whitelisted models are blocked' + }, + wechatConnect: { + title: 'WeChat Connect', + description: 'Third-party login configuration for WeChat Open Platform or Official Account / Mini Program.', + enabledLabel: 'Enable WeChat Connect', + enabledHint: 'Enable this to configure WeChat OAuth callbacks and authorization.', + appIdLabel: 'App ID', + appIdPlaceholder: 'WeChat App ID', + appSecretLabel: 'App Secret', + appSecretConfiguredPlaceholder: 'Secret configured. Leave empty to keep the current value.', + appSecretPlaceholder: 'WeChat App Secret', + appSecretConfiguredHint: 'Secret configured. Leave empty to keep the current value.', + appSecretHint: 'Enter a new secret to replace the current WeChat credential.', + modeLabel: 'Mode', + openModeLabel: 'Use Open outside WeChat', + openModeHint: 'Use Open Platform QR authorization outside the WeChat browser.', + mpModeLabel: 'Use MP inside WeChat', + mpModeHint: 'Use Official Account authorization inside the WeChat browser.', + redirectUrlLabel: 'Redirect URL', + redirectUrlPlaceholder: 'https://your-site.com/api/v1/auth/oauth/wechat/callback', + generateAndCopy: 'Generate & Copy (current site)', + redirectUrlSetAndCopied: 'Redirect URL generated and copied to clipboard', + frontendRedirectUrlLabel: 'Frontend redirect URL', + frontendRedirectUrlPlaceholder: '/auth/wechat/callback', + frontendRedirectUrlHint: 'Usually the frontend route callback path; keep it aligned with the backend.' + }, + authSourceDefaults: { + title: 'Auth Source Defaults', + description: 'Configure per-source default balance, concurrency, subscriptions, and grant rules.', + requireEmailLabel: 'Require email on third-party signup', + requireEmailHint: 'When enabled, Linux DO, OIDC, and WeChat signups must provide an email before account creation.', + enabledHint: 'These defaults apply when a new user registers through this source. Grant on first bind only applies when an existing user binds this source.', + sources: { + email: { + title: 'Email signup', + description: 'Default quota grants for email-password signups.' + }, + linuxdo: { + title: 'Linux DO signup', + description: 'Default quota grants for Linux DO signups.' + }, + oidc: { + title: 'OIDC signup', + description: 'Default quota grants for OIDC signups.' + }, + wechat: { + title: 'WeChat signup', + description: 'Default quota grants for WeChat signups.' + } + }, + grantOnFirstBindLabel: 'Grant on first bind', + grantOnFirstBindHint: 'Grant default entitlements when an existing user first binds this source.', + defaultSubscriptionsLabel: 'Default subscriptions', + defaultSubscriptionsHint: 'Applies only to this auth source. Leave empty to skip source-specific subscriptions.', + noSourceSubscriptions: 'No source-specific default subscriptions configured.', + platformQuotasOverride: 'Platform Quota Overrides', + platformQuotasOverrideHint: 'Blank fields inherit the system default. Set to 0 to fully block that window for this auth source.', + }, + paymentVisibleMethods: { + methodLabel: '{title} visible method', + methodHint: 'Controls whether checkout shows this method and which source key it exposes.', + sourceLabel: 'Payment source', + sourceHint: 'Choose an explicit source before enabling the method. Not configured methods are not exposed.', + sourceRequiredError: 'Select a payment source before enabling {title}.' + }, + openaiExperimentalScheduler: { + title: 'OpenAI experimental scheduler policy', + description: "Disabled by default. When enabled, this only changes the gateway's experimental account-selection policy for OpenAI traffic; it does not indicate an upstream OpenAI capability." + }, + saveSettings: 'Save Settings', + saving: 'Сохранение...', + settingsSaved: 'Settings saved successfully', + smtpConnectionSuccess: 'SMTP connection successful', + testEmailSent: 'Test email sent successfully', + failedToLoad: 'Failed to load settings', + failedToSave: 'Failed to save settings', + failedToTestSmtp: 'SMTP connection test failed', + failedToSendTestEmail: 'Failed to send test email' + }, + + // Error Passthrough Rules + errorPassthrough: { + title: 'Error Passthrough Rules', + description: 'Configure how upstream errors are returned to clients', + createRule: 'Create Rule', + editRule: 'Edit Rule', + deleteRule: 'Delete Rule', + noRules: 'No rules configured', + createFirstRule: 'Create your first error passthrough rule', + allPlatforms: 'All Platforms', + passthrough: 'Passthrough', + custom: 'Свой', + code: 'Code', + body: 'Body', + skipMonitoring: 'Skip Monitoring', + + // Columns + columns: { + priority: 'Priority', + name: 'Имя', + conditions: 'Conditions', + platforms: 'Platforms', + behavior: 'Behavior', + status: 'Статус', + actions: 'Действия' + }, + + // Match Mode + matchMode: { + any: 'Code OR Keyword', + all: 'Code AND Keyword', + anyHint: 'Status code matches any error code, OR message contains any keyword', + allHint: 'Status code matches any error code, AND message contains any keyword' + }, + + // Form + form: { + name: 'Rule Name', + namePlaceholder: 'e.g., Context Limit Passthrough', + priority: 'Priority', + priorityHint: 'Lower values have higher priority', + description: 'Description', + descriptionPlaceholder: 'Describe the purpose of this rule...', + matchConditions: 'Match Conditions', + errorCodes: 'Error Codes', + errorCodesPlaceholder: '422, 400, 429', + errorCodesHint: 'Separate multiple codes with commas', + keywords: 'Keywords', + keywordsPlaceholder: 'One keyword per line\ncontext limit\nmodel not supported', + keywordsHint: 'One keyword per line, case-insensitive', + matchMode: 'Match Mode', + platforms: 'Platforms', + platformsHint: 'Leave empty to apply to all platforms', + responseBehavior: 'Response Behavior', + passthroughCode: 'Passthrough upstream status code', + responseCode: 'Custom status code', + passthroughBody: 'Passthrough upstream error message', + customMessage: 'Custom error message', + customMessagePlaceholder: 'Error message to return to client...', + skipMonitoring: 'Skip monitoring', + skipMonitoringHint: 'When enabled, errors matching this rule will not be recorded in ops monitoring', + enabled: 'Enable this rule' + }, + + // Messages + nameRequired: 'Please enter rule name', + conditionsRequired: 'Please configure at least one error code or keyword', + ruleCreated: 'Rule created successfully', + ruleUpdated: 'Rule updated successfully', + ruleDeleted: 'Rule deleted successfully', + deleteConfirm: 'Are you sure you want to delete rule "{name}"?', + failedToLoad: 'Failed to load rules', + failedToSave: 'Failed to save rule', + failedToDelete: 'Failed to delete rule', + failedToToggle: 'Failed to toggle status' + }, + + // TLS Fingerprint Profiles + tlsFingerprintProfiles: { + title: 'TLS Fingerprint Profiles', + description: 'Manage TLS fingerprint profiles for simulating specific client TLS handshake characteristics', + createProfile: 'Create Profile', + editProfile: 'Изменить профиль', + deleteProfile: 'Delete Profile', + noProfiles: 'No profiles configured', + createFirstProfile: 'Create your first TLS fingerprint profile', + + columns: { + name: 'Имя', + description: 'Description', + grease: 'GREASE', + alpn: 'ALPN', + actions: 'Действия' + }, + + form: { + pasteYaml: 'Paste YAML Configuration', + pasteYamlPlaceholder: 'Paste YAML output from TLS Fingerprint Collector here...', + pasteYamlHint: 'Paste the YAML copied from TLS Fingerprint Collector to auto-fill all fields.', + openCollector: 'Open Collector', + parseYaml: 'Parse YAML', + yamlParsed: 'YAML parsed successfully, fields auto-filled', + yamlParseFailed: 'Failed to parse YAML: name field not found', + name: 'Profile Name', + namePlaceholder: 'e.g. macOS Node.js v24', + description: 'Description', + descriptionPlaceholder: 'Optional description for this profile', + enableGrease: 'Enable GREASE', + enableGreaseHint: 'Insert GREASE values in TLS ClientHello extensions', + cipherSuites: 'Cipher Suites', + cipherSuitesHint: 'Comma-separated hex values, e.g. 0x1301, 0x1302, 0xc02c', + curves: 'Elliptic Curves', + curvesHint: 'Comma-separated curve IDs', + pointFormats: 'Point Formats', + signatureAlgorithms: 'Signature Algorithms', + alpnProtocols: 'ALPN Protocols', + alpnProtocolsHint: 'Comma-separated, e.g. h2, http/1.1', + supportedVersions: 'Supported TLS Versions', + keyShareGroups: 'Key Share Groups', + pskModes: 'PSK Modes', + extensions: 'Extensions' + }, + + deleteConfirm: 'Delete Profile', + deleteConfirmMessage: 'Are you sure you want to delete profile "{name}"? Accounts using this profile will fall back to the built-in default.', + createSuccess: 'Profile created successfully', + updateSuccess: 'Профиль обновлён', + deleteSuccess: 'Profile deleted successfully', + loadFailed: 'Failed to load profiles', + saveFailed: 'Failed to save profile', + deleteFailed: 'Failed to delete profile' + } + }, + + // Subscription Progress (Header component) + subscriptionProgress: { + title: 'Мои подписки', + viewDetails: 'Посмотреть детали подписки', + activeCount: 'Активных подписок: {count}', + daily: 'Daily', + weekly: 'Weekly', + monthly: 'Monthly', + daysRemaining: 'Осталось дней: {days}', + expired: 'Истекла', + expiresToday: 'Истекает сегодня', + expiresTomorrow: 'Истекает завтра', + viewAll: 'Все подписки', + noSubscriptions: 'Нет активных подписок', + unlimited: 'Безлимитно' + }, + + // Version Badge + version: { + currentVersion: 'Current Version', + latestVersion: 'Latest Version', + upToDate: "You're running the latest version.", + updateAvailable: 'A new version is available!', + releaseNotes: 'Release Notes', + noReleaseNotes: 'No release notes', + viewUpdate: 'View Update', + viewRelease: 'View Release', + viewChangelog: 'View Changelog', + refresh: 'Обновить', + sourceMode: 'Source Build', + sourceModeHint: 'Source build, use git pull to update', + updateNow: 'Update Now', + updating: 'Updating...', + updateComplete: 'Update Complete', + updateFailed: 'Update Failed', + restartRequired: 'Please restart the service to apply the update', + restartNow: 'Restart Now', + restarting: 'Restarting...', + retry: 'Retry' + }, + + // Recharge / Subscription Page + purchase: { + title: 'Пополнение / подписка', + description: 'Recharge balance or purchase subscription via the embedded page', + openInNewTab: 'Открыть в новой вкладке', + notEnabledTitle: 'Функция не включена', + notEnabledDesc: 'The administrator has not enabled the recharge/subscription entry. Please contact admin.', + notConfiguredTitle: 'Recharge / Subscription URL not configured', + notConfiguredDesc: + 'The administrator enabled the entry but has not configured a recharge/subscription URL. Please contact admin.' + }, + + // Custom Page (iframe embed) + customPage: { + title: 'Custom Page', + openInNewTab: 'Открыть в новой вкладке', + notFoundTitle: 'Страница не найдена', + notFoundDesc: 'This custom page does not exist or has been removed.', + notConfiguredTitle: 'Page URL not configured', + notConfiguredDesc: 'The URL for this custom page has not been properly configured.', + }, + + // Announcements Page + announcements: { + title: 'Объявления', + description: 'Просмотр системных объявлений', + unreadOnly: 'Только непрочитанные', + markRead: 'Отметить прочитанным', + markAllRead: 'Отметить все прочитанными', + viewAll: 'View all announcements', + markedAsRead: 'Marked as read', + allMarkedAsRead: 'All announcements marked as read', + newCount: '{count} new announcement | {count} new announcements', + readAt: 'Read at', + read: 'Read', + unread: 'Unread', + startsAt: 'Starts at', + endsAt: 'Ends at', + empty: 'Нет объявлений', + emptyUnread: 'Нет непрочитанных объявлений', + total: 'announcements', + emptyDescription: 'There are no system announcements at this time', + readStatus: 'You have read this announcement', + markReadHint: 'Click "Отметить прочитанным" to mark this announcement' + }, + + // User Subscriptions Page + userSubscriptions: { + title: 'Мои подписки', + description: 'Просмотр ваших подписок и расхода', + noActiveSubscriptions: 'Нет активных подписок', + noActiveSubscriptionsDesc: + "You don't have any active subscriptions. Contact administrator to get one.", + failedToLoad: 'Failed to load subscriptions', + status: { + active: 'Активен', + expired: 'Истекла', + revoked: 'Revoked' + }, + usage: 'Расход', + expires: 'Expires', + noExpiration: 'Без срока', + unlimited: 'Безлимитно', + unlimitedDesc: 'У этой подписки нет лимитов расхода', + daily: 'Daily', + weekly: 'Weekly', + monthly: 'Monthly', + daysRemaining: 'Осталось дней: {days}', + expiresOn: 'Истекает {date}', + resetIn: 'Сброс через {time}', + quotaEndsIn: 'Quota ends in {time}', + windowNotActive: 'Awaiting first use', + usageOf: '{used} из {limit}' + }, + + // Onboarding Tour + onboarding: { + restartTour: 'Restart Onboarding Tour', + dontShowAgain: "Don't show again", + dontShowAgainTitle: 'Permanently close onboarding guide', + confirmDontShow: "Are you sure you don't want to see the onboarding guide again?\n\nYou can restart it anytime from the user menu in the top right corner.", + confirmExit: 'Are you sure you want to exit the onboarding guide? You can restart it anytime from the top right menu.', + interactiveHint: 'Press Enter or Click to continue', + navigation: { + flipPage: 'Flip Page', + exit: 'Exit' + }, + // Admin tour steps + admin: { + welcome: { + title: '👋 Welcome to Sub2API', + description: '

Sub2API is a powerful AI service gateway platform that helps you easily manage and distribute AI services.

🎯 Core Features:

  • 📦 Group Management - Create service tiers (VIP, Free Trial, etc.)
  • 🔗 Account Pool - Connect multiple upstream AI service accounts
  • 🔑 Key Distribution - Generate independent API Keys for users
  • 💰 Billing Control - Flexible rate and quota management

Let\'s complete the initial setup in 3 minutes →

', + nextBtn: 'Start Setup 🚀', + prevBtn: 'Skip' + }, + groupManage: { + title: '📦 Step 1: Group Management', + description: '

What is a Group?

Groups are the core concept of Sub2API, like a "service package":

  • 🎯 Each group can contain multiple upstream accounts
  • 💰 Each group has independent billing multiplier
  • 👥 Can be set as public or exclusive

💡 Example: You can create "VIP Premium" (high rate) and "Free Trial" (low rate) groups

👉 Click "Group Management" on the left sidebar

' + }, + createGroup: { + title: '➕ Create New Group', + description: '

Let\'s create your first group.

📝 Tip: Recommend creating a test group first to familiarize yourself with the process

👉 Click the "Create Group" button

' + }, + groupName: { + title: '✏️ 1. Group Name', + description: '

Give your group an easy-to-identify name.

💡 Naming Suggestions:
  • "Test Group" - For testing
  • "VIP Premium" - High-quality service
  • "Free Trial" - Trial version

Click "Далее" when done

', + nextBtn: 'Далее' + }, + groupPlatform: { + title: '🤖 2. Select Platform', + description: '

Choose the AI platform this group supports.

📌 Platform Guide:
  • Anthropic - Claude models
  • OpenAI - GPT models
  • Google - Gemini models

One group can only have one platform

', + nextBtn: 'Далее' + }, + groupMultiplier: { + title: '💰 3. Rate Multiplier', + description: '

Set the billing multiplier to control user charges.

⚙️ Billing Rules:
  • 1.0 - Original price (cost price)
  • 1.5 - User consumes $1, charged $1.5
  • 2.0 - User consumes $1, charged $2
  • 0.8 - Subsidy mode (loss-making)

Recommend setting test group to 1.0

', + nextBtn: 'Далее' + }, + groupExclusive: { + title: '🔒 4. Exclusive Group (Optional)', + description: '

Control group visibility and access permissions.

🔐 Permission Guide:
  • Off - Public group, visible to all users
  • On - Exclusive group, only for specified users

💡 Use Cases: VIP exclusive, internal testing, special customers

', + nextBtn: 'Далее' + }, + groupSubmit: { + title: '✅ Save Group', + description: '

Confirm the information and click create to save the group.

⚠️ Note: Platform type cannot be changed after creation, but other settings can be edited anytime

📌 Next Step: After creation, we\'ll add upstream accounts to this group

👉 Click "Создать" button

' + }, + accountManage: { + title: '🔗 Step 2: Add Account', + description: '

Great! Group created successfully 🎉

Now add upstream AI service accounts to enable actual service delivery.

🔑 Account Purpose:
  • Connect to upstream AI services (Claude, GPT, etc.)
  • One group can contain multiple accounts (load balancing)
  • Supports OAuth and Session Key methods

👉 Click "Управление аккаунтами" on the left sidebar

' + }, + createAccount: { + title: '➕ Add New Account', + description: '

Click the button to start adding your first upstream account.

💡 Tip: Recommend using OAuth method - more secure and no manual key extraction needed

👉 Click "Add Account" button

' + }, + accountName: { + title: '✏️ 1. Account Name', + description: '

Set an easy-to-identify name for the account.

💡 Naming Suggestions: "Claude Main", "GPT Backup 1", "Test Account", etc.

', + nextBtn: 'Далее' + }, + accountPlatform: { + title: '🤖 2. Select Platform', + description: '

Choose the service provider platform for this account.

⚠️ Important: Platform must match the group you just created

', + nextBtn: 'Далее' + }, + accountType: { + title: '🔐 3. Authorization Method', + description: '

Choose the account authorization method.

✅ Recommended: OAuth Method
  • No manual key extraction needed
  • More secure with auto-refresh support
  • Works with Claude Code, ChatGPT OAuth
📌 Session Key Method
  • Requires manual extraction from browser
  • May need periodic updates
  • For platforms without OAuth support
', + nextBtn: 'Далее' + }, + accountPriority: { + title: '⚖️ 4. Priority (Optional)', + description: '

Set the account call priority.

📊 Priority Rules:
  • Lower number = higher priority
  • System uses low-value accounts first
  • Same priority = random selection

💡 Use Case: Set main account to lower value, backup accounts to higher value

', + nextBtn: 'Далее' + }, + accountGroups: { + title: '🎯 5. Assign Groups', + description: '

Key Step! Assign the account to the group you just created.

⚠️ Important Reminder:
  • Must select at least one group
  • Unassigned accounts cannot be used
  • One account can be assigned to multiple groups

💡 Tip: Select the test group you just created

', + nextBtn: 'Далее' + }, + accountSubmit: { + title: '✅ Save Account', + description: '

Confirm the information and click save.

📌 OAuth Flow:
  • Will redirect to service provider page after clicking save
  • Complete login and authorization on provider page
  • Auto-return after successful authorization

📌 Next Step: After adding account, we\'ll create an API key

👉 Click "Сохранить" button

' + }, + keyManage: { + title: '🔑 Step 3: Generate Key', + description: '

Congratulations! Account setup complete 🎉

Final step: generate an API Key to test if the service works properly.

🔑 API Key Purpose:
  • Credential for calling AI services
  • Each key is bound to one group
  • Can set quota and expiration
  • Supports independent usage statistics

👉 Click "API-ключи" on the left sidebar

' + }, + createKey: { + title: '➕ Create Key', + description: '

Click the button to create your first API Key.

💡 Tip: Copy and save immediately after creation - key is only shown once

👉 Click "Create Key" button

' + }, + keyName: { + title: '✏️ 1. Key Name', + description: '

Set an easy-to-manage name for the key.

💡 Naming Suggestions: "Test Key", "Production", "Mobile", etc.

', + nextBtn: 'Далее' + }, + keyGroup: { + title: '🎯 2. Select Group', + description: '

Select the group you just configured.

📌 Group Determines:
  • Which accounts this key can use
  • What billing multiplier applies
  • Whether it\'s an exclusive key

💡 Tip: Select the test group you just created

', + nextBtn: 'Далее' + }, + keySubmit: { + title: '🎉 Generate and Copy', + description: '

System will generate a complete API Key after clicking create.

⚠️ Important Reminder:
  • Key is only shown once, copy immediately
  • Need to regenerate if lost
  • Keep it safe, don\'t share with others
🚀 Next Steps:
  • Copy the generated sk-xxx key
  • Use in any OpenAI-compatible client
  • Start experiencing AI services!

👉 Click "Создать" button

' + } + }, + // User tour steps + user: { + welcome: { + title: '👋 Welcome to Sub2API', + description: '

Hello! Welcome to the Sub2API AI service platform.

🎯 Quick Start:

  • 🔑 Create API Key
  • 📋 Copy key to your application
  • 🚀 Start using AI services

Just 1 minute, let\'s get started →

', + nextBtn: 'Start 🚀', + prevBtn: 'Skip' + }, + keyManage: { + title: '🔑 API Key Management', + description: '

Manage all your API access keys here.

📌 What is an API Key?
An API key is your credential for accessing AI services, like a key that allows your application to call AI capabilities.

👉 Click to enter key page

' + }, + createKey: { + title: '➕ Create New Key', + description: '

Click the button to create your first API key.

💡 Tip: Key is only shown once after creation, make sure to copy and save

👉 Click "Create Key"

' + }, + keyName: { + title: '✏️ Key Name', + description: '

Give your key an easy-to-identify name.

💡 Examples: "My First Key", "For Testing", etc.

', + nextBtn: 'Далее' + }, + keyGroup: { + title: '🎯 Select Group', + description: '

Select the service group assigned by the administrator.

📌 Group Info:
Different groups may have different service quality and billing rates, choose according to your needs.

', + nextBtn: 'Далее' + }, + keySubmit: { + title: '🎉 Complete Creation', + description: '

Click to confirm and create your API key.

⚠️ Important:
  • Copy the key (sk-xxx) immediately after creation
  • Key is only shown once, need to regenerate if lost

🚀 How to Use:
Configure the key in any OpenAI-compatible client (like ChatBox, OpenCat, etc.) and start using!

👉 Click "Создать" button

' + } + } + }, + + // Payment System + payment: { + title: 'Пополнение / подписка', + amountLabel: 'Сумма', + paymentAmount: 'Сумма платежа', + creditedBalance: 'Зачисляемый баланс', + quickAmounts: 'Быстрые суммы', + customAmount: 'Своя сумма', + enterAmount: 'Введите сумму', + paymentMethod: 'Способ оплаты', + fee: 'Комиссия', + actualPay: 'К оплате', + createOrder: 'Подтвердить оплату', + methods: { + easypay: 'EasyPay', + alipay: 'Alipay', + wxpay: 'WeChat Pay', + stripe: 'Stripe', + airwallex: 'Airwallex', + card: 'Card', + link: 'Link', + alipay_direct: 'Alipay (Direct)', + wxpay_direct: 'WeChat Pay (Direct)', + }, + status: { + pending: 'Ожидает', + paid: 'Оплачено', + recharging: 'Recharging', + completed: 'Завершено', + expired: 'Истекла', + cancelled: 'Отменено', + failed: 'Ошибка', + refund_requested: 'Refund Requested', + refunding: 'Refunding', + refunded: 'Refunded', + partially_refunded: 'Partially Refunded', + refund_failed: 'Refund Failed', + }, + qr: { + scanToPay: 'Сканируйте для оплаты', + scanAlipay: 'Alipay QR Payment', + scanWxpay: 'WeChat QR Payment', + scanAlipayHint: 'Open Alipay on your phone and scan the QR code to pay', + scanWxpayHint: 'Open WeChat on your phone and scan the QR code to pay', + payInNewWindow: 'Complete Payment in New Window', + payInNewWindowHint: 'The payment page has opened in a new window. Please complete the payment there and return to this page.', + openPayWindow: 'Reopen Payment Page', + expiresIn: 'Expires in', + expired: 'Заказ истёк', + expiredDesc: 'This order has expired. Please create a new one.', + cancelled: 'Заказ отменён', + cancelledDesc: 'You have cancelled this payment.', + waitingPayment: 'Ожидание оплаты...', + cancelOrder: 'Отменить заказ', + }, + orders: { + title: 'Мои заказы', + empty: 'Заказов пока нет', + orderId: 'ID заказа', + orderNo: 'Номер заказа', + amount: 'Сумма', + payAmount: 'Оплачено', + creditedAmount: 'Credited Amount', + fee: 'Комиссия', + baseAmount: 'Base Amount', + includedInPayAmount: 'included in paid amount', + status: 'Статус', + paymentMethod: 'Способ оплаты', + createdAt: 'Создан', + cancel: 'Отменить заказ', + userId: 'User ID', + orderType: 'Order Type', + actions: 'Действия', + requestRefund: 'Request Refund', + }, + result: { + success: 'Оплата успешна', + subscriptionSuccess: 'Подписка оформлена', + processing: 'Платёж обрабатывается', + processingHint: 'Payment confirmation is still pending. This page will refresh automatically.', + failed: 'Оплата не удалась', + backToRecharge: 'Назад к пополнению', + viewOrders: 'Посмотреть заказы', + }, + currentBalance: 'Текущий баланс', + groupFallback: 'Group #{id}', + rechargeAccount: 'Пополнить аккаунт', + activeSubscription: 'Активная подписка', + noActiveSubscription: 'No active subscription', + tabTopUp: 'Пополнить', + tabSubscribe: 'Подписаться', + noPlans: 'Нет доступных планов подписки', + notAvailable: 'Top-up is currently unavailable', + confirmSubscription: 'Подтвердить подписку', + confirmCancel: 'Are you sure you want to cancel this order?', + amountTooLow: 'Minimum amount is {min}', + amountTooHigh: 'Maximum amount is {max}', + amountNoMethod: 'No payment method available for this amount', + rechargeRatePreview: 'Current rate: 1 CNY = {usd} USD', + refundReason: 'Refund Reason', + refundReasonPlaceholder: 'Please describe your refund reason', + stripeLoadFailed: 'Failed to load payment component. Please refresh and try again.', + stripeMissingParams: 'Missing order ID or client secret', + stripeNotConfigured: 'Stripe is not configured', + airwallexLoadFailed: 'Failed to load Airwallex payment component. Please refresh and try again.', + airwallexMissingParams: 'Missing Airwallex payment parameters', + errors: { + tooManyPending: 'Too many pending orders (max {max}). Please complete or cancel existing orders first.', + cancelRateLimited: 'Too many cancellations. Please try again later.', + wechatH5NotAuthorized: 'This merchant has not enabled WeChat H5 payment. Open this page in WeChat to continue.', + wechatPaymentMpNotConfigured: 'This site has not completed WeChat MP/JSAPI payment setup, so in-app WeChat payment is unavailable right now.', + wechatJsapiUnavailable: 'WeChat payment could not be invoked in the current environment. Reopen this page inside WeChat and try again.', + wechatJsapiFailed: 'WeChat payment did not complete. Try invoking it again or switch to QR payment.', + wechatUnavailable: 'WeChat payment is temporarily unavailable. Please try again later.', + wechatOpenInWeChatHint: 'Open the current page inside WeChat, or switch to desktop WeChat QR payment.', + wechatScanOnDesktopHint: 'On desktop, use WeChat Scan to pay; on mobile, reopen the current page inside WeChat.', + wechatSwitchBrowserHint: 'Switch to desktop WeChat QR payment, or reopen this page in an external browser and retry.', + mobilePaymentFallbackToQr: 'This merchant has not enabled mobile payment. The flow has been switched to QR payment automatically.', + alipayDesktopUnavailable: 'The desktop Alipay flow could not generate a QR code.', + alipayDesktopQrHint: 'Desktop Alipay should render a QR code. Refresh and retry, or make sure the payment page was not blocked.', + alipayMobileUnavailable: 'This page could not hand off to Alipay.', + alipayMobileOpenHint: 'Allow the current page to open the Alipay app, or retry from the system browser.', + // Structured error codes (reason strings from backend ApplicationError) + PAYMENT_DISABLED: 'Payment system is disabled.', + USER_INACTIVE: 'Your account is disabled.', + BALANCE_PAYMENT_DISABLED: 'Balance recharge has been disabled.', + INVALID_AMOUNT: 'Invalid amount.', + INVALID_INPUT: 'Invalid request.', + PLAN_NOT_AVAILABLE: 'Plan not found or no longer available.', + GROUP_NOT_FOUND: 'Subscription group is no longer available.', + GROUP_TYPE_MISMATCH: 'Group is not a subscription type.', + TOO_MANY_PENDING: 'Too many pending orders (max {max}). Please complete or cancel existing orders first.', + DAILY_LIMIT_EXCEEDED: 'Daily recharge limit reached. Remaining: {remaining}.', + PAYMENT_GATEWAY_ERROR: 'Payment method is unavailable.', + NO_AVAILABLE_INSTANCE: 'No payment channel available right now.', + PAYMENT_PROVIDER_MISCONFIGURED: 'Payment provider misconfigured. Please contact an administrator.', + WXPAY_CONFIG_MISSING_KEY: 'WeChat Pay config missing required key: {key}.', + WXPAY_CONFIG_INVALID_KEY_LENGTH: 'WeChat Pay {key} length is invalid (expected {expected} bytes, got {actual}).', + WXPAY_CONFIG_INVALID_KEY: 'WeChat Pay {key} is malformed. Make sure you copied the full PEM content.', + PENDING_ORDERS: 'This provider has pending orders. Please wait for them to complete before making changes.', + PAYMENT_PROVIDER_CONFLICT: 'Another enabled provider instance is already serving this payment method. Disable it before continuing.', + CANCEL_RATE_LIMITED: 'Too many cancellations. Please try again later.', + NOT_FOUND: 'Order not found.', + FORBIDDEN: 'No permission for this order.', + CONFLICT: 'Order status has changed. Please refresh.', + INVALID_ORDER_TYPE: 'Only balance orders can request a refund.', + INVALID_STATUS: 'The current order status does not allow this operation.', + BALANCE_NOT_ENOUGH: 'Refund amount exceeds balance.', + REFUND_AMOUNT_EXCEEDED: 'Refund amount exceeds the recharge amount.', + REFUND_FAILED: 'Refund failed.', + }, + airwallexPay: 'Airwallex Payment', + stripePay: 'Pay Now', + stripeSuccessProcessing: 'Payment successful, processing your order...', + stripePopup: { + redirecting: 'Redirecting to payment page...', + loadingQr: 'Loading WeChat Pay QR code...', + timeout: 'Timed out waiting for payment credentials, please retry', + qrFailed: 'Failed to get WeChat Pay QR code', + }, + subscribeNow: 'Подписаться', + renewNow: 'Продлить', + selectPlan: 'Выбрать план', + planFeatures: 'Возможности', + planCard: { + rate: 'Тариф', + dailyLimit: 'Daily', + weeklyLimit: 'Weekly', + monthlyLimit: 'Monthly', + quota: 'Quota', + unlimited: 'Безлимитно', + models: 'Модели', + }, + days: 'дней', + months: 'месяцев', + years: 'лет', + oneMonth: '1 месяц', + oneYear: '1 год', + perMonth: 'месяц', + perYear: 'год', + admin: { + tabs: { + overview: 'Overview', + orders: 'Заказы', + channels: 'Каналы', + plans: 'Планы', + }, + todayRevenue: 'Today Revenue', + totalRevenue: 'Total Revenue', + todayOrders: 'Today Orders', + orderCount: 'Order Count', + avgAmount: 'Average Amount', + revenue: 'Revenue', + dailyRevenue: 'Daily Revenue', + paymentDistribution: 'Payment Distribution', + colUser: 'Пользователь', + topUsers: 'Top Users', + noData: 'Нет данных', + days: 'дней', + weeks: 'weeks', + months: 'месяцев', + searchOrders: 'Search orders...', + allStatuses: 'All Statuses', + allPaymentTypes: 'All Payment Types', + allOrderTypes: 'All Order Types', + orderDetail: 'Order Detail', + orderType: 'Order Type', + orders: 'Заказы', + balanceOrder: 'Balance Top-Up', + subscriptionOrder: 'Subscription', + paidAt: 'Paid At', + completedAt: 'Completed At', + expiresAt: 'Истекает', + feeRate: 'Fee Rate', + refund: 'Refund', + refundOrder: 'Refund Order', + refundAmount: 'Refund Amount', + maxRefundable: 'Max Refundable', + refundReason: 'Refund Reason', + refundReasonPlaceholder: 'Please enter refund reason', + confirmRefund: 'Confirm Refund', + refundSuccess: 'Refund successful', + refundInfo: 'Refund Info', + refundEnabled: 'Refund Enabled', + allowUserRefund: 'Allow User Refund', + alreadyRefunded: 'Already Refunded', + deductBalance: 'Deduct Balance', + deductBalanceHint: 'Subtract recharged amount from user balance', + userBalance: 'User Balance', + orderAmount: 'Order Amount', + insufficientBalance: 'Insufficient balance — will deduct to $0', + noDeduction: 'Will NOT deduct user balance', + forceRefund: 'Force refund (ignore balance check)', + orderCancelled: 'Заказ отменён', + retry: 'Retry', + retrySuccess: 'Retry successful', + approveRefund: 'Approve Refund', + retryRefund: 'Retry Refund', + refundRequestInfo: 'Refund Request Info', + refundRequestedAt: 'Requested At', + refundRequestedBy: 'Requested By', + refundRequestReason: 'Request Reason', + auditLogs: 'Audit Logs', + operator: 'Operator', + channelName: 'Channel Name', + channelDescription: 'Channel Description', + createChannel: 'Create Channel', + editChannel: 'Edit Channel', + deleteChannel: 'Delete Channel', + deleteChannelConfirm: 'Are you sure you want to delete this channel?', + planName: 'Plan Name', + planDescription: 'Plan Description', + createPlan: 'Create Plan', + editPlan: 'Edit Plan', + deletePlan: 'Delete Plan', + deletePlanConfirm: 'Are you sure you want to delete this plan?', + originalPrice: 'Original Price', + price: 'Price', + validityDays: 'Validity (days)', + validityUnit: 'Validity Unit', + sortOrder: 'Sort Order', + forSale: 'For Sale', + onSale: 'On Sale', + offSale: 'Off Sale', + group: 'Группа', + groupId: 'Group ID', + features: 'Возможности', + featuresHint: 'One feature per line', + featuresPlaceholder: 'Enter plan features...', + providerManagement: 'Provider Management', + providerManagementDesc: 'Manage payment provider instances', + createProvider: 'Create Provider', + editProvider: 'Edit Provider', + deleteProvider: 'Delete Provider', + deleteProviderConfirm: 'Are you sure you want to delete this provider?', + providerName: 'Provider Name', + providerKey: 'Provider Key', + selectProviderKey: 'Select Provider Key', + providerConfig: 'Provider Config', + noProviders: 'No providers configured', + noProvidersHint: 'Create a provider instance to start accepting payments', + supportedTypes: 'Supported Payment Types', + supportedTypesHint: 'Select the payment types this provider supports', + rateMultiplier: 'Rate Multiplier', + dashboardTitle: 'Платежная панель', + dashboardDesc: 'Recharge order analytics and insights', + daySuffix: 'd', + paymentConfigTitle: 'Настройки платежей', + paymentConfigDesc: 'Configure payment providers and settings', + plansPageTitle: 'Subscription Plans', + plansPageDesc: 'Manage subscription plan configuration', + tabPlanConfig: 'Plan Configuration', + tabUserSubs: 'User Subscriptions', + selectGroup: 'Выберите группу', + groupRequired: 'Please select a subscription group', + priceRequired: 'Price must be greater than 0', + validityDaysRequired: 'Validity days must be greater than 0', + groupMissing: 'Missing', + groupInfo: 'Group Info', + platform: 'Platform', + rateMultiplierLabel: 'Тариф', + dailyLimit: 'Дневной лимит', + weeklyLimit: 'Недельный лимит', + monthlyLimit: 'Месячный лимит', + unlimited: 'Безлимитно', + searchUserSubs: 'Search user subscriptions...', + daily: 'D', + weekly: 'W', + monthly: 'M', + subsStatus: { + active: 'Активен', + expired: 'Истекла', + revoked: 'Revoked', + }, + }, + }, + +} From 211da78261b811736ce6bfe33c929232d847f556 Mon Sep 17 00:00:00 2001 From: Slavx Date: Thu, 28 May 2026 10:11:22 +0300 Subject: [PATCH 2/4] fix: refine Russian translations Co-authored-by: Qwen-Coder --- frontend/src/i18n/locales/ru.ts | 1770 +++++++++++++++---------------- 1 file changed, 885 insertions(+), 885 deletions(-) diff --git a/frontend/src/i18n/locales/ru.ts b/frontend/src/i18n/locales/ru.ts index 29f03e963c2..6c64276fc8f 100644 --- a/frontend/src/i18n/locales/ru.ts +++ b/frontend/src/i18n/locales/ru.ts @@ -136,9 +136,9 @@ export default { requests: 'Запросы', inputTokens: 'Входные токены', outputTokens: 'Выходные токены', - cacheCreationTokens: 'Cache Creation', - cacheReadTokens: 'Cache Read', - cacheWriteTokens: 'Cache Write', + cacheCreationTokens: 'Создание кэша', + cacheReadTokens: 'Чтение кэша', + cacheWriteTokens: 'Запись кэша', totalTokens: 'Всего токенов', cost: 'Стоимость', // Status @@ -146,9 +146,9 @@ export default { walletBalance: 'Баланс кошелька', // Ring card titles totalQuota: 'Общая квота', - limit5h: '5-Hour Limit', + limit5h: 'Лимит на 5 часов', limitDaily: 'Дневной лимит', - limit7d: '7-Day Limit', + limit7d: 'Лимит на 7 дней', limitWeekly: 'Недельный лимит', limitMonthly: 'Месячный лимит', // Detail rows @@ -162,88 +162,88 @@ export default { subscriptionExpires: 'Подписка истекает', // Usage stat cells todayRequests: 'Запросов сегодня', - todayInputTokens: 'Today Input', - todayOutputTokens: 'Today Output', + todayInputTokens: 'Вход сегодня', + todayOutputTokens: 'Выход сегодня', todayTokens: 'Токенов сегодня', - todayCacheCreation: 'Today Cache Creation', - todayCacheRead: 'Today Cache Read', + todayCacheCreation: 'Создание кэша сегодня', + todayCacheRead: 'Чтение кэша сегодня', todayCost: 'Расход сегодня', rpmTpm: 'RPM / TPM', totalRequests: 'Всего запросов', - totalInputTokens: 'Total Input', - totalOutputTokens: 'Total Output', + totalInputTokens: 'Всего входных', + totalOutputTokens: 'Всего выходных', totalTokensLabel: 'Всего токенов', - totalCacheCreation: 'Total Cache Creation', - totalCacheRead: 'Total Cache Read', + totalCacheCreation: 'Всего созданий кэша', + totalCacheRead: 'Всего чтений кэша', totalCost: 'Общая стоимость', avgDuration: 'Средняя длительность', // Messages - enterApiKey: 'Please enter an API Key', - querySuccess: 'Query successful', - queryFailed: 'Query failed', - queryFailedRetry: 'Query failed, please try again later', + enterApiKey: 'Введите API-ключ', + querySuccess: 'Запрос выполнен', + queryFailed: 'Запрос не удался', + queryFailedRetry: 'Не удалось выполнить запрос. Попробуйте позже.', noDailyUsage: 'Нет данных за дни', }, // Setup Wizard setup: { - title: 'Sub2API Setup', - description: 'Configure your Sub2API instance', + title: 'Настройка Sub2API', + description: 'Настройте ваш экземпляр Sub2API', database: { - title: 'Database Configuration', - description: 'Connect to your PostgreSQL database', - host: 'Host', - port: 'Port', + title: 'Настройка базы данных', + description: 'Подключение к базе PostgreSQL', + host: 'Хост', + port: 'Порт', username: 'Имя пользователя', password: 'Пароль', - databaseName: 'Database Name', - sslMode: 'SSL Mode', + databaseName: 'Имя базы данных', + sslMode: 'Режим SSL', passwordPlaceholder: 'Пароль', ssl: { disable: 'Отключить', - require: 'Require', - verifyCa: 'Verify CA', - verifyFull: 'Verify Full' + require: 'Требовать', + verifyCa: 'Проверять CA', + verifyFull: 'Полная проверка' } }, redis: { - title: 'Redis Configuration', - description: 'Connect to your Redis server', - host: 'Host', - port: 'Port', - password: 'Password (optional)', - database: 'Database', + title: 'Настройка Redis', + description: 'Подключение к серверу Redis', + host: 'Хост', + port: 'Порт', + password: 'Пароль (необязательно)', + database: 'База данных', passwordPlaceholder: 'Пароль', - enableTls: 'Enable TLS', - enableTlsHint: 'Use TLS when connecting to Redis (public CA certs)' + enableTls: 'Включить TLS', + enableTlsHint: 'Использовать TLS при подключении к Redis (публичные CA-сертификаты)' }, admin: { - title: 'Admin Account', - description: 'Create your administrator account', + title: 'Аккаунт администратора', + description: 'Создайте аккаунт администратора', email: 'Email', password: 'Пароль', - confirmPassword: 'Confirm Password', - passwordPlaceholder: 'Min 8 characters', - confirmPasswordPlaceholder: 'Confirm password', - passwordMismatch: 'Passwords do not match' + confirmPassword: 'Подтвердите пароль', + passwordPlaceholder: 'Минимум 8 символов', + confirmPasswordPlaceholder: 'Подтвердите пароль', + passwordMismatch: 'Пароли не совпадают' }, ready: { - title: 'Ready to Install', - description: 'Review your configuration and complete setup', - database: 'Database', + title: 'Готово к установке', + description: 'Проверьте настройки и завершите установку', + database: 'База данных', redis: 'Redis', - adminEmail: 'Admin Email' + adminEmail: 'Email администратора' }, status: { - testing: 'Testing...', - success: 'Connection Successful', - testConnection: 'Test Connection', - installing: 'Installing...', - completeInstallation: 'Complete Installation', - completed: 'Installation completed!', - redirecting: 'Redirecting to login page...', - restarting: 'Service is restarting, please wait...', - timeout: 'Service restart is taking longer than expected. Please refresh the page manually.' + testing: 'Проверка...', + success: 'Подключение успешно', + testConnection: 'Проверить подключение', + installing: 'Установка...', + completeInstallation: 'Завершить установку', + completed: 'Установка завершена!', + redirecting: 'Переход на страницу входа...', + restarting: 'Сервис перезапускается, подождите...', + timeout: 'Перезапуск занимает больше времени, чем ожидалось. Обновите страницу вручную.' } }, @@ -328,7 +328,7 @@ export default { today: 'Сегодня', tomorrow: 'Завтра', unknown: 'Неизвестно', - minutes: 'min', + minutes: 'мин', time: { never: 'Никогда', justNow: 'Только что', @@ -365,7 +365,7 @@ export default { accounts: 'Аккаунты', proxies: 'Прокси', redeemCodes: 'Коды активации', - ops: 'Ops', + ops: 'Операции', promoCodes: 'Промокоды', settings: 'Настройки', myAccount: 'Мой аккаунт', @@ -420,194 +420,194 @@ export default { USER_NOT_ACTIVE: 'Аккаунт отключён.', }, registrationFailed: 'Регистрация не удалась. Попробуйте снова.', - emailSuffixNotAllowed: 'This email domain is not allowed for registration.', + emailSuffixNotAllowed: 'Этот домен email не разрешён для регистрации.', emailSuffixNotAllowedWithAllowed: - 'This email domain is not allowed. Allowed domains: {suffixes}', - emailSuffixAllowedMore: 'and {count} more', - loginSuccess: 'Login successful! Welcome back.', - accountCreatedSuccess: 'Account created successfully! Welcome to {siteName}.', + 'Этот домен email не разрешён. Разрешённые домены: {suffixes}', + emailSuffixAllowedMore: 'и ещё {count}', + loginSuccess: 'Вход выполнен. С возвращением!', + accountCreatedSuccess: 'Аккаунт создан. Добро пожаловать в {siteName}!', reloginRequired: 'Сессия истекла. Войдите снова.', - turnstileExpired: 'Verification expired, please try again', - turnstileFailed: 'Verification failed, please try again', - completeVerification: 'Please complete the verification', - verifyYourEmail: 'Verify Your Email', - sessionExpired: 'Session expired', - sessionExpiredDesc: 'Please go back to the registration page and start again.', + turnstileExpired: 'Проверка истекла, попробуйте снова', + turnstileFailed: 'Проверка не удалась, попробуйте снова', + completeVerification: 'Завершите проверку', + verifyYourEmail: 'Подтвердите email', + sessionExpired: 'Сессия истекла', + sessionExpiredDesc: 'Вернитесь на страницу регистрации и начните заново.', verificationCode: 'Код подтверждения', - verificationCodeHint: 'Enter the 6-digit code sent to your email', - sendingCode: 'Sending...', + verificationCodeHint: 'Введите 6-значный код из письма', + sendingCode: 'Отправка...', sendCode: 'Отправить код', - clickToResend: 'Click to resend code', - resendCode: 'Resend verification code', - sendCodeDesc: "We'll send a verification code to", + clickToResend: 'Нажмите, чтобы отправить код повторно', + resendCode: 'Отправить код повторно', + sendCodeDesc: "Мы отправим код подтверждения на", codeSentSuccess: 'Код отправлен. Проверьте почту.', verifying: 'Проверка...', verifyAndCreate: 'Подтвердить и создать аккаунт', - resendCountdown: 'Resend code in {countdown}s', + resendCountdown: 'Повторная отправка через {countdown}с', backToRegistration: 'Назад к регистрации', - sendCodeFailed: 'Failed to send verification code. Please try again.', - verifyFailed: 'Verification failed. Please try again.', - codeRequired: 'Verification code is required', - invalidCode: 'Please enter a valid 6-digit code', + sendCodeFailed: 'Не удалось отправить код подтверждения. Попробуйте снова.', + verifyFailed: 'Проверка не удалась. Попробуйте снова.', + codeRequired: 'Введите код подтверждения', + invalidCode: 'Введите корректный 6-значный код', promoCodeLabel: 'Промокод', - promoCodePlaceholder: 'Enter promo code (optional)', - promoCodeValid: 'Valid! You will receive ${amount} bonus balance', - promoCodeInvalid: 'Invalid promo code', - promoCodeNotFound: 'Promo code not found', - promoCodeExpired: 'This promo code has expired', - promoCodeDisabled: 'This promo code is disabled', - promoCodeMaxUsed: 'This promo code has reached its usage limit', - promoCodeAlreadyUsed: 'You have already used this promo code', - promoCodeValidating: 'Promo code is being validated, please wait', - promoCodeInvalidCannotRegister: 'Invalid promo code. Please check and try again or clear the promo code field', + promoCodePlaceholder: 'Введите промокод (необязательно)', + promoCodeValid: 'Промокод принят! Вы получите бонус ${amount}', + promoCodeInvalid: 'Недействительный промокод', + promoCodeNotFound: 'Промокод не найден', + promoCodeExpired: 'Срок действия промокода истёк', + promoCodeDisabled: 'Промокод отключён', + promoCodeMaxUsed: 'Лимит использования промокода исчерпан', + promoCodeAlreadyUsed: 'Вы уже использовали этот промокод', + promoCodeValidating: 'Проверка промокода, подождите', + promoCodeInvalidCannotRegister: 'Недействительный промокод. Проверьте его или очистите поле.', invitationCodeLabel: 'Код приглашения', - invitationCodePlaceholder: 'Enter invitation code', - invitationCodeRequired: 'Invitation code is required', - invitationCodeValid: 'Invitation code is valid', - invitationCodeInvalid: 'Invalid or used invitation code', - invitationCodeValidating: 'Validating invitation code...', - invitationCodeInvalidCannotRegister: 'Invalid invitation code. Please check and try again', - oauthOrContinue: 'or continue with others', + invitationCodePlaceholder: 'Введите код приглашения', + invitationCodeRequired: 'Код приглашения обязателен', + invitationCodeValid: 'Код приглашения действителен', + invitationCodeInvalid: 'Недействительный или использованный код приглашения', + invitationCodeValidating: 'Проверка кода приглашения...', + invitationCodeInvalidCannotRegister: 'Недействительный код приглашения. Проверьте и попробуйте снова.', + oauthOrContinue: 'или продолжить другим способом', linuxdo: { - signIn: 'Continue with Linux.do', - orContinue: 'or continue with email', - callbackTitle: 'Signing you in', - callbackProcessing: 'Completing login, please wait...', - callbackHint: 'If you are not redirected automatically, go back to the login page and try again.', - callbackMissingToken: 'Missing login token, please try again.', - backToLogin: 'Back to Login', - invitationRequired: 'This Linux.do account is not yet registered. The site requires an invitation code — please enter one to complete registration.', - invalidPendingToken: 'The registration token has expired. Please sign in with Linux.do again.', - completeRegistration: 'Complete Registration', - completing: 'Completing registration…', - completeRegistrationFailed: 'Registration failed. Please check your invitation code and try again.' + signIn: 'Продолжить через Linux.do', + orContinue: 'или продолжить через email', + callbackTitle: 'Выполняется вход', + callbackProcessing: 'Завершаем вход, подождите...', + callbackHint: 'Если перенаправление не произошло, вернитесь на страницу входа и попробуйте снова.', + callbackMissingToken: 'Отсутствует токен входа, попробуйте снова.', + backToLogin: 'Назад ко входу', + invitationRequired: 'Этот аккаунт Linux.do ещё не зарегистрирован. Для регистрации нужен код приглашения.', + invalidPendingToken: 'Токен регистрации истёк. Войдите через Linux.do снова.', + completeRegistration: 'Завершить регистрацию', + completing: 'Завершение регистрации…', + completeRegistrationFailed: 'Регистрация не удалась. Проверьте код приглашения и попробуйте снова.' }, dingtalk: { - signIn: 'Continue with DingTalk', - callbackTitle: 'Signing you in with DingTalk', - callbackProcessing: 'Completing DingTalk login, please wait...', - callbackHint: 'If you are not redirected automatically, go back to the login page and try again.', - callbackMissingToken: 'Missing login token, please try again.', - backToLogin: 'Back to Login', - invitationRequired: 'This DingTalk account is not yet registered. The site requires an invitation code — please enter one to complete registration.', - invalidPendingToken: 'The registration token has expired. Please sign in with DingTalk again.', - completeRegistration: 'Complete Registration', - completing: 'Completing registration…', - completeRegistrationFailed: 'Registration failed. Please check your invitation code and try again.', - createAccountTitle: 'Create DingTalk Account', - registrationDisabledRedirectToBind: 'New account registration is currently disabled. Please bind to your existing account with its email and password.', + signIn: 'Продолжить через DingTalk', + callbackTitle: 'Вход через DingTalk', + callbackProcessing: 'Завершаем вход через DingTalk, подождите...', + callbackHint: 'Если перенаправление не произошло, вернитесь на страницу входа и попробуйте снова.', + callbackMissingToken: 'Отсутствует токен входа, попробуйте снова.', + backToLogin: 'Назад ко входу', + invitationRequired: 'Этот аккаунт DingTalk ещё не зарегистрирован. Для регистрации нужен код приглашения.', + invalidPendingToken: 'Токен регистрации истёк. Войдите через DingTalk снова.', + completeRegistration: 'Завершить регистрацию', + completing: 'Завершение регистрации…', + completeRegistrationFailed: 'Регистрация не удалась. Проверьте код приглашения и попробуйте снова.', + createAccountTitle: 'Создать аккаунт DingTalk', + registrationDisabledRedirectToBind: 'Регистрация новых аккаунтов отключена. Привяжите существующий аккаунт через email и пароль.', error: { - title: 'DingTalk Sign-in Failed', - csrf: 'Login session expired, please scan again', - corp_rejected: 'Your DingTalk account is not part of this organization. Please contact administrator', - dingtalk_not_enabled: 'DingTalk login is not enabled', - upstream_error: 'DingTalk service is temporarily unavailable. Please try again later', - missing_browser_session: 'Browser session lost. Please login again', - missing_params: 'Request parameters are incomplete', - invalid_state: 'Invalid login state', - provider_error: 'DingTalk authorization failed', - session_error: 'Failed to create session. Please retry', - retry: 'Retry Login' + title: 'Вход через DingTalk не удался', + csrf: 'Сессия входа истекла, отсканируйте снова', + corp_rejected: 'Ваш аккаунт DingTalk не входит в эту организацию. Обратитесь к администратору.', + dingtalk_not_enabled: 'Вход через DingTalk не включён', + upstream_error: 'Сервис DingTalk временно недоступен. Попробуйте позже.', + missing_browser_session: 'Сессия браузера потеряна. Войдите снова.', + missing_params: 'Параметры запроса неполные', + invalid_state: 'Недействительное состояние входа', + provider_error: 'Авторизация DingTalk не удалась', + session_error: 'Не удалось создать сессию. Повторите попытку.', + retry: 'Повторить вход' } }, emailOAuth: { - signIn: 'Continue with {providerName}' + signIn: 'Продолжить через {providerName}' }, oidc: { - signIn: 'Continue with {providerName}', - callbackTitle: 'Signing you in with {providerName}', - callbackProcessing: 'Completing login with {providerName}, please wait...', - callbackHint: 'If you are not redirected automatically, go back to the login page and try again.', - callbackMissingToken: 'Missing login token, please try again.', - backToLogin: 'Back to Login', + signIn: 'Продолжить через {providerName}', + callbackTitle: 'Вход через {providerName}', + callbackProcessing: 'Завершаем вход через {providerName}, подождите...', + callbackHint: 'Если перенаправление не произошло, вернитесь на страницу входа и попробуйте снова.', + callbackMissingToken: 'Отсутствует токен входа, попробуйте снова.', + backToLogin: 'Назад ко входу', invitationRequired: - 'This {providerName} account is not yet registered. The site requires an invitation code — please enter one to complete registration.', - invalidPendingToken: 'The registration token has expired. Please sign in again.', - completeRegistration: 'Complete Registration', - completing: 'Completing registration…', - completeRegistrationFailed: 'Registration failed. Please check your invitation code and try again.' + 'Этот аккаунт {providerName} ещё не зарегистрирован. Для регистрации нужен код приглашения.', + invalidPendingToken: 'Токен регистрации истёк. Войдите снова.', + completeRegistration: 'Завершить регистрацию', + completing: 'Завершение регистрации…', + completeRegistrationFailed: 'Регистрация не удалась. Проверьте код приглашения и попробуйте снова.' }, oauthFlow: { - profileDetailsTitle: 'Use {providerName} profile details', - profileDetailsDescription: 'Choose whether to apply the nickname or avatar from {providerName} to this account.', - useDisplayName: 'Use display name', - useAvatar: 'Use avatar', - avatarAlt: '{providerName} avatar', - reviewProfileBeforeContinue: 'Review the {providerName} profile details before continuing.', - chooseHowToContinue: 'Choose how to continue', - chooseAccountActionHint: 'Choose whether to bind an existing account or create a new one.', - suggestedEmail: 'Suggested email: {email}', - bindExistingAccount: 'Bind existing account', - createNewAccount: 'Create new account', - createAccountHint: 'Enter an email address to create your account and continue.', - bindLoginHint: 'Log in to an existing account to bind this {providerName} sign-in.', - signInThenBindDescription: 'Sign in to an existing account, then bind this {providerName} sign-in to it.', - bindSignInToExistingAccount: 'Bind this {providerName} sign-in to an existing account.', - bindCurrentAccountTitle: 'Bind the current account', - bindCurrentAccountDescription: 'Bind this {providerName} sign-in to the account currently signed in on this browser.', - bindCurrentAccount: 'Bind current account', - logInAndBind: 'Log in and bind', - useDifferentEmail: 'Use a different email', - backToOptions: 'Back to options', - yourAccount: 'your account', - totpHint: 'Enter the 6-digit verification code for {account} to finish binding this {providerName} sign-in.', - verifyAndContinue: 'Verify and continue', - wechatAvailabilityUnknown: 'WeChat sign-in availability could not be confirmed. Refresh and retry.', - wechatSystemBrowserOnly: 'This WeChat sign-in flow is only available in your system browser.', - wechatBrowserOnly: 'This WeChat sign-in flow is only available inside the WeChat browser.', - wechatNotConfigured: 'WeChat sign-in is not configured yet.' + profileDetailsTitle: 'Использовать данные профиля {providerName}', + profileDetailsDescription: 'Выберите, применять ли имя или аватар из {providerName} к этому аккаунту.', + useDisplayName: 'Использовать имя', + useAvatar: 'Использовать аватар', + avatarAlt: 'Аватар {providerName}', + reviewProfileBeforeContinue: 'Проверьте данные профиля {providerName} перед продолжением.', + chooseHowToContinue: 'Выберите способ продолжения', + chooseAccountActionHint: 'Выберите: привязать существующий аккаунт или создать новый.', + suggestedEmail: 'Предложенный email: {email}', + bindExistingAccount: 'Привязать существующий аккаунт', + createNewAccount: 'Создать новый аккаунт', + createAccountHint: 'Введите email, чтобы создать аккаунт и продолжить.', + bindLoginHint: 'Войдите в существующий аккаунт, чтобы привязать вход через {providerName}.', + signInThenBindDescription: 'Войдите в существующий аккаунт и привяжите к нему вход через {providerName}.', + bindSignInToExistingAccount: 'Привязать вход через {providerName} к существующему аккаунту.', + bindCurrentAccountTitle: 'Привязать текущий аккаунт', + bindCurrentAccountDescription: 'Привязать вход через {providerName} к аккаунту, открытому в этом браузере.', + bindCurrentAccount: 'Привязать текущий аккаунт', + logInAndBind: 'Войти и привязать', + useDifferentEmail: 'Использовать другой email', + backToOptions: 'Назад к вариантам', + yourAccount: 'ваш аккаунт', + totpHint: 'Введите 6-значный код для {account}, чтобы завершить привязку {providerName}.', + verifyAndContinue: 'Подтвердить и продолжить', + wechatAvailabilityUnknown: 'Не удалось проверить доступность входа через WeChat. Обновите страницу и повторите.', + wechatSystemBrowserOnly: 'Вход через WeChat доступен только в системном браузере.', + wechatBrowserOnly: 'Вход через WeChat доступен только в браузере WeChat.', + wechatNotConfigured: 'Вход через WeChat ещё не настроен.' }, - linuxdoCallbackPageTitle: 'LinuxDo Sign-In Callback', - dingtalkCallbackPageTitle: 'DingTalk Sign-In Callback', - oidcCallbackPageTitle: 'OIDC Sign-In Callback', - oauthCallbackPageTitle: 'OAuth Callback', + linuxdoCallbackPageTitle: 'Callback входа LinuxDo', + dingtalkCallbackPageTitle: 'Callback входа DingTalk', + oidcCallbackPageTitle: 'Callback входа OIDC', + oauthCallbackPageTitle: 'OAuth callback', wechatProviderName: 'WeChat', - wechatCallbackPageTitle: 'WeChat Sign-In Callback', - wechatPaymentCallbackPageTitle: 'WeChat Payment Callback', + wechatCallbackPageTitle: 'Callback входа WeChat', + wechatPaymentCallbackPageTitle: 'Callback платежа WeChat', wechatPayment: { - callbackTitle: 'Resuming WeChat payment', - callbackProcessing: 'Resuming WeChat payment...', - backToPayment: 'Back to payment', - callbackMissingResumeToken: 'The WeChat payment callback is missing the resume token.' + callbackTitle: 'Возобновление платежа WeChat', + callbackProcessing: 'Возобновление платежа WeChat...', + backToPayment: 'Назад к оплате', + callbackMissingResumeToken: 'В callback платежа WeChat отсутствует токен возобновления.' }, oauth: { - callbackTitle: 'OAuth Callback', - callbackHint: 'Copy the code and state back to the admin authorization flow when needed.', - invalidCallbackTitle: 'Invalid sign-in callback', - invalidCallbackHint: 'This page does not contain a valid authorization result. Return to the login page and start quick sign-in again.', - code: 'Code', - state: 'State', - fullUrl: 'Full URL' + callbackTitle: 'OAuth callback', + callbackHint: 'При необходимости скопируйте code и state обратно в admin authorization flow.', + invalidCallbackTitle: 'Недействительный callback входа', + invalidCallbackHint: 'Эта страница не содержит корректного результата авторизации. Вернитесь на страницу входа и начните быстрый вход заново.', + code: 'Код', + state: 'Состояние', + fullUrl: 'Полный URL' }, // Forgot password - forgotPassword: 'Forgot password?', - forgotPasswordTitle: 'Reset Your Password', - forgotPasswordHint: 'Enter your email address and we will send you a link to reset your password.', - sendResetLink: 'Send Reset Link', - sendingResetLink: 'Sending...', - sendResetLinkFailed: 'Failed to send reset link. Please try again.', - resetEmailSent: 'Reset Link Sent', - resetEmailSentHint: 'If an account exists with this email, you will receive a password reset link shortly. Please check your inbox and spam folder.', - backToLogin: 'Back to Login', - rememberedPassword: 'Remembered your password?', + forgotPassword: 'Забыли пароль?', + forgotPasswordTitle: 'Сброс пароля', + forgotPasswordHint: 'Введите email, и мы отправим ссылку для сброса пароля.', + sendResetLink: 'Отправить ссылку', + sendingResetLink: 'Отправка...', + sendResetLinkFailed: 'Не удалось отправить ссылку. Попробуйте снова.', + resetEmailSent: 'Ссылка отправлена', + resetEmailSentHint: 'Если аккаунт с таким email существует, ссылка для сброса скоро придёт на почту. Проверьте входящие и спам.', + backToLogin: 'Назад ко входу', + rememberedPassword: 'Вспомнили пароль?', // Reset password - resetPasswordTitle: 'Set New Password', - resetPasswordHint: 'Enter your new password below.', + resetPasswordTitle: 'Новый пароль', + resetPasswordHint: 'Введите новый пароль ниже.', newPassword: 'Новый пароль', - newPasswordPlaceholder: 'Enter your new password', - confirmPassword: 'Confirm Password', - confirmPasswordPlaceholder: 'Confirm your new password', - confirmPasswordRequired: 'Please confirm your password', - passwordsDoNotMatch: 'Passwords do not match', - resetPassword: 'Reset Password', - resettingPassword: 'Resetting...', - resetPasswordFailed: 'Failed to reset password. Please try again.', - passwordResetSuccess: 'Password Reset Successful', - passwordResetSuccessHint: 'Your password has been reset. You can now sign in with your new password.', - invalidResetLink: 'Invalid Reset Link', - invalidResetLinkHint: 'This password reset link is invalid or has expired. Please request a new one.', - requestNewResetLink: 'Request New Reset Link', - invalidOrExpiredToken: 'The password reset link is invalid or has expired. Please request a new one.' + newPasswordPlaceholder: 'Введите новый пароль', + confirmPassword: 'Подтвердите пароль', + confirmPasswordPlaceholder: 'Подтвердите новый пароль', + confirmPasswordRequired: 'Подтвердите пароль', + passwordsDoNotMatch: 'Пароли не совпадают', + resetPassword: 'Сбросить пароль', + resettingPassword: 'Сброс...', + resetPasswordFailed: 'Не удалось сбросить пароль. Попробуйте снова.', + passwordResetSuccess: 'Пароль сброшен', + passwordResetSuccessHint: 'Пароль сброшен. Теперь можно войти с новым паролем.', + invalidResetLink: 'Недействительная ссылка', + invalidResetLinkHint: 'Ссылка для сброса недействительна или истекла. Запросите новую.', + requestNewResetLink: 'Запросить новую ссылку', + invalidOrExpiredToken: 'Ссылка для сброса недействительна или истекла. Запросите новую.' }, // Dashboard @@ -620,8 +620,8 @@ export default { todayCost: 'Расход сегодня', todayTokens: 'Токенов сегодня', totalTokens: 'Всего токенов', - cacheToday: 'Cache (Today)', - performance: 'Performance', + cacheToday: 'Кэш сегодня', + performance: 'Производительность', avgResponse: 'Средний ответ', averageTime: 'Среднее время', timeRange: 'Период', @@ -629,32 +629,32 @@ export default { day: 'День', hour: 'Час', modelDistribution: 'Распределение моделей', - groupDistribution: 'Group Usage Distribution', - platformBreakdown: 'Per-platform Breakdown', - platformBreakdownEmpty: 'No platform usage yet', - platformCount: '{count} platforms', - platformOther: 'Other', + groupDistribution: 'Распределение расхода по группам', + platformBreakdown: 'Разбивка по платформам', + platformBreakdownEmpty: 'Расхода по платформам пока нет', + platformCount: 'Платформ: {count}', + platformOther: 'Другое', platformQuota: { - title: 'Quota Usage', - daily: 'Daily', - weekly: 'Weekly', - monthly: 'Monthly (30-day rolling)', - resetsAt: 'Resets {time}', - noLimit: 'unlimited', + title: 'Использование квоты', + daily: 'Дневная', + weekly: 'Недельная', + monthly: 'Месячный (скользящие 30 дней)', + resetsAt: 'Сброс {time}', + noLimit: 'безлимитно', disabled: 'Отключено', }, - tokenUsageTrend: 'Token Usage Trend', + tokenUsageTrend: 'Динамика расхода токенов', noDataAvailable: 'Нет данных', model: 'Модель', group: 'Группа', - noGroup: 'No Group', + noGroup: 'Без группы', requests: 'Запросы', - tokens: 'Tokens', - actual: 'Actual', - standard: 'Standard', - input: 'Input', - output: 'Output', - cache: 'Cache', + tokens: 'Токены', + actual: 'Факт', + standard: 'Стандарт', + input: 'Вход', + output: 'Выход', + cache: 'Кэш', recentUsage: 'Последний расход', last7Days: 'Последние 7 дней', noUsageRecords: 'Нет записей расхода', @@ -671,7 +671,7 @@ export default { // Groups (shared) groups: { - subscription: 'Sub' + subscription: 'Подписка' }, // API Keys @@ -692,7 +692,7 @@ export default { createKey: 'Создать API-ключ', editKey: 'Изменить API-ключ', deleteKey: 'Удалить API-ключ', - deleteConfirmMessage: "Are you sure you want to delete '{name}'? This action cannot be undone.", + deleteConfirmMessage: "Удалить '{name}'? Это действие нельзя отменить.", apiKey: 'API-ключ', group: 'Группа', noGroup: 'Без группы', @@ -700,8 +700,8 @@ export default { noGroupFound: 'Группы не найдены', created: 'Создан', copyToClipboard: 'Копировать', - copied: 'Copied!', - importToCcSwitch: 'Import to CCS', + copied: 'Скопировано!', + importToCcSwitch: 'Импорт в CCS', enable: 'Включить', disable: 'Отключить', nameLabel: 'Имя', @@ -712,35 +712,35 @@ export default { selectStatus: 'Выберите статус', saving: 'Сохранение...', noKeysYet: 'API-ключей пока нет', - createFirstKey: 'Create your first API key to get started with the API.', + createFirstKey: 'Создайте первый API-ключ, чтобы начать работу.', keyCreatedSuccess: 'API-ключ создан', keyUpdatedSuccess: 'API-ключ обновлён', keyDeletedSuccess: 'API-ключ удалён', - keyEnabledSuccess: 'API key enabled successfully', - keyDisabledSuccess: 'API key disabled successfully', - failedToLoad: 'Failed to load API keys', - failedToSave: 'Failed to save API key', - failedToDelete: 'Failed to delete API key', - failedToUpdateStatus: 'Failed to update API key status', - clickToChangeGroup: 'Click to change group', - groupChangedSuccess: 'Group changed successfully', - failedToChangeGroup: 'Failed to change group', - groupRequired: 'Please select a group', + keyEnabledSuccess: 'API-ключ включён', + keyDisabledSuccess: 'API-ключ отключён', + failedToLoad: 'Не удалось загрузить API-ключи', + failedToSave: 'Не удалось сохранить API-ключ', + failedToDelete: 'Не удалось удалить API-ключ', + failedToUpdateStatus: 'Не удалось обновить статус API-ключа', + clickToChangeGroup: 'Нажмите, чтобы сменить группу', + groupChangedSuccess: 'Группа изменена', + failedToChangeGroup: 'Не удалось изменить группу', + groupRequired: 'Выберите группу', usage: 'Расход', today: 'Сегодня', - total: 'Last 30d', - quota: 'Quota', - lastUsedAt: 'Last Used', - useKey: 'Use Key', + total: 'Последние 30 дн.', + quota: 'Квота', + lastUsedAt: 'Последнее использование', + useKey: 'Использовать ключ', useKeyModal: { - title: 'Use API Key', + title: 'Использовать API-ключ', description: - 'Add the following environment variables to your terminal profile or run directly in terminal to configure API access.', + 'Добавьте эти переменные окружения в профиль терминала или выполните в терминале для настройки API-доступа.', copy: 'Копировать', copied: 'Скопировано', note: 'These environment variables will be active in the current terminal session. For permanent configuration, add them to ~/.bashrc, ~/.zshrc, or the appropriate configuration file.', - noGroupTitle: 'Please assign a group first', - noGroupDescription: 'This API key has not been assigned to a group. Please click the group column in the key list to assign one before viewing the configuration.', + noGroupTitle: 'Сначала назначьте группу', + noGroupDescription: 'Этот API-ключ не назначен группе. Сначала выберите группу в списке ключей.', openai: { description: 'Add the following configuration files to your Codex CLI config directory.', configTomlHint: 'Make sure the following content is at the beginning of the config.toml file', @@ -772,68 +772,68 @@ export default { hint: 'Config path: ~/.config/opencode/opencode.json (or opencode.jsonc), create if not exists. Use default providers (openai/anthropic/google) or custom provider_id. API Key can be configured directly or via /connect command. This is an example, adjust models and options as needed.', }, }, - customKeyLabel: 'Custom Key', - customKeyPlaceholder: 'Enter your custom key (min 16 chars)', - customKeyHint: 'Only letters, numbers, underscores and hyphens allowed. Minimum 16 characters.', - customKeyTooShort: 'Custom key must be at least 16 characters', - customKeyInvalidChars: 'Custom key can only contain letters, numbers, underscores, and hyphens', - customKeyRequired: 'Please enter a custom key', - ipRestriction: 'IP Restriction', - ipWhitelist: 'IP Whitelist', + customKeyLabel: 'Свой ключ', + customKeyPlaceholder: 'Введите свой ключ (мин. 16 символов)', + customKeyHint: 'Только буквы, цифры, подчёркивания и дефисы. Минимум 16 символов.', + customKeyTooShort: 'Свой ключ должен быть не короче 16 символов', + customKeyInvalidChars: 'Свой ключ может содержать только буквы, цифры, подчёркивания и дефисы', + customKeyRequired: 'Введите свой ключ', + ipRestriction: 'Ограничение по IP', + ipWhitelist: 'Белый список IP', ipWhitelistPlaceholder: '192.168.1.100\n10.0.0.0/8', - ipWhitelistHint: 'One IP or CIDR per line. Only these IPs can use this key when set.', - ipBlacklist: 'IP Blacklist', + ipWhitelistHint: 'Один IP или CIDR на строку. Если задано, только эти IP смогут использовать ключ.', + ipBlacklist: 'Чёрный список IP', ipBlacklistPlaceholder: '1.2.3.4\n5.6.0.0/16', - ipBlacklistHint: 'One IP or CIDR per line. These IPs will be blocked from using this key.', - ipRestrictionEnabled: 'IP restriction enabled', + ipBlacklistHint: 'Один IP или CIDR на строку. Эти IP будут заблокированы для ключа.', + ipRestrictionEnabled: 'Ограничение по IP включено', ccSwitchNotInstalled: 'CC-Switch is not installed or the protocol handler is not registered. Please install CC-Switch first or manually copy the API key.', ccsClientSelect: { - title: 'Select Client', - description: 'Please select the client type to import to CC-Switch:', + title: 'Выберите клиент', + description: 'Выберите тип клиента для импорта в CC-Switch:', claudeCode: 'Claude Code', - claudeCodeDesc: 'Import as Claude Code configuration', + claudeCodeDesc: 'Импортировать как конфигурацию Claude Code', geminiCli: 'Gemini CLI', - geminiCliDesc: 'Import as Gemini CLI configuration', + geminiCliDesc: 'Импортировать как конфигурацию Gemini CLI', }, // Quota and expiration - quotaLimit: 'Quota Limit', - quotaAmount: 'Quota Amount (USD)', - quotaAmountPlaceholder: 'Enter quota limit in USD', - quotaAmountHint: 'Set the maximum amount this key can spend. 0 = unlimited.', - quotaUsed: 'Quota Used', + quotaLimit: 'Лимит квоты', + quotaAmount: 'Размер квоты (USD)', + quotaAmountPlaceholder: 'Введите лимит квоты в USD', + quotaAmountHint: 'Максимальная сумма расхода для ключа. 0 = без лимита.', + quotaUsed: 'Квота использована', reset: 'Сбросить', - resetQuotaUsed: 'Reset used quota to 0', - resetQuotaTitle: 'Confirm Reset Quota', - resetQuotaConfirmMessage: 'Are you sure you want to reset the used quota (${used}) for key "{name}" to 0? This action cannot be undone.', - quotaResetSuccess: 'Quota reset successfully', - failedToResetQuota: 'Failed to reset quota', - rateLimitColumn: 'Rate Limit', - rateLimitSection: 'Rate Limit', + resetQuotaUsed: 'Сбросить использованную квоту в 0', + resetQuotaTitle: 'Подтвердите сброс квоты', + resetQuotaConfirmMessage: 'Сбросить использованную квоту (${used}) для ключа "{name}" в 0? Это действие нельзя отменить.', + quotaResetSuccess: 'Квота сброшена', + failedToResetQuota: 'Не удалось сбросить квоту', + rateLimitColumn: 'Лимит частоты', + rateLimitSection: 'Лимит частоты', resetUsage: 'Сбросить', - rateLimit5h: '5-Hour Limit (USD)', - rateLimit1d: 'Daily Limit (USD)', - rateLimit7d: '7-Day Limit (USD)', - rateLimitHint: 'Set the maximum spending for this key within each time window. 0 = unlimited.', - rateLimitUsage: 'Rate Limit Usage', - resetRateLimitUsage: 'Reset Rate Limit Usage', - resetRateLimitTitle: 'Confirm Reset Rate Limit', + rateLimit5h: 'Лимит на 5 часов (USD)', + rateLimit1d: 'Дневной лимит (USD)', + rateLimit7d: 'Лимит на 7 дней (USD)', + rateLimitHint: 'Максимальный расход ключа в каждом периоде. 0 = без лимита.', + rateLimitUsage: 'Использование лимита частоты', + resetRateLimitUsage: 'Сбросить расход лимита частоты', + resetRateLimitTitle: 'Подтвердите сброс лимита частоты', resetRateLimitConfirmMessage: 'Are you sure you want to reset the rate limit usage for key "{name}"? All time window usage will be reset to zero. This action cannot be undone.', - rateLimitResetSuccess: 'Rate limit usage reset successfully', - failedToResetRateLimit: 'Failed to reset rate limit usage', + rateLimitResetSuccess: 'Расход лимита частоты сброшен', + failedToResetRateLimit: 'Не удалось сбросить расход лимита частоты', resetNow: 'Скоро сброс', - expiration: 'Expiration', - expiresInDays: '{days} days', - extendDays: '+{days} days', + expiration: 'Срок действия', + expiresInDays: '{days} дн.', + extendDays: '+{days} дн.', customDate: 'Свой', - expirationDate: 'Expiration Date', - expirationDateHint: 'Select when this API key should expire.', - currentExpiration: 'Current expiration', - expiresAt: 'Expires', + expirationDate: 'Дата истечения', + expirationDateHint: 'Выберите срок действия API-ключа.', + currentExpiration: 'Текущий срок действия', + expiresAt: 'Истекает', noExpiration: 'Никогда', status: { active: 'Активен', inactive: 'Неактивен', - quota_exhausted: 'Quota Exhausted', + quota_exhausted: 'Квота исчерпана', expired: 'Истекла', }, }, @@ -844,103 +844,103 @@ export default { description: 'Просматривайте и анализируйте историю использования API', costDetails: 'Разбивка стоимости', tokenDetails: 'Разбивка токенов', - cacheTtlOverriddenHint: 'Cache TTL Override enabled', - cacheTtlOverriddenLabel: 'TTL Override', - cacheTtlOverridden5m: 'Billed as 5m', - cacheTtlOverridden1h: 'Billed as 1h', + cacheTtlOverriddenHint: 'Переопределение TTL кэша включено', + cacheTtlOverriddenLabel: 'Переопределение TTL', + cacheTtlOverridden5m: 'Тарифицируется как 5 мин', + cacheTtlOverridden1h: 'Тарифицируется как 1 ч', totalRequests: 'Всего запросов', totalTokens: 'Всего токенов', totalCost: 'Общая стоимость', - standardCost: 'Standard', - actualCost: 'Actual', + standardCost: 'Стандарт', + actualCost: 'Факт', accountCost: 'Стоимость', - userBilled: 'User billed', - accountBilled: 'Account billed', - accountMultiplier: 'Account rate', + userBilled: 'Списано с пользователя', + accountBilled: 'Списано с аккаунта', + accountMultiplier: 'Тариф аккаунта', avgDuration: 'Средняя длительность', - inSelectedRange: 'in selected range', - perRequest: 'per request', + inSelectedRange: 'за выбранный период', + perRequest: 'за запрос', apiKeyFilter: 'API-ключ', allApiKeys: 'Все API-ключи', timeRange: 'Период', exportCsv: 'Экспорт CSV', exportExcel: 'Экспорт Excel', exportingProgress: 'Экспорт данных...', - exportedCount: 'Exported {current}/{total} records', - estimatedTime: 'Estimated time remaining: {time}', - cancelExport: 'Cancel Export', - exportCancelled: 'Export cancelled', - exporting: 'Exporting...', - preparingExport: 'Preparing export...', + exportedCount: 'Экспортировано {current}/{total} записей', + estimatedTime: 'Осталось примерно: {time}', + cancelExport: 'Отменить экспорт', + exportCancelled: 'Экспорт отменён', + exporting: 'Экспорт...', + preparingExport: 'Подготовка экспорта...', model: 'Модель', - requestedModel: 'Requested', - upstreamModel: 'Upstream', - reasoningEffort: 'Reasoning Effort', + requestedModel: 'Запрошенная', + upstreamModel: 'Апстрим', + reasoningEffort: 'Уровень рассуждения', endpoint: 'Endpoint', - endpointDistribution: 'Endpoint Distribution', - inbound: 'Inbound', - upstream: 'Upstream', - mapping: 'Mapping', - path: 'Path', - inboundEndpoint: 'Inbound Endpoint', - upstreamEndpoint: 'Upstream Endpoint', - type: 'Type', - tokens: 'Tokens', + endpointDistribution: 'Распределение endpoint-ов', + inbound: 'Входящий', + upstream: 'Апстрим', + mapping: 'Сопоставление', + path: 'Путь', + inboundEndpoint: 'Входящий endpoint', + upstreamEndpoint: 'Апстрим endpoint', + type: 'Тип', + tokens: 'Токены', cost: 'Стоимость', - firstToken: 'First Token', - duration: 'Duration', - time: 'Time', + firstToken: 'Первый токен', + duration: 'Длительность', + time: 'Время', ws: 'WS', stream: 'Stream', sync: 'Sync', unknown: 'Неизвестно', - in: 'In', - out: 'Out', - inputTokenPrice: 'Input price', - outputTokenPrice: 'Output price', - perMillionTokens: '/ 1M tokens', - unitPrice: 'Per-request price', - imageUnitPrice: 'Per-image price', - imageTotalPrice: 'Image total price', - imageCount: 'Image count', - imageBillingSize: 'Billing size', - imageInputSize: 'Input size', - imageOutputSize: 'Output size', - imageSizeSource: 'Size source', - imageSizeBreakdown: 'Size breakdown', - imageSizeSourceOutput: 'Upstream output', - imageSizeSourceInput: 'Request input', - imageSizeSourceDefault: 'Default billing tier', - imageSizeSourceLegacy: 'Legacy record', - imageSizeSourceMissing: 'Not recorded', - imageSizeNotRecorded: 'not recorded', - imageSizeLegacyUnstandardized: 'legacy unstandardized', - imageSizeUnknown: 'unknown', - cacheRead: 'Read', - cacheWrite: 'Write', - serviceTier: 'Service tier', - serviceTierPriority: 'Fast', - serviceTierFlex: 'Flex', - serviceTierStandard: 'Standard', + in: 'Вход', + out: 'Выход', + inputTokenPrice: 'Цена входа', + outputTokenPrice: 'Цена выхода', + perMillionTokens: '/ 1 млн токенов', + unitPrice: 'Цена за запрос', + imageUnitPrice: 'Цена за изображение', + imageTotalPrice: 'Итог по изображениям', + imageCount: 'Кол-во изображений', + imageBillingSize: 'Размер для биллинга', + imageInputSize: 'Размер входа', + imageOutputSize: 'Размер выхода', + imageSizeSource: 'Источник размера', + imageSizeBreakdown: 'Разбивка размера', + imageSizeSourceOutput: 'Выход апстрима', + imageSizeSourceInput: 'Вход запроса', + imageSizeSourceDefault: 'Тариф по умолчанию', + imageSizeSourceLegacy: 'Старая запись', + imageSizeSourceMissing: 'Не записано', + imageSizeNotRecorded: 'не записано', + imageSizeLegacyUnstandardized: 'старый нестандартизированный', + imageSizeUnknown: 'неизвестно', + cacheRead: 'Чтение', + cacheWrite: 'Запись', + serviceTier: 'Уровень сервиса', + serviceTierPriority: 'Быстрый', + serviceTierFlex: 'Гибкий', + serviceTierStandard: 'Стандарт', rate: 'Тариф', - original: 'Original', - billed: 'Billed', + original: 'Исходная', + billed: 'Списано', noRecords: 'Записи расхода не найдены. Попробуйте изменить фильтры.', failedToLoad: 'Не удалось загрузить логи расхода', noDataToExport: 'Нет данных для экспорта', exportSuccess: 'Данные расхода экспортированы', - exportFailed: 'Failed to export usage data', - exportExcelSuccess: 'Usage data exported successfully (Excel format)', - exportExcelFailed: 'Failed to export usage data', - imageUnit: ' images', + exportFailed: 'Не удалось экспортировать данные расхода', + exportExcelSuccess: 'Данные расхода экспортированы в Excel', + exportExcelFailed: 'Не удалось экспортировать данные расхода', + imageUnit: ' изображений', userAgent: 'User-Agent' }, // Shared keys for channel monitor (admin + user views) monitorCommon: { status: { - operational: 'Operational', - degraded: 'Degraded', + operational: 'Работает', + degraded: 'Проблемы', failed: 'Ошибка', error: 'Ошибка', unknown: '-' @@ -950,21 +950,21 @@ export default { anthropic: 'Anthropic', gemini: 'Gemini' }, - extraModelsHeader: 'Extra Models', - extraModelsEmpty: 'No extra models', + extraModelsHeader: 'Дополнительные модели', + extraModelsEmpty: 'Нет дополнительных моделей', latencyEmpty: '-', - availabilityPrefix: 'Availability', - dialogLatency: 'Dialog Latency', + availabilityPrefix: 'Доступность', + dialogLatency: 'Задержка диалога', endpointPing: 'Endpoint PING', - history60pts: 'HISTORY ({n} PTS)', - nextUpdateIn: 'NEXT UPDATE IN {n}s', - past: 'PAST', - now: 'NOW', - maintenancePaused: 'Maintenance · timeline paused', - extraModelsCount: '+ {n} models', - pollEvery: '{n}s polling', - updatedAt: 'Updated {time}', - relativeSecondsAgo: '{n}s ago', + history60pts: 'ИСТОРИЯ ({n} ТОЧ.)', + nextUpdateIn: 'ОБНОВЛЕНИЕ ЧЕРЕЗ {n}с', + past: 'ПРОШЛОЕ', + now: 'СЕЙЧАС', + maintenancePaused: 'Обслуживание · таймлайн приостановлен', + extraModelsCount: '+ {n} моделей', + pollEvery: 'Опрос каждые {n}с', + updatedAt: 'Обновлено {time}', + relativeSecondsAgo: '{n} с назад', relativeMinutesAgo: '{n} мин назад', relativeHoursAgo: '{n} ч назад', relativeDaysAgo: '{n} дн назад' @@ -973,126 +973,126 @@ export default { // Channel Status (user-facing read-only view) channelStatus: { title: 'Статус каналов', - description: 'Inspect channel availability, latency and recent status', - searchPlaceholder: 'Search channels...', - allProviders: 'All Providers', - loadError: 'Failed to load channel status', - detailLoadError: 'Failed to load channel detail', - detailTitle: 'Channel Detail', + description: 'Проверяйте доступность каналов, задержку и последние статусы', + searchPlaceholder: 'Поиск каналов...', + allProviders: 'Все провайдеры', + loadError: 'Не удалось загрузить статус каналов', + detailLoadError: 'Не удалось загрузить детали канала', + detailTitle: 'Детали канала', closeDetail: 'Закрыть', windowTab: { - '7d': '7 days', - '15d': '15 days', - '30d': '30 days' + '7d': '7 дней', + '15d': '15 дней', + '30d': '30 дней' }, overall: { - operational: 'OPERATIONAL', - degraded: 'DEGRADED', - unavailable: 'UNAVAILABLE' + operational: 'РАБОТАЕТ', + degraded: 'ПРОБЛЕМЫ', + unavailable: 'НЕДОСТУПЕН' }, columns: { name: 'Имя', - provider: 'Provider', + provider: 'Провайдер', groupName: 'Группа', - primaryModel: 'Primary Model', - availability7d: '7d Availability', - latency: 'Latency (ms)' + primaryModel: 'Основная модель', + availability7d: 'Доступность 7 дн.', + latency: 'Задержка (мс)' }, detailColumns: { model: 'Модель', - latestStatus: 'Latest Status', - latestLatency: 'Latest Latency (ms)', - availability7d: '7d Availability', - availability15d: '15d Availability', - availability30d: '30d Availability', - avgLatency7d: '7d Avg Latency (ms)' + latestStatus: 'Последний статус', + latestLatency: 'Последняя задержка (мс)', + availability7d: 'Доступность 7 дн.', + availability15d: 'Доступность 15 дн.', + availability30d: 'Доступность 30 дн.', + avgLatency7d: 'Средняя задержка 7 дн. (мс)' }, empty: { - title: 'No channels available', - description: 'No monitored channels have been configured yet.' + title: 'Нет доступных каналов', + description: 'Отслеживаемые каналы ещё не настроены.' } }, // Available Channels (user-facing) availableChannels: { title: 'Доступные каналы', - description: 'Channels you can access, along with their supported models and pricing', - searchPlaceholder: 'Search channels or models...', - empty: 'No available channels', - noModels: 'No models configured', - noPricing: 'Pricing not configured', - exclusive: 'Exclusive', - public: 'Public', - exclusiveTooltip: 'Exclusive groups granted to you by an admin', - publicTooltip: 'Groups open to all users', + description: 'Доступные вам каналы, поддерживаемые модели и цены', + searchPlaceholder: 'Поиск каналов или моделей...', + empty: 'Нет доступных каналов', + noModels: 'Модели не настроены', + noPricing: 'Цены не настроены', + exclusive: 'Эксклюзивная', + public: 'Публичная', + exclusiveTooltip: 'Эксклюзивные группы, выданные администратором', + publicTooltip: 'Группы, открытые всем пользователям', columns: { - name: 'Channel', - description: 'Description', - platform: 'Platform', - groups: 'Your Accessible Groups', - supportedModels: 'Supported Models' + name: 'Канал', + description: 'Описание', + platform: 'Платформа', + groups: 'Ваши доступные группы', + supportedModels: 'Поддерживаемые модели' }, pricing: { - billingMode: 'Billing Mode', - billingModeToken: 'Per Token', - billingModePerRequest: 'Per Request', - billingModeImage: 'Per Image', - inputPrice: 'Input', - outputPrice: 'Output', - cacheWritePrice: 'Cache Write', - cacheReadPrice: 'Cache Read', - imageOutputPrice: 'Image Output', - perRequestPrice: 'Per Request', - intervals: 'Tiered Pricing', - unitPerMillion: '/ 1M tokens', - unitPerRequest: '/ request' + billingMode: 'Режим биллинга', + billingModeToken: 'За токен', + billingModePerRequest: 'За запрос', + billingModeImage: 'За изображение', + inputPrice: 'Вход', + outputPrice: 'Выход', + cacheWritePrice: 'Запись кэша', + cacheReadPrice: 'Чтение кэша', + imageOutputPrice: 'Выход изображения', + perRequestPrice: 'За запрос', + intervals: 'Ступенчатые цены', + unitPerMillion: '/ 1 млн токенов', + unitPerRequest: '/ запрос' } }, affiliate: { title: 'Партнёрские бонусы', - description: 'Invite new users and convert your rebate quota into account balance', - yourCode: 'Your Affiliate Code', - inviteLink: 'Invite Link', - copyCode: 'Copy Code', - copyLink: 'Copy Link', - codeCopied: 'Affiliate code copied', - linkCopied: 'Invite link copied', - loadFailed: 'Failed to load affiliate data', - transferFailed: 'Failed to transfer affiliate quota', + description: 'Приглашайте пользователей и переводите партнёрские бонусы на баланс', + yourCode: 'Ваш партнёрский код', + inviteLink: 'Ссылка приглашения', + copyCode: 'Скопировать код', + copyLink: 'Скопировать ссылку', + codeCopied: 'Партнёрский код скопирован', + linkCopied: 'Ссылка приглашения скопирована', + loadFailed: 'Не удалось загрузить партнёрские данные', + transferFailed: 'Не удалось перевести партнёрскую квоту', stats: { - rebateRate: 'My Rebate Rate', - rebateRateHint: 'What you earn each time an invitee recharges', - invitedUsers: 'Invited Users', - availableQuota: 'Available Rebate Quota', - frozenQuota: 'Frozen', - frozenQuotaHint: 'Recently earned rebates pending release', - totalQuota: 'Historical Rebate Quota' + rebateRate: 'Моя ставка бонуса', + rebateRateHint: 'Сколько вы получаете при пополнении приглашённого пользователя', + invitedUsers: 'Приглашённые пользователи', + availableQuota: 'Доступная бонусная квота', + frozenQuota: 'Заморожено', + frozenQuotaHint: 'Недавние бонусы ожидают разблокировки', + totalQuota: 'Бонусная квота за всё время' }, transfer: { - title: 'Transfer Rebate Quota', - description: 'Move available rebate quota into your account balance', - button: 'Transfer to Balance', - transferring: 'Transferring...', - empty: 'No available rebate quota', - success: '{amount} has been transferred to your balance' + title: 'Перевести бонусную квоту', + description: 'Переведите доступную бонусную квоту на баланс аккаунта', + button: 'Перевести на баланс', + transferring: 'Перевод...', + empty: 'Нет доступной бонусной квоты', + success: '{amount} переведено на ваш баланс' }, invitees: { - title: 'Invited Users', - empty: 'No invited users yet', + title: 'Приглашённые пользователи', + empty: 'Приглашённых пользователей пока нет', columns: { email: 'Email', username: 'Имя пользователя', - rebate: 'Rebate', - joinedAt: 'Joined At' + rebate: 'Бонус', + joinedAt: 'Дата регистрации' } }, tips: { - title: 'How It Works', - line1: 'Share your affiliate code or invite link with new users.', - line2: 'When invitees recharge, you receive {rate} of the recharge as rebate quota.', - line3: 'Transfer rebate quota to balance at any time.', - line4: 'Newly earned rebates may have a waiting period before they can be transferred.' + title: 'Как это работает', + line1: 'Поделитесь партнёрским кодом или ссылкой с новыми пользователями.', + line2: 'Когда приглашённые пополняют баланс, вы получаете {rate} от пополнения как бонусную квоту.', + line3: 'Переводите бонусную квоту на баланс в любое время.', + line4: 'Для новых бонусов может быть период ожидания перед переводом.' } }, @@ -1111,32 +1111,32 @@ export default { redeemSuccess: 'Код успешно активирован!', redeemFailed: 'Активация не удалась', added: 'Добавлено', - concurrentRequests: 'concurrent requests', + concurrentRequests: 'одновременных запросов', newBalance: 'Новый баланс', newConcurrency: 'Новый параллелизм', aboutCodes: 'О кодах активации', codeRule1: 'Каждый код можно использовать только один раз', - codeRule2: 'Codes may add balance, increase concurrency, or grant trial access', - codeRule3: 'Contact support if you have issues redeeming a code', - codeRule4: 'Balance and concurrency updates are immediate', - recentActivity: 'Recent Activity', - historyWillAppear: 'Your redemption history will appear here', - balanceAddedRedeem: 'Balance Added (Redeem)', - balanceAddedAffiliate: 'Balance Added (Affiliate Transfer)', - balanceAddedAdmin: 'Balance Added (Admin)', - balanceDeductedAdmin: 'Balance Deducted (Admin)', - concurrencyAddedRedeem: 'Concurrency Added (Redeem)', - concurrencyAddedAdmin: 'Concurrency Added (Admin)', - concurrencyReducedAdmin: 'Concurrency Reduced (Admin)', - adminAdjustment: 'Admin Adjustment', - subscriptionAssigned: 'Subscription Assigned', - subscriptionAssignedDesc: 'You have been granted access to {groupName}', - subscriptionDays: '{days} days', + codeRule2: 'Коды могут пополнять баланс, увеличивать параллелизм или выдавать тестовый доступ', + codeRule3: 'Обратитесь в поддержку, если не получается активировать код', + codeRule4: 'Баланс и параллелизм обновляются сразу', + recentActivity: 'Последняя активность', + historyWillAppear: 'Здесь появится история активаций', + balanceAddedRedeem: 'Баланс пополнен кодом', + balanceAddedAffiliate: 'Баланс пополнен партнёрским переводом', + balanceAddedAdmin: 'Баланс пополнен администратором', + balanceDeductedAdmin: 'Баланс списан администратором', + concurrencyAddedRedeem: 'Параллелизм увеличен кодом', + concurrencyAddedAdmin: 'Параллелизм увеличен администратором', + concurrencyReducedAdmin: 'Параллелизм уменьшен администратором', + adminAdjustment: 'Корректировка администратора', + subscriptionAssigned: 'Подписка назначена', + subscriptionAssignedDesc: 'Вам выдан доступ к {groupName}', + subscriptionDays: '{days} дн.', days: ' days', - codeRedeemSuccess: 'Code redeemed successfully!', - failedToRedeem: 'Failed to redeem code. Please check the code and try again.', - subscriptionRefreshFailed: 'Redeemed successfully, but failed to refresh subscription status.', - pleaseEnterCode: 'Please enter a redeem code' + codeRedeemSuccess: 'Код успешно активирован!', + failedToRedeem: 'Не удалось активировать код. Проверьте код и попробуйте снова.', + subscriptionRefreshFailed: 'Код активирован, но не удалось обновить статус подписки.', + pleaseEnterCode: 'Введите код активации' }, // Profile @@ -1145,17 +1145,17 @@ export default { description: 'Управляйте информацией и настройками аккаунта', accountBalance: 'Баланс аккаунта', concurrencyLimit: 'Лимит параллелизма', - rpmLimit: 'RPM Limit', + rpmLimit: 'Лимит RPM', rpmUnlimited: 'Безлимитно', memberSince: 'Дата регистрации', overviewTitle: 'Обзор аккаунта', - overviewDescription: 'Check account status, profile sources, and common actions at a glance.', + overviewDescription: 'Быстрый обзор статуса аккаунта, источников профиля и основных действий.', basicsTitle: 'Профиль и аватар', - basicsDescription: 'Keep your public profile details and avatar aligned.', - linkedProfileSources: 'Profile Sources', - linkedProfileSourcesDescription: 'Some profile details may stay synced from third-party sign-in methods.', + basicsDescription: 'Поддерживайте данные профиля и аватар актуальными.', + linkedProfileSources: 'Источники профиля', + linkedProfileSourcesDescription: 'Некоторые данные профиля могут синхронизироваться из сторонних способов входа.', securityTitle: 'Настройки безопасности', - securityDescription: 'Password, two-factor authentication, and alerts live in the right rail.', + securityDescription: 'Пароль, двухфакторная аутентификация и уведомления находятся справа.', administrator: 'Администратор', user: 'Пользователь', username: 'Имя пользователя', @@ -1165,129 +1165,129 @@ export default { enterUsername: 'Введите имя пользователя', editProfile: 'Изменить профиль', updateProfile: 'Обновить профиль', - updating: 'Updating...', + updating: 'Обновление...', updateSuccess: 'Профиль обновлён', updateFailed: 'Не удалось обновить профиль', - usernameRequired: 'Username is required', + usernameRequired: 'Введите имя пользователя', changePassword: 'Сменить пароль', currentPassword: 'Текущий пароль', newPassword: 'Новый пароль', confirmNewPassword: 'Подтвердите новый пароль', - passwordHint: 'Password must be at least 8 characters long', - changingPassword: 'Changing...', + passwordHint: 'Пароль должен быть не короче 8 символов', + changingPassword: 'Изменение...', changePasswordButton: 'Сменить пароль', - passwordsNotMatch: 'New passwords do not match', - passwordTooShort: 'Password must be at least 8 characters long', - passwordChangeSuccess: 'Password changed successfully', - passwordChangeFailed: 'Failed to change password', + passwordsNotMatch: 'Новые пароли не совпадают', + passwordTooShort: 'Пароль должен быть не короче 8 символов', + passwordChangeSuccess: 'Пароль изменён', + passwordChangeFailed: 'Не удалось изменить пароль', // TOTP 2FA totp: { title: 'Двухфакторная аутентификация (2FA)', - description: 'Enhance account security with Google Authenticator or similar apps', + description: 'Повысьте безопасность аккаунта через Google Authenticator или похожие приложения', enabled: 'Включено', - enabledAt: 'Enabled at', - notEnabled: 'Not Enabled', - notEnabledHint: 'Enable two-factor authentication to enhance account security', + enabledAt: 'Включено', + notEnabled: 'Не включено', + notEnabledHint: 'Включите двухфакторную аутентификацию для защиты аккаунта', enable: 'Включить', disable: 'Отключить', - featureDisabled: 'Feature Unavailable', - featureDisabledHint: 'Two-factor authentication has not been enabled by the administrator', - setupTitle: 'Set Up Two-Factor Authentication', - setupStep1: 'Scan the QR code below with your authenticator app', - setupStep2: 'Enter the 6-digit code from your app', - manualEntry: "Can't scan? Enter the key manually:", - enterCode: 'Enter 6-digit code', - verify: 'Verify', - setupFailed: 'Failed to get setup information', - verifyFailed: 'Invalid code, please try again', - enableSuccess: 'Two-factor authentication enabled', - disableTitle: 'Disable Two-Factor Authentication', - disableWarning: 'After disabling, you will no longer need a verification code to log in. This may reduce your account security.', - enterPassword: 'Enter your current password to confirm', - confirmDisable: 'Confirm Disable', - disableSuccess: 'Two-factor authentication disabled', - disableFailed: 'Failed to disable, please check your password', - loginTitle: 'Two-Factor Authentication', - loginHint: 'Enter the 6-digit code from your authenticator app', - loginFailed: 'Verification failed, please try again', + featureDisabled: 'Функция недоступна', + featureDisabledHint: 'Двухфакторная аутентификация не включена администратором', + setupTitle: 'Настроить двухфакторную аутентификацию', + setupStep1: 'Отсканируйте QR-код приложением аутентификатора', + setupStep2: 'Введите 6-значный код из приложения', + manualEntry: "Не получается сканировать? Введите ключ вручную:", + enterCode: 'Введите 6-значный код', + verify: 'Проверить', + setupFailed: 'Не удалось получить данные настройки', + verifyFailed: 'Неверный код, попробуйте снова', + enableSuccess: 'Двухфакторная аутентификация включена', + disableTitle: 'Отключить двухфакторную аутентификацию', + disableWarning: 'После отключения для входа больше не потребуется код. Это может снизить безопасность аккаунта.', + enterPassword: 'Введите текущий пароль для подтверждения', + confirmDisable: 'Подтвердить отключение', + disableSuccess: 'Двухфакторная аутентификация отключена', + disableFailed: 'Не удалось отключить, проверьте пароль', + loginTitle: 'Двухфакторная аутентификация', + loginHint: 'Введите 6-значный код из приложения аутентификатора', + loginFailed: 'Проверка не удалась, попробуйте снова', // New translations for email verification - verifyEmailFirst: 'Please verify your email first', - verifyPasswordFirst: 'Please verify your identity first', - emailCode: 'Email Verification Code', - enterEmailCode: 'Enter 6-digit code', - sendCode: 'Send Code', - codeSent: 'Verification code sent to your email', - sendCodeFailed: 'Failed to send verification code' + verifyEmailFirst: 'Сначала подтвердите email', + verifyPasswordFirst: 'Сначала подтвердите личность', + emailCode: 'Код подтверждения email', + enterEmailCode: 'Введите 6-значный код', + sendCode: 'Отправить код', + codeSent: 'Код подтверждения отправлен на email', + sendCodeFailed: 'Не удалось отправить код подтверждения' }, balanceNotify: { - title: 'Balance Low Notification', - description: 'Send email alert when account balance falls below threshold', - enabled: 'Enable Balance Low Notification', - threshold: 'Custom Threshold', - thresholdHint: 'Leave empty to use system default', + title: 'Уведомление о низком балансе', + description: 'Отправлять email, когда баланс опускается ниже порога', + enabled: 'Включить уведомление о низком балансе', + threshold: 'Свой порог', + thresholdHint: 'Оставьте пустым, чтобы использовать системное значение', thresholdPlaceholder: 'Введите сумму', - systemDefault: 'System Default', - extraEmails: 'Notification Emails', - extraEmailsHint: 'You must add and verify an email address to receive low balance alerts', - primaryEmail: 'Primary', - noExtraEmails: 'No extra notification emails', - enterEmail: 'Enter email address', - addEmail: 'Add Email', - emailPlaceholder: 'Enter email address', - sendCode: 'Send Code', - resend: 'Resend', - codeSent: 'Verification code sent', - codeSentTo: 'Code sent to {email}', - enterCode: 'Enter verification code', - codePlaceholder: '6-digit code', - verify: 'Verify', - emailAdded: 'Email added', - emailRemoved: 'Email removed', - verifySuccess: 'Email added successfully', - removeEmail: 'Remove', - removeSuccess: 'Email removed', - emailDuplicate: 'This email already exists', - maxEmailsReached: 'Maximum number of notification emails reached', - unverified: 'Unverified', - verified: 'Verified', + systemDefault: 'Системное значение', + extraEmails: 'Email для уведомлений', + extraEmailsHint: 'Добавьте и подтвердите email, чтобы получать уведомления о низком балансе', + primaryEmail: 'Основной', + noExtraEmails: 'Дополнительных email нет', + enterEmail: 'Введите email', + addEmail: 'Добавить email', + emailPlaceholder: 'Введите email', + sendCode: 'Отправить код', + resend: 'Отправить повторно', + codeSent: 'Код подтверждения отправлен', + codeSentTo: 'Код отправлен на {email}', + enterCode: 'Введите код подтверждения', + codePlaceholder: '6-значный код', + verify: 'Проверить', + emailAdded: 'Email добавлен', + emailRemoved: 'Email удалён', + verifySuccess: 'Email успешно добавлен', + removeEmail: 'Удалить', + removeSuccess: 'Email удалён', + emailDuplicate: 'Этот email уже добавлен', + maxEmailsReached: 'Достигнут максимум email для уведомлений', + unverified: 'Не подтверждён', + verified: 'Подтверждён', }, avatar: { - title: 'Profile Avatar', - description: 'Upload an avatar image. Static uploads are compressed to 20KB before saving.', - uploadAction: 'Upload image', - uploadHint: 'Static uploads are compressed to 20KB when possible. GIF uploads must already be within 20KB.', - uploadRequired: 'Upload an avatar image first', - saveSuccess: 'Avatar updated', - deleteSuccess: 'Avatar removed', - invalidType: 'Please choose an image file', - gifTooLarge: 'GIF avatars must already be 20KB or smaller', - compressTooLarge: 'Unable to compress this image below 20KB. Try a smaller image.', - compressFailed: 'Failed to compress the selected image.', - readFailed: 'Failed to read the selected image.', - emptyDeleteHint: 'Avatar is already empty', + title: 'Аватар профиля', + description: 'Загрузите аватар. Статичные изображения сжимаются до 20 КБ перед сохранением.', + uploadAction: 'Загрузить изображение', + uploadHint: 'Статичные изображения по возможности сжимаются до 20 КБ. GIF должен быть не больше 20 КБ.', + uploadRequired: 'Сначала загрузите изображение', + saveSuccess: 'Аватар обновлён', + deleteSuccess: 'Аватар удалён', + invalidType: 'Выберите файл изображения', + gifTooLarge: 'GIF-аватар должен быть не больше 20 КБ', + compressTooLarge: 'Не удалось сжать изображение до 20 КБ. Попробуйте меньший файл.', + compressFailed: 'Не удалось сжать выбранное изображение.', + readFailed: 'Не удалось прочитать выбранное изображение.', + emptyDeleteHint: 'Аватар уже пустой', }, authBindings: { - title: 'Connected Sign-In Methods', - description: 'View current bindings and connect another provider to this account.', - bindAction: 'Bind {providerName}', - bindSuccess: 'Account linked successfully', - emailPlaceholder: 'Enter email address', - codePlaceholder: 'Enter verification code', - passwordPlaceholder: 'Set a login password', - replaceEmailPasswordPlaceholder: 'Enter current password', + title: 'Подключённые способы входа', + description: 'Просматривайте текущие привязки и подключайте других провайдеров.', + bindAction: 'Привязать {providerName}', + bindSuccess: 'Аккаунт привязан', + emailPlaceholder: 'Введите email', + codePlaceholder: 'Введите код подтверждения', + passwordPlaceholder: 'Задайте пароль для входа', + replaceEmailPasswordPlaceholder: 'Введите текущий пароль', sendCodeAction: 'Отправить код', - manageEmailAction: 'Manage email', - hideEmailFormAction: 'Hide email form', - confirmEmailBindAction: 'Bind email', - confirmEmailReplaceAction: 'Replace primary email', - codeSentTo: 'Code sent to {email}', - replaceSuccess: 'Primary email updated', - unbindAction: 'Unbind', - unbindSuccess: '{providerName} unbound', - boundCount: '{count} linked records', + manageEmailAction: 'Управление email', + hideEmailFormAction: 'Скрыть форму email', + confirmEmailBindAction: 'Привязать email', + confirmEmailReplaceAction: 'Заменить основной email', + codeSentTo: 'Код отправлен на {email}', + replaceSuccess: 'Основной email обновлён', + unbindAction: 'Отвязать', + unbindSuccess: '{providerName} отвязан', + boundCount: 'Привязок: {count}', status: { - bound: 'Bound', - notBound: 'Not bound', + bound: 'Привязан', + notBound: 'Не привязан', }, providers: { email: 'Email', @@ -1297,13 +1297,13 @@ export default { wechat: 'WeChat', }, notes: { - emailManagedFromProfile: 'Primary email is managed in the profile form', - canUnbind: 'You can unbind this sign-in method', - bindAnotherBeforeUnbind: 'Bind another sign-in method before unbinding', + emailManagedFromProfile: 'Основной email управляется в форме профиля', + canUnbind: 'Этот способ входа можно отвязать', + bindAnotherBeforeUnbind: 'Перед отвязкой привяжите другой способ входа', }, source: { - avatar: 'Avatar is currently synced from {providerName}', - username: 'Nickname is currently synced from {providerName}', + avatar: 'Аватар синхронизируется из {providerName}', + username: 'Имя синхронизируется из {providerName}', }, } }, @@ -1380,8 +1380,8 @@ export default { newUsersToday: 'New Users Today', todayTokens: 'Токенов сегодня', totalTokens: 'Всего токенов', - cacheToday: 'Cache (Today)', - performance: 'Performance', + cacheToday: 'Кэш сегодня', + performance: 'Производительность', avgResponse: 'Средний ответ', active: 'active', ok: 'ok', @@ -1393,18 +1393,18 @@ export default { day: 'День', hour: 'Час', modelDistribution: 'Распределение моделей', - groupDistribution: 'Group Usage Distribution', + groupDistribution: 'Распределение расхода по группам', metricTokens: 'By Tokens', metricActualCost: 'By Actual Cost', - tokenUsageTrend: 'Token Usage Trend', + tokenUsageTrend: 'Динамика расхода токенов', userUsageTrend: 'User Usage Trend (Top 12)', model: 'Модель', group: 'Группа', - noGroup: 'No Group', + noGroup: 'Без группы', requests: 'Запросы', - tokens: 'Tokens', - actual: 'Actual', - standard: 'Standard', + tokens: 'Токены', + actual: 'Факт', + standard: 'Стандарт', accountCost: 'Стоимость', noDataAvailable: 'Нет данных', recentUsage: 'Последний расход', @@ -1413,7 +1413,7 @@ export default { spendingRankingTitle: 'User Spending Ranking', spendingRankingUser: 'Пользователь', spendingRankingRequests: 'Запросы', - spendingRankingTokens: 'Tokens', + spendingRankingTokens: 'Токены', spendingRankingSpend: 'Spend', spendingRankingOther: 'Others', spendingRankingUsage: 'Расход', @@ -1440,7 +1440,7 @@ export default { secretAccessKey: 'Secret Access Key', secretConfigured: 'Already configured, leave empty to keep', forcePathStyle: 'Force Path Style', - testConnection: 'Test Connection', + testConnection: 'Проверить подключение', testSuccess: 'S3 connection test successful', testFailed: 'S3 connection test failed', saved: 'S3 configuration saved' @@ -1598,12 +1598,12 @@ export default { }, postgres: { title: 'PostgreSQL', - host: 'Host', - port: 'Port', + host: 'Хост', + port: 'Порт', user: 'Пользователь', password: 'Пароль', - database: 'Database', - sslMode: 'SSL Mode', + database: 'База данных', + sslMode: 'Режим SSL', containerName: 'Container Name (docker_exec mode)' }, redis: { @@ -1638,7 +1638,7 @@ export default { profile: 'Профиль', active: 'Активен', connection: 'Connection', - database: 'Database', + database: 'База данных', updatedAt: 'Updated At', actions: 'Действия' } @@ -1662,7 +1662,7 @@ export default { empty: 'No backup jobs yet', columns: { jobID: 'Job ID', - type: 'Type', + type: 'Тип', status: 'Статус', triggeredBy: 'Triggered By', pgProfile: 'PostgreSQL Profile', @@ -1749,7 +1749,7 @@ export default { title: 'Affiliate User Overview', affCode: 'Invite Code', rebateRate: 'Rebate Rate', - invitedCount: 'Invited Users', + invitedCount: 'Приглашённые пользователи', rebatedInviteeCount: 'Rebated Invitees', availableQuota: 'Available Quota', historyQuota: 'Historical Rebate' @@ -1786,7 +1786,7 @@ export default { generatePassword: 'Generate random password', copyPassword: 'Copy password', creating: 'Creating...', - updating: 'Updating...', + updating: 'Обновление...', form: { rpmLimit: 'Requests Per Minute (RPM)', rpmLimitPlaceholder: '0 = unlimited', @@ -1811,12 +1811,12 @@ export default { concurrency: 'Параллелизм', status: 'Статус', lastActive: 'Last Active', - lastUsed: 'Last Used', + lastUsed: 'Последнее использование', created: 'Создан', actions: 'Действия' }, today: 'Сегодня', - total: 'Last 30d', + total: 'Последние 30 дн.', sortBy: 'Sort By', sortCurrentPageOnly: 'Sorts current page only', noSubscription: 'No subscription', @@ -1914,9 +1914,9 @@ export default { columnAlwaysVisible: 'This column is always visible', // Per-platform usage breakdown (hover tooltip) platformBreakdown: 'Per-platform breakdown', - platformBreakdownEmpty: 'No platform usage yet', + platformBreakdownEmpty: 'Расхода по платформам пока нет', platformBreakdownHint: 'Hover for per-platform usage', - platformOther: 'Other', + platformOther: 'Другое', balanceHistoryTitle: 'User Recharge & Concurrency History', noBalanceHistory: 'No records found for this user', allTypes: 'All Types', @@ -1953,7 +1953,7 @@ export default { name: 'Display Name', nameHint: 'Name shown in forms', type: 'Attribute Type', - fieldDescription: 'Description', + fieldDescription: 'Описание', fieldDescriptionHint: 'Description text for the attribute', placeholder: 'Placeholder', placeholderHint: 'Placeholder text for input field', @@ -2001,18 +2001,18 @@ export default { title: 'Platform Quotas', subtitle: 'Configure daily / weekly / monthly USD usage limits for each upstream platform for user {email}', columns: { - platform: 'Platform', + platform: 'Платформа', daily: 'Daily (USD)', weekly: 'Weekly (USD)', monthly: 'Monthly (USD, 30-day rolling)', usage: 'Current Usage', }, - placeholder: 'unlimited', + placeholder: 'безлимитно', save: 'Сохранить', saving: 'Сохранение...', cancel: 'Отмена', clearAll: 'Clear All (remove all limits)', - clearAllConfirm: 'Clear daily / weekly / monthly limits for ALL platforms? All platforms will become "unlimited" with no local undo — you must manually re-enter values before saving.', + clearAllConfirm: 'Clear daily / weekly / monthly limits for ALL platforms? All platforms will become "безлимитно" with no local undo — you must manually re-enter values before saving.', reset: { button: 'Reset window', confirm: 'Reset the {window} usage for {platform} for this user? This is effective immediately.', @@ -2048,18 +2048,18 @@ export default { allPlatforms: 'All Platforms', allStatus: 'Все статусы', allGroups: 'Все группы', - exclusive: 'Exclusive', + exclusive: 'Эксклюзивная', nonExclusive: 'Non-Exclusive', - public: 'Public', + public: 'Публичная', columns: { name: 'Имя', - platform: 'Platform', + platform: 'Платформа', rateMultiplier: 'Rate Multiplier', rpmOverride: 'RPM Override', rpmOverrideHint: 'Per-user RPM cap in this group; empty = group default; 0 = unlimited', rateDefault: 'default', rpmDefault: 'default', - type: 'Type', + type: 'Тип', accounts: 'Аккаунты', capacity: 'Capacity', usage: 'Расход', @@ -2081,8 +2081,8 @@ export default { accountsCount: '{count} accounts', form: { name: 'Имя', - description: 'Description', - platform: 'Platform', + description: 'Описание', + platform: 'Платформа', rateMultiplier: 'Rate Multiplier', status: 'Статус', exclusive: 'Exclusive Group', @@ -2105,7 +2105,7 @@ export default { noGroupsYet: 'No groups yet', createFirstGroup: 'Create your first group to organize API keys.', creating: 'Creating...', - updating: 'Updating...', + updating: 'Обновление...', limitDay: 'd', limitWeek: 'w', limitMonth: 'mo', @@ -2162,7 +2162,7 @@ export default { typeNotEditable: 'Billing type cannot be changed after group creation.', standard: 'Standard (Balance)', subscription: 'Subscription (Quota)', - dailyLimit: 'Daily Limit (USD)', + dailyLimit: 'Дневной лимит (USD)', weeklyLimit: 'Weekly Limit (USD)', monthlyLimit: 'Monthly Limit (USD)', defaultValidityDays: 'Default Validity (Days)', @@ -2277,18 +2277,18 @@ export default { availableChannels: { title: 'Доступные каналы', description: 'Aggregated view: each channel with its linked groups and supported models (wildcards expanded)', - searchPlaceholder: 'Search channels or models...', + searchPlaceholder: 'Поиск каналов или моделей...', columns: { - name: 'Channel', + name: 'Канал', status: 'Статус', billingSource: 'Billing Model Source', groups: 'Linked Groups', - supportedModels: 'Supported Models' + supportedModels: 'Поддерживаемые модели' }, empty: 'Нет данных', noGroups: 'No linked groups', noModels: 'No model mapping configured', - noPricing: 'Pricing not configured', + noPricing: 'Цены не настроены', statusActive: 'Активен', statusDisabled: 'Отключено', billingSource: { @@ -2297,19 +2297,19 @@ export default { channel_mapped: 'Channel-mapped model' }, pricing: { - billingMode: 'Billing Mode', - billingModeToken: 'Per Token', - billingModePerRequest: 'Per Request', - billingModeImage: 'Per Image', - inputPrice: 'Input', - outputPrice: 'Output', - cacheWritePrice: 'Cache Write', - cacheReadPrice: 'Cache Read', - imageOutputPrice: 'Image Output', - perRequestPrice: 'Per Request', - intervals: 'Tiered Pricing', - unitPerMillion: '/ 1M tokens', - unitPerRequest: '/ request' + billingMode: 'Режим биллинга', + billingModeToken: 'За токен', + billingModePerRequest: 'За запрос', + billingModeImage: 'За изображение', + inputPrice: 'Вход', + outputPrice: 'Выход', + cacheWritePrice: 'Запись кэша', + cacheReadPrice: 'Чтение кэша', + imageOutputPrice: 'Выход изображения', + perRequestPrice: 'За запрос', + intervals: 'Ступенчатые цены', + unitPerMillion: '/ 1 млн токенов', + unitPerRequest: '/ запрос' } }, @@ -2317,7 +2317,7 @@ export default { channels: { title: 'Channel Management', description: 'Manage channels and custom model pricing', - searchChannels: 'Search channels...', + searchChannels: 'Поиск каналов...', createChannel: 'Create Channel', editChannel: 'Edit Channel', deleteChannel: 'Delete Channel', @@ -2342,7 +2342,7 @@ export default { deleteConfirm: 'Are you sure you want to delete channel "{name}"? This cannot be undone.', columns: { name: 'Имя', - description: 'Description', + description: 'Описание', status: 'Статус', groups: 'Группы', pricing: 'Оплата', @@ -2351,13 +2351,13 @@ export default { }, billingMode: { token: 'Token', - perRequest: 'Per Request', + perRequest: 'За запрос', image: 'Image (Per Request)' }, form: { name: 'Имя', namePlaceholder: 'Enter channel name', - description: 'Description', + description: 'Описание', descriptionPlaceholder: 'Optional description', status: 'Статус', groups: 'Associated Groups', @@ -2367,13 +2367,13 @@ export default { models: 'Модели', modelsPlaceholder: 'Type full model name and press Enter', modelInputHint: 'Press Enter to add, supports paste for batch import.', - billingMode: 'Billing Mode', + billingMode: 'Режим биллинга', defaultPrices: 'Default prices (fallback when no interval matches)', - inputPrice: 'Input', - outputPrice: 'Output', - cacheWritePrice: 'Cache Write', - cacheReadPrice: 'Cache Read', - imageTokenPrice: 'Image Output', + inputPrice: 'Вход', + outputPrice: 'Выход', + cacheWritePrice: 'Запись кэша', + cacheReadPrice: 'Чтение кэша', + imageTokenPrice: 'Выход изображения', imageOutputPrice: 'Image Output Price', pricePlaceholder: 'По умолчанию', intervals: 'Context Intervals (optional)', @@ -2510,7 +2510,7 @@ export default { apiKeyHealthEmptyHint: 'Save keys or test input keys to see availability.', apiKeyStatusOk: 'Доступно', apiKeyStatusError: 'Ошибка', - apiKeyStatusFrozen: 'Frozen', + apiKeyStatusFrozen: 'Заморожено', apiKeyStatusUnknown: 'Untested', apiKeyFailureCount: '{count} failures', apiKeyLatency: '{ms} ms', @@ -2662,7 +2662,7 @@ export default { allEndpoints: 'All Endpoints', }, table: { - time: 'Time', + time: 'Время', group: 'Группа', user: 'Пользователь', apiKey: 'API-ключ', @@ -2692,7 +2692,7 @@ export default { title: 'Монитор каналов', description: 'Monitor channel availability, latency and status', searchPlaceholder: 'Search monitor name...', - allProviders: 'All Providers', + allProviders: 'Все провайдеры', allStatus: 'Все статусы', enabledFilter: 'Включено', onlyEnabled: 'Enabled only', @@ -2713,17 +2713,17 @@ export default { primaryModelRequired: 'Please enter a primary model', columns: { name: 'Имя', - provider: 'Provider', - primaryModel: 'Primary Model', - availability7d: '7d Availability', - latency: 'Latency (ms)', + provider: 'Провайдер', + primaryModel: 'Основная модель', + availability7d: 'Доступность 7 дн.', + latency: 'Задержка (мс)', enabled: 'Включено', actions: 'Действия' }, form: { name: 'Имя', namePlaceholder: 'Enter monitor name', - provider: 'Platform', + provider: 'Платформа', apiMode: 'OpenAI protocol', apiModeChatCompletions: 'OpenAI Compatible', apiModeChatCompletionsHint: 'Use /v1/chat/completions with messages; works for most compatible providers.', @@ -2739,9 +2739,9 @@ export default { selectKeyTitle: 'Select my API Key', selectKeyHint: 'Only your active, non-expired keys are listed.', noActiveKey: 'No active API keys available', - primaryModel: 'Primary Model', + primaryModel: 'Основная модель', primaryModelPlaceholder: 'gpt-4o-mini', - extraModels: 'Extra Models', + extraModels: 'Дополнительные модели', extraModelsPlaceholder: 'Press Enter to add extra model', groupName: 'Group Name', groupNamePlaceholder: 'Optional, used to group rows in user view', @@ -2810,7 +2810,7 @@ export default { form: { name: 'Template name', namePlaceholder: 'e.g. Claude Code mimicry', - description: 'Description', + description: 'Описание', descriptionPlaceholder: 'Optional: what this template is for, capture date, etc.' } } @@ -2826,9 +2826,9 @@ export default { allStatus: 'Все статусы', allGroups: 'Все группы', allPlatforms: 'All Platforms', - daily: 'Daily', - weekly: 'Weekly', - monthly: 'Monthly', + daily: 'Дневная', + weekly: 'Недельная', + monthly: 'Месячная', noLimits: 'No limits configured', unlimited: 'Безлимитно', resetNow: 'Скоро сброс', @@ -2845,13 +2845,13 @@ export default { status: { active: 'Активен', expired: 'Истекла', - revoked: 'Revoked' + revoked: 'Отозвана' }, columns: { user: 'Пользователь', group: 'Группа', usage: 'Расход', - expires: 'Expires', + expires: 'Истекает', status: 'Статус', actions: 'Действия' }, @@ -2866,7 +2866,7 @@ export default { groupHint: 'Only groups with subscription billing type are shown', validityHint: 'Number of days the subscription will be valid', adjustingFor: 'Adjusting subscription for', - currentExpiration: 'Current expiration', + currentExpiration: 'Текущий срок действия', adjustDaysPlaceholder: 'Positive to extend, negative to shorten', adjustHint: 'Enter positive number to extend, negative to shorten (remaining days must be > 0)', assign: 'Assign', @@ -2877,21 +2877,21 @@ export default { resetQuota: 'Reset Quota', resetQuotaTitle: 'Reset Usage Quota', resetQuotaConfirm: "Reset the daily, weekly, and monthly usage quota for '{user}'? Usage will be zeroed and windows restarted from today.", - quotaResetSuccess: 'Quota reset successfully', - failedToResetQuota: 'Failed to reset quota', + quotaResetSuccess: 'Квота сброшена', + failedToResetQuota: 'Не удалось сбросить квоту', noSubscriptionsYet: 'No subscriptions yet', assignFirstSubscription: 'Assign a subscription to get started.', subscriptionAssigned: 'Subscription assigned successfully', subscriptionAdjusted: 'Subscription adjusted successfully', subscriptionRevoked: 'Subscription revoked successfully', - failedToLoad: 'Failed to load subscriptions', + failedToLoad: 'Не удалось загрузить подписки', failedToAssign: 'Failed to assign subscription', failedToAdjust: 'Failed to adjust subscription', failedToRevoke: 'Failed to revoke subscription', adjustWouldExpire: 'Remaining days after adjustment must be greater than 0', adjustOutOfRange: 'Adjustment days must be between -36500 and 36500', pleaseSelectUser: 'Please select a user', - pleaseSelectGroup: 'Please select a group', + pleaseSelectGroup: 'Выберите группу', validityDaysRequired: 'Please enter a valid number of days (at least 1)', revokeConfirm: "Are you sure you want to revoke the subscription for '{user}'? This action cannot be undone.", @@ -3037,7 +3037,7 @@ export default { codeAssist: 'Code Assist', antigravityOauth: 'Antigravity OAuth', antigravityApikey: 'Connect via Base URL + API Key', - upstream: 'Upstream', + upstream: 'Апстрим', upstreamDesc: 'Connect via Base URL + API Key' }, status: { @@ -3064,8 +3064,8 @@ export default { columns: { name: 'Имя', platformType: 'Platform/Type', - platform: 'Platform', - type: 'Type', + platform: 'Платформа', + type: 'Тип', capacity: 'Capacity', notes: 'Notes', priority: 'Priority', @@ -3077,7 +3077,7 @@ export default { groups: 'Группы', usageWindows: 'Usage Windows', proxy: 'Proxy', - lastUsed: 'Last Used', + lastUsed: 'Последнее использование', createdAt: 'Создан', expiresAt: 'Истекает', actions: 'Действия' @@ -3091,7 +3091,7 @@ export default { privacyAntigravityFailed: 'Privacy setting failed', setPrivacy: 'Set Privacy', subscriptionAbnormal: 'Abnormal', - subscriptionExpires: 'Expires', + subscriptionExpires: 'Истекает', // Capacity status tooltips capacity: { windowCost: { @@ -3135,7 +3135,7 @@ export default { keywords: 'Keywords', keywordsPlaceholder: 'e.g. overloaded, too many requests', keywordsHint: 'Separate keywords with commas; any keyword match will trigger.', - description: 'Description', + description: 'Описание', descriptionPlaceholder: 'Optional note for this rule', rulesInvalid: 'Add at least one rule with error code, keywords, and duration.', viewDetails: 'View temp unschedulable details', @@ -3165,7 +3165,7 @@ export default { }, clearRateLimit: 'Clear Rate Limit', resetQuota: 'Reset Quota', - quotaLimit: 'Quota Limit', + quotaLimit: 'Лимит квоты', quotaLimitPlaceholder: '0 means unlimited', quotaLimitHint: 'Set daily/weekly/total spending limits (USD). Anthropic API key accounts can also configure client affinity. Changing limits won\'t reset usage.', quotaLimitToggle: 'Enable Quota Limit', @@ -3201,7 +3201,7 @@ export default { threshold: 'Alert Amount', thresholdPlaceholder: 'Enter percentage', }, - testConnection: 'Test Connection', + testConnection: 'Проверить подключение', reAuthorize: 'Re-Authorize', refreshToken: 'Refresh Token', noAccountsYet: 'No accounts yet', @@ -3234,7 +3234,7 @@ export default { baseUrlPlaceholder: 'https://api.anthropic.com or https://api.openai.com', baseUrlNotice: 'Applies to API Key accounts only; leave empty to keep existing value', submit: 'Update Accounts', - updating: 'Updating...', + updating: 'Обновление...', success: 'Updated {count} account(s)', partialSuccess: 'Partially updated: {success} succeeded, {failed} failed', failed: 'Bulk update failed', @@ -3258,9 +3258,9 @@ export default { failedToRefresh: 'Failed to refresh token', failedToDelete: 'Failed to delete account', failedToClearRateLimit: 'Failed to clear rate limit', - deleteConfirm: "Are you sure you want to delete '{name}'? This action cannot be undone.", + deleteConfirm: "Удалить '{name}'? Это действие нельзя отменить.", // Create/Edit Account Modal - platform: 'Platform', + platform: 'Платформа', accountName: 'Account Name', enterAccountName: 'Enter account name', accountType: 'Account Type', @@ -3463,7 +3463,7 @@ export default { idleTimeoutHint: 'Sessions will be released after idle timeout' }, rpmLimit: { - label: 'RPM Limit', + label: 'Лимит RPM', hint: 'Limit requests per minute to protect upstream accounts', baseRpm: 'Base RPM', baseRpmPlaceholder: '15', @@ -3545,7 +3545,7 @@ export default { allowOveragesTooltip: 'Only use AI Credits after free quota is explicitly exhausted. Ordinary concurrent 429 rate limits will not switch to overages.', creating: 'Creating...', - updating: 'Updating...', + updating: 'Обновление...', accountCreated: 'Account created successfully', accountUpdated: 'Account updated successfully', failedToCreate: 'Failed to create account', @@ -3929,8 +3929,8 @@ export default { sendingImageRequest: 'Sending image generation test request...', response: 'Response:', startTest: 'Start Test', - testing: 'Testing...', - retry: 'Retry', + testing: 'Проверка...', + retry: 'Повторить', copyOutput: 'Copy output', outputCopied: 'Output copied', startingTestForAccount: 'Starting test for account: {name}', @@ -3952,7 +3952,7 @@ export default { stats: { totalCost: '30-Day Total Cost', accumulatedCost: 'Accumulated cost', - standardCost: 'Standard', + standardCost: 'Стандарт', totalRequests: '30-Day Total Requests', totalCalls: 'Total API calls', avgDailyCost: 'Daily Avg Cost', @@ -3962,17 +3962,17 @@ export default { todayOverview: 'Today Overview', cost: 'Стоимость', requests: 'Запросы', - tokens: 'Tokens', + tokens: 'Токены', highestCostDay: 'Highest Cost Day', highestRequestDay: 'Highest Request Day', date: 'Дата', accumulatedTokens: 'Accumulated Tokens', totalTokens: '30-Day Total', dailyAvgTokens: 'Daily Average', - performance: 'Performance', + performance: 'Производительность', avgResponseTime: 'Средний ответ', daysActive: 'Days Active', - recentActivity: 'Recent Activity', + recentActivity: 'Последняя активность', todayRequests: 'Запросов сегодня', todayTokens: 'Токенов сегодня', todayCost: 'Расход сегодня', @@ -3996,7 +3996,7 @@ export default { pro: 'Pro', ultra: 'Ultra', aiPremium: 'AI Premium', - standard: 'Standard', + standard: 'Стандарт', basic: 'Basic', personal: 'Personal', unlimited: 'Безлимитно' @@ -4007,7 +4007,7 @@ export default { forbiddenValidation: 'Verification Required', forbiddenViolation: 'Violation Ban', openVerification: 'Open Verification Link', - copyLink: 'Copy Link', + copyLink: 'Скопировать ссылку', linkCopied: 'Link Copied', needsReauth: 'Re-auth Required', rateLimited: 'Rate Limited', @@ -4109,7 +4109,7 @@ export default { latency: 'Latency', actions: 'Действия' }, - testConnection: 'Test Connection', + testConnection: 'Проверить подключение', qualityCheck: 'Quality Check', batchQualityCheck: 'Batch Quality Check', batchTest: 'Test All Proxies', @@ -4129,12 +4129,12 @@ export default { accountsEmpty: 'No accounts are using this proxy', accountsFailed: 'Failed to load accounts list', accountName: 'Account', - accountPlatform: 'Platform', + accountPlatform: 'Платформа', accountNotes: 'Notes', name: 'Имя', protocol: 'Protocol', - host: 'Host', - port: 'Port', + host: 'Хост', + port: 'Порт', username: 'Username (Optional)', password: 'Password (Optional)', status: 'Статус', @@ -4165,7 +4165,7 @@ export default { failedToImport: 'Failed to batch import', // Other messages creating: 'Creating...', - updating: 'Updating...', + updating: 'Обновление...', proxyCreated: 'Proxy created successfully', proxyUpdated: 'Proxy updated successfully', proxyDeleted: 'Proxy deleted successfully', @@ -4223,8 +4223,8 @@ export default { unused: 'Unused', used: 'Использовано', columns: { - code: 'Code', - type: 'Type', + code: 'Код', + type: 'Тип', value: 'Value', status: 'Статус', usedBy: 'Used By', @@ -4267,7 +4267,7 @@ export default { generating: 'Generating...', generate: 'Generate', copyAll: 'Copy All', - copied: 'Copied!', + copied: 'Скопировано!', download: 'Download', codesExported: 'Codes exported successfully', codeDeleted: 'Redeem code deleted successfully', @@ -4293,7 +4293,7 @@ export default { validityDays: 'Validity Days', codeExpiry: 'Code Expiry', neverExpires: 'Never expires', - expiryPresetDays: '{days} days', + expiryPresetDays: '{days} дн.', customExpiry: 'Свой', customExpiryDays: 'Custom days', expiryDaysRequired: 'Please enter a valid expiry day count', @@ -4370,8 +4370,8 @@ export default { timeNever: 'Никогда', readStatus: 'Read Status', eligible: 'Eligible', - readAt: 'Read at', - unread: 'Unread', + readAt: 'Прочитано', + unread: 'Непрочитано', searchUsers: 'Search users...', failedToLoad: 'Failed to load announcements', failedToCreate: 'Failed to create announcement', @@ -4391,7 +4391,7 @@ export default { searchCodes: 'Search codes...', allStatus: 'Все статусы', columns: { - code: 'Code', + code: 'Код', bonusAmount: 'Bonus Amount', maxUses: 'Max Uses', usedCount: 'Использовано', @@ -4423,7 +4423,7 @@ export default { viewUsages: 'View Usages', noUsages: 'No usage records yet', userPrefix: 'User #{id}', - copied: 'Copied!', + copied: 'Скопировано!', // Messages noCodesYet: 'No promo codes yet', createFirstCode: 'Create your first promo code to offer registration bonuses.', @@ -4465,17 +4465,17 @@ export default { inputTokens: 'Входные токены', outputTokens: 'Выходные токены', cacheCreationTokens: 'Cache Creation Tokens', - cacheCreation5mTokens: 'Cache Write', - cacheCreation1hTokens: 'Cache Write', + cacheCreation5mTokens: 'Запись кэша', + cacheCreation1hTokens: 'Запись кэша', cacheReadTokens: 'Cache Read Tokens', failedToLoad: 'Failed to load usage records', billingType: 'Billing Type', allBillingTypes: 'All Billing Types', billingTypeBalance: 'Баланс', billingTypeSubscription: 'Subscription', - billingMode: 'Billing Mode', + billingMode: 'Режим биллинга', billingModeToken: 'Token', - billingModePerRequest: 'Per Request', + billingModePerRequest: 'За запрос', billingModeImage: 'Image', allBillingModes: 'All Billing Modes', ipAddress: 'IP', @@ -4545,7 +4545,7 @@ export default { ready: 'ready', requestsTotal: 'Requests (total)', slaScope: 'SLA scope:', - tokens: 'Tokens', + tokens: 'Токены', tps: 'TPS:', current: 'current', peak: 'peak', @@ -4575,10 +4575,10 @@ export default { max: 'max:', requests: 'Запросы', requestsTitle: 'Запросы', - upstream: 'Upstream', + upstream: 'Апстрим', client: 'Client', system: 'System', - other: 'Other', + other: 'Другое', errorsSla: 'Errors (SLA scope)', upstreamExcl429529: 'Upstream (excl 429/529)', failedToLoadData: 'Failed to load ops data.', @@ -4704,14 +4704,14 @@ export default { connectionRefused: 'connection refused', rateLimit: 'rate limit' }, - time: 'Time', - type: 'Type', + time: 'Время', + type: 'Тип', context: 'Context', - platform: 'Platform', + platform: 'Платформа', model: 'Модель', group: 'Группа', user: 'Пользователь', - userId: 'User ID', + userId: 'ID пользователя', account: 'Account', accountId: 'Account ID', status: 'Статус', @@ -4724,13 +4724,13 @@ export default { details: 'Details', phase: 'Phase', id: 'ID:', - typeUpstream: 'Upstream', + typeUpstream: 'Апстрим', typeRequest: 'Request', typeAuth: 'Auth', typeRouting: 'Routing', typeInternal: 'Internal', endpoint: 'Endpoint', - requestType: 'Type', + requestType: 'Тип', requestTypeSync: 'Sync', requestTypeStream: 'Stream', requestTypeWs: 'WS' @@ -4743,17 +4743,17 @@ export default { resolved: 'Resolved', viewErrors: 'Errors', viewExcluded: 'Excluded', - statusCodeOther: 'Other', + statusCodeOther: 'Другое', owner: { - provider: 'Provider', + provider: 'Провайдер', client: 'Client', - platform: 'Platform' + platform: 'Платформа' }, phase: { request: 'Request', auth: 'Auth', routing: 'Routing', - upstream: 'Upstream', + upstream: 'Апстрим', network: 'Network', internal: 'Internal' }, @@ -4794,12 +4794,12 @@ export default { }, loading: 'Loading…', requestId: 'Request ID', - time: 'Time', + time: 'Время', phase: 'Phase', status: 'Статус', message: 'Message', basicInfo: 'Basic Info', - platform: 'Platform', + platform: 'Платформа', model: 'Модель', group: 'Группа', user: 'Пользователь', @@ -4807,8 +4807,8 @@ export default { latency: 'Request Duration', businessLimited: 'Business Limited', requestPath: 'Request Path', - inboundEndpoint: 'Inbound Endpoint', - upstreamEndpoint: 'Upstream Endpoint', + inboundEndpoint: 'Входящий endpoint', + upstreamEndpoint: 'Апстрим endpoint', requestedModel: 'Requested Model', upstreamModel: 'Upstream Model', requestType: 'Request Type', @@ -4820,7 +4820,7 @@ export default { timings: 'Timings', auth: 'Auth', routing: 'Routing', - upstream: 'Upstream', + upstream: 'Апстрим', response: 'Response', classification: 'Classification', errorBody: 'Error Body', @@ -4858,11 +4858,11 @@ export default { error: 'ERROR' }, table: { - time: 'Time', + time: 'Время', kind: 'Kind', - platform: 'Platform', + platform: 'Платформа', model: 'Модель', - duration: 'Duration', + duration: 'Длительность', status: 'Статус', requestId: 'Request ID', actions: 'Действия' @@ -4902,13 +4902,13 @@ export default { historyEmpty: 'No history' }, table: { - time: 'Time', + time: 'Время', status: 'Статус', severity: 'Severity', - platform: 'Platform', + platform: 'Платформа', ruleId: 'Rule ID', title: 'Title', - duration: 'Duration', + duration: 'Длительность', metric: 'Metric / Threshold', dimensions: 'Dimensions', email: 'Email Sent', @@ -4985,7 +4985,7 @@ export default { }, form: { name: 'Имя', - description: 'Description', + description: 'Описание', metric: 'Metric', operator: 'Operator', groupId: 'Group (group_id)', @@ -5123,7 +5123,7 @@ export default { alertConfig: 'Alert Configuration', enableAlert: 'Enable Alerts', alertRecipients: 'Alert Recipient Emails', - emailPlaceholder: 'Enter email address', + emailPlaceholder: 'Введите email', recipientsHint: 'If empty, the system will use the first admin email as default recipient', minSeverity: 'Minimum Severity', reportConfig: 'Report Configuration', @@ -5534,7 +5534,7 @@ export default { platformQuotaNotice: 'Monthly quota uses a 30-day rolling window, not a calendar month.', }, platformQuota: { - platform: 'Platform', + platform: 'Платформа', daily: 'Daily (USD)', weekly: 'Weekly (USD)', monthly: 'Monthly (USD, 30d rolling)', @@ -5593,7 +5593,7 @@ export default { hideApiKey: 'Hide', copyApiKey: 'Копировать', copied: 'Скопировано', - quotaLimit: 'Quota Limit', + quotaLimit: 'Лимит квоты', quotaLimitHint: 'Leave empty for unlimited; must be > 0 if set', quotaLimitMustBePositive: 'Quota limit must be greater than 0', subscribedAt: 'Subscribed At', @@ -5603,13 +5603,13 @@ export default { resetUsageConfirm: 'Reset usage counter for this provider?', resetUsageSuccess: 'Usage counter reset', proxy: 'Proxy', - removeProvider: 'Remove', + removeProvider: 'Удалить', noProviders: 'No search providers configured', test: 'Test', testDefaultQuery: 'Major world events this year', testing: 'Searching...', testResultTitle: 'Search Results', - testResultProvider: 'Provider', + testResultProvider: 'Провайдер', testNoResults: 'No results found', }, site: { @@ -5627,7 +5627,7 @@ export default { apiBaseUrl: 'API Base URL', apiBaseUrlPlaceholder: 'https://api.example.com', apiBaseUrlHint: - 'Used for "Use Key" and "Import to CC Switch" features. Leave empty to use current site URL.', + 'Used for "Использовать ключ" and "Import to CC Switch" features. Leave empty to use current site URL.', tablePreferencesTitle: 'Global Table Preferences', tablePreferencesDescription: 'Configure default pagination behavior for shared table components', tableDefaultPageSize: 'Default Rows Per Page', @@ -5645,7 +5645,7 @@ export default { namePlaceholder: 'e.g., OpenAI Compatible', endpointUrl: 'Endpoint URL', endpointUrlPlaceholder: 'https://api2.example.com', - descriptionLabel: 'Description', + descriptionLabel: 'Описание', descriptionPlaceholder: 'e.g., Supports OpenAI format requests', add: 'Add Endpoint', }, @@ -5657,7 +5657,7 @@ export default { docUrlHint: 'Link to your documentation site. Leave empty to hide the documentation link.', siteLogo: 'Site Logo', uploadImage: 'Upload Image', - remove: 'Remove', + remove: 'Удалить', logoHint: 'PNG, JPG, or SVG. Max 300KB. Recommended: 80x80px square image.', logoSizeError: 'Image size exceeds 300KB limit ({size}KB)', logoTypeError: 'Please select an image file', @@ -5667,7 +5667,7 @@ export default { homeContentHint: 'Customize the home page content. Supports Markdown/HTML. If you enter a URL (starting with http:// or https://), it will be used as an iframe src to embed an external page. When set, the default status information will no longer be displayed.', homeContentIframeWarning: '⚠️ iframe mode note: Some websites have X-Frame-Options or CSP security policies that prevent embedding in iframes. If the page appears blank or shows an error, please verify the target website allows embedding, or consider using HTML mode to build your own content.', hideCcsImportButton: 'Hide CCS Import Button', - hideCcsImportButtonHint: 'When enabled, the "Import to CCS" button will be hidden on the API Keys page' + hideCcsImportButtonHint: 'When enabled, the "Импорт в CCS" button will be hidden on the API Keys page' }, purchase: { title: 'Recharge / Subscription Page', @@ -5700,12 +5700,12 @@ export default { iconSvgPlaceholder: '...', iconPreview: 'Icon Preview', uploadSvg: 'Upload SVG', - removeSvg: 'Remove', + removeSvg: 'Удалить', visibility: 'Visible To', visibilityUser: 'Regular Users', visibilityAdmin: 'Administrators', add: 'Add Menu Item', - remove: 'Remove', + remove: 'Удалить', moveUp: 'Move Up', moveDown: 'Move Down', }, @@ -5865,9 +5865,9 @@ export default { enableConflict: '{method} already has an enabled provider instance: {provider}. Disable the existing instance before switching.', }, balanceNotify: { - title: 'Balance Low Notification', + title: 'Уведомление о низком балансе', description: 'Send email notification when user balance falls below threshold', - enabled: 'Enable Balance Low Notification', + enabled: 'Включить уведомление о низком балансе', threshold: 'Default Threshold', thresholdHint: 'Used when user has not set a custom value', thresholdPlaceholder: 'Введите сумму', @@ -5879,10 +5879,10 @@ export default { title: 'Account Quota Notification', description: 'Notify admins when account quota usage reaches alert threshold', enabled: 'Enable Account Quota Notification', - emails: 'Notification Emails', + emails: 'Email для уведомлений', emailsHint: 'Leave empty to disable notifications', - addEmail: 'Add Email', - emailPlaceholder: 'Enter email address', + addEmail: 'Добавить email', + emailPlaceholder: 'Введите email', }, subscriptionExpiryNotify: { title: 'Subscription Expiry Reminder', @@ -5893,8 +5893,8 @@ export default { smtp: { title: 'SMTP Settings', description: 'Configure email sending for verification codes', - testConnection: 'Test Connection', - testing: 'Testing...', + testConnection: 'Проверить подключение', + testing: 'Проверка...', host: 'SMTP Host', hostPlaceholder: 'smtp.gmail.com', port: 'SMTP Port', @@ -5919,7 +5919,7 @@ export default { recipientEmail: 'Recipient Email', recipientEmailPlaceholder: "test{'@'}example.com", sendTestEmail: 'Send Test Email', - sending: 'Sending...', + sending: 'Отправка...', enterRecipientHint: 'Please enter a recipient email address' }, emailTemplates: { @@ -6022,7 +6022,7 @@ export default { profile: 'Профиль', profileId: 'Profile ID', name: 'Имя', - provider: 'Type', + provider: 'Тип', active: 'Активен', endpoint: 'Endpoint', bucket: 'Bucket', @@ -6037,7 +6037,7 @@ export default { actions: 'Действия', rootFolder: 'Root folder', testInTable: 'Test', - testingInTable: 'Testing...', + testingInTable: 'Проверка...', testTimeout: 'Test timed out (15s)' }, enabled: 'Enable Storage', @@ -6054,8 +6054,8 @@ export default { forcePathStyle: 'Force Path Style', defaultQuota: 'Default Storage Quota', defaultQuotaHint: 'Default quota when not specified at user or group level. 0 means unlimited', - testConnection: 'Test Connection', - testing: 'Testing...', + testConnection: 'Проверить подключение', + testing: 'Проверка...', testSuccess: 'Connection test successful', testFailed: 'Connection test failed', saved: 'Storage settings saved successfully', @@ -6165,7 +6165,7 @@ export default { modelWhitelistHint: 'Leave empty to apply to all models. Supports exact match and wildcard prefix (e.g., claude-opus-*)', modelPatternPlaceholder: 'e.g., claude-opus-* or claude-opus-4-6', addModelPattern: 'Add model pattern', - removePattern: 'Remove', + removePattern: 'Удалить', fallbackAction: 'Fallback Action', fallbackActionHint: 'Action for models not matching the whitelist', fallbackErrorMessagePlaceholder: 'Custom error message when non-whitelisted models are blocked', @@ -6297,7 +6297,7 @@ export default { allPlatforms: 'All Platforms', passthrough: 'Passthrough', custom: 'Свой', - code: 'Code', + code: 'Код', body: 'Body', skipMonitoring: 'Skip Monitoring', @@ -6326,7 +6326,7 @@ export default { namePlaceholder: 'e.g., Context Limit Passthrough', priority: 'Priority', priorityHint: 'Lower values have higher priority', - description: 'Description', + description: 'Описание', descriptionPlaceholder: 'Describe the purpose of this rule...', matchConditions: 'Match Conditions', errorCodes: 'Error Codes', @@ -6374,7 +6374,7 @@ export default { columns: { name: 'Имя', - description: 'Description', + description: 'Описание', grease: 'GREASE', alpn: 'ALPN', actions: 'Действия' @@ -6390,7 +6390,7 @@ export default { yamlParseFailed: 'Failed to parse YAML: name field not found', name: 'Profile Name', namePlaceholder: 'e.g. macOS Node.js v24', - description: 'Description', + description: 'Описание', descriptionPlaceholder: 'Optional description for this profile', enableGrease: 'Enable GREASE', enableGreaseHint: 'Insert GREASE values in TLS ClientHello extensions', @@ -6424,9 +6424,9 @@ export default { title: 'Мои подписки', viewDetails: 'Посмотреть детали подписки', activeCount: 'Активных подписок: {count}', - daily: 'Daily', - weekly: 'Weekly', - monthly: 'Monthly', + daily: 'Дневная', + weekly: 'Недельная', + monthly: 'Месячная', daysRemaining: 'Осталось дней: {days}', expired: 'Истекла', expiresToday: 'Истекает сегодня', @@ -6438,48 +6438,48 @@ export default { // Version Badge version: { - currentVersion: 'Current Version', - latestVersion: 'Latest Version', - upToDate: "You're running the latest version.", - updateAvailable: 'A new version is available!', - releaseNotes: 'Release Notes', - noReleaseNotes: 'No release notes', - viewUpdate: 'View Update', - viewRelease: 'View Release', - viewChangelog: 'View Changelog', + currentVersion: 'Текущая версия', + latestVersion: 'Последняя версия', + upToDate: 'У вас установлена последняя версия.', + updateAvailable: 'Доступна новая версия!', + releaseNotes: 'Примечания к релизу', + noReleaseNotes: 'Нет примечаний к релизу', + viewUpdate: 'Посмотреть обновление', + viewRelease: 'Посмотреть релиз', + viewChangelog: 'История изменений', refresh: 'Обновить', - sourceMode: 'Source Build', - sourceModeHint: 'Source build, use git pull to update', - updateNow: 'Update Now', - updating: 'Updating...', - updateComplete: 'Update Complete', - updateFailed: 'Update Failed', - restartRequired: 'Please restart the service to apply the update', - restartNow: 'Restart Now', - restarting: 'Restarting...', - retry: 'Retry' + sourceMode: 'Сборка из исходников', + sourceModeHint: 'Сборка из исходников, обновляйте через git pull', + updateNow: 'Обновить сейчас', + updating: 'Обновление...', + updateComplete: 'Обновление завершено', + updateFailed: 'Обновление не удалось', + restartRequired: 'Перезапустите сервис, чтобы применить обновление', + restartNow: 'Перезапустить сейчас', + restarting: 'Перезапуск...', + retry: 'Повторить' }, // Recharge / Subscription Page purchase: { title: 'Пополнение / подписка', - description: 'Recharge balance or purchase subscription via the embedded page', + description: 'Пополните баланс или купите подписку через встроенную страницу', openInNewTab: 'Открыть в новой вкладке', notEnabledTitle: 'Функция не включена', - notEnabledDesc: 'The administrator has not enabled the recharge/subscription entry. Please contact admin.', - notConfiguredTitle: 'Recharge / Subscription URL not configured', + notEnabledDesc: 'Администратор не включил раздел пополнения/подписки. Обратитесь к администратору.', + notConfiguredTitle: 'URL пополнения / подписки не настроен', notConfiguredDesc: - 'The administrator enabled the entry but has not configured a recharge/subscription URL. Please contact admin.' + 'Раздел включён, но URL пополнения/подписки не настроен. Обратитесь к администратору.' }, // Custom Page (iframe embed) customPage: { - title: 'Custom Page', + title: 'Пользовательская страница', openInNewTab: 'Открыть в новой вкладке', notFoundTitle: 'Страница не найдена', - notFoundDesc: 'This custom page does not exist or has been removed.', - notConfiguredTitle: 'Page URL not configured', - notConfiguredDesc: 'The URL for this custom page has not been properly configured.', + notFoundDesc: 'Эта пользовательская страница не существует или была удалена.', + notConfiguredTitle: 'URL страницы не настроен', + notConfiguredDesc: 'URL этой пользовательской страницы настроен некорректно.', }, // Announcements Page @@ -6489,21 +6489,21 @@ export default { unreadOnly: 'Только непрочитанные', markRead: 'Отметить прочитанным', markAllRead: 'Отметить все прочитанными', - viewAll: 'View all announcements', - markedAsRead: 'Marked as read', - allMarkedAsRead: 'All announcements marked as read', - newCount: '{count} new announcement | {count} new announcements', - readAt: 'Read at', - read: 'Read', - unread: 'Unread', - startsAt: 'Starts at', - endsAt: 'Ends at', + viewAll: 'Все объявления', + markedAsRead: 'Отмечено прочитанным', + allMarkedAsRead: 'Все объявления отмечены прочитанными', + newCount: '{count} новое объявление | {count} новых объявлений', + readAt: 'Прочитано', + read: 'Прочитано', + unread: 'Непрочитано', + startsAt: 'Начинается', + endsAt: 'Заканчивается', empty: 'Нет объявлений', emptyUnread: 'Нет непрочитанных объявлений', - total: 'announcements', - emptyDescription: 'There are no system announcements at this time', - readStatus: 'You have read this announcement', - markReadHint: 'Click "Отметить прочитанным" to mark this announcement' + total: 'объявлений', + emptyDescription: 'Сейчас системных объявлений нет', + readStatus: 'Вы прочитали это объявление', + markReadHint: 'Нажмите "Отметить прочитанным", чтобы отметить объявление' }, // User Subscriptions Page @@ -6512,40 +6512,40 @@ export default { description: 'Просмотр ваших подписок и расхода', noActiveSubscriptions: 'Нет активных подписок', noActiveSubscriptionsDesc: - "You don't have any active subscriptions. Contact administrator to get one.", - failedToLoad: 'Failed to load subscriptions', + "У вас нет активных подписок. Обратитесь к администратору.", + failedToLoad: 'Не удалось загрузить подписки', status: { active: 'Активен', expired: 'Истекла', - revoked: 'Revoked' + revoked: 'Отозвана' }, usage: 'Расход', - expires: 'Expires', + expires: 'Истекает', noExpiration: 'Без срока', unlimited: 'Безлимитно', unlimitedDesc: 'У этой подписки нет лимитов расхода', - daily: 'Daily', - weekly: 'Weekly', - monthly: 'Monthly', + daily: 'Дневная', + weekly: 'Недельная', + monthly: 'Месячная', daysRemaining: 'Осталось дней: {days}', expiresOn: 'Истекает {date}', resetIn: 'Сброс через {time}', - quotaEndsIn: 'Quota ends in {time}', - windowNotActive: 'Awaiting first use', + quotaEndsIn: 'Квота закончится через {time}', + windowNotActive: 'Ожидает первого использования', usageOf: '{used} из {limit}' }, // Onboarding Tour onboarding: { - restartTour: 'Restart Onboarding Tour', - dontShowAgain: "Don't show again", - dontShowAgainTitle: 'Permanently close onboarding guide', + restartTour: 'Перезапустить обучение', + dontShowAgain: 'Больше не показывать', + dontShowAgainTitle: 'Закрыть обучение навсегда', confirmDontShow: "Are you sure you don't want to see the onboarding guide again?\n\nYou can restart it anytime from the user menu in the top right corner.", confirmExit: 'Are you sure you want to exit the onboarding guide? You can restart it anytime from the top right menu.', - interactiveHint: 'Press Enter or Click to continue', + interactiveHint: 'Нажмите Enter или кликните, чтобы продолжить', navigation: { - flipPage: 'Flip Page', - exit: 'Exit' + flipPage: 'Перелистнуть', + exit: 'Выйти' }, // Admin tour steps admin: { @@ -6699,39 +6699,39 @@ export default { wxpay: 'WeChat Pay', stripe: 'Stripe', airwallex: 'Airwallex', - card: 'Card', - link: 'Link', + card: 'Карта', + link: 'Ссылка', alipay_direct: 'Alipay (Direct)', wxpay_direct: 'WeChat Pay (Direct)', }, status: { pending: 'Ожидает', paid: 'Оплачено', - recharging: 'Recharging', + recharging: 'Пополнение', completed: 'Завершено', expired: 'Истекла', cancelled: 'Отменено', failed: 'Ошибка', - refund_requested: 'Refund Requested', - refunding: 'Refunding', - refunded: 'Refunded', - partially_refunded: 'Partially Refunded', - refund_failed: 'Refund Failed', + refund_requested: 'Запрошен возврат', + refunding: 'Возврат', + refunded: 'Возвращено', + partially_refunded: 'Частично возвращено', + refund_failed: 'Возврат не удался', }, qr: { scanToPay: 'Сканируйте для оплаты', - scanAlipay: 'Alipay QR Payment', - scanWxpay: 'WeChat QR Payment', - scanAlipayHint: 'Open Alipay on your phone and scan the QR code to pay', - scanWxpayHint: 'Open WeChat on your phone and scan the QR code to pay', - payInNewWindow: 'Complete Payment in New Window', - payInNewWindowHint: 'The payment page has opened in a new window. Please complete the payment there and return to this page.', - openPayWindow: 'Reopen Payment Page', - expiresIn: 'Expires in', + scanAlipay: 'Оплата Alipay по QR', + scanWxpay: 'Оплата WeChat по QR', + scanAlipayHint: 'Откройте Alipay на телефоне и отсканируйте QR-код', + scanWxpayHint: 'Откройте WeChat на телефоне и отсканируйте QR-код', + payInNewWindow: 'Завершить оплату в новом окне', + payInNewWindowHint: 'Страница оплаты открыта в новом окне. Завершите оплату там и вернитесь сюда.', + openPayWindow: 'Открыть страницу оплаты снова', + expiresIn: 'Истекает через', expired: 'Заказ истёк', - expiredDesc: 'This order has expired. Please create a new one.', + expiredDesc: 'Этот заказ истёк. Создайте новый.', cancelled: 'Заказ отменён', - cancelledDesc: 'You have cancelled this payment.', + cancelledDesc: 'Вы отменили этот платёж.', waitingPayment: 'Ожидание оплаты...', cancelOrder: 'Отменить заказ', }, @@ -6742,50 +6742,50 @@ export default { orderNo: 'Номер заказа', amount: 'Сумма', payAmount: 'Оплачено', - creditedAmount: 'Credited Amount', + creditedAmount: 'Зачислено', fee: 'Комиссия', - baseAmount: 'Base Amount', - includedInPayAmount: 'included in paid amount', + baseAmount: 'Базовая сумма', + includedInPayAmount: 'включено в сумму оплаты', status: 'Статус', paymentMethod: 'Способ оплаты', createdAt: 'Создан', cancel: 'Отменить заказ', - userId: 'User ID', - orderType: 'Order Type', + userId: 'ID пользователя', + orderType: 'Тип заказа', actions: 'Действия', - requestRefund: 'Request Refund', + requestRefund: 'Запросить возврат', }, result: { success: 'Оплата успешна', subscriptionSuccess: 'Подписка оформлена', processing: 'Платёж обрабатывается', - processingHint: 'Payment confirmation is still pending. This page will refresh automatically.', + processingHint: 'Подтверждение платежа ещё ожидается. Страница обновится автоматически.', failed: 'Оплата не удалась', backToRecharge: 'Назад к пополнению', viewOrders: 'Посмотреть заказы', }, currentBalance: 'Текущий баланс', - groupFallback: 'Group #{id}', + groupFallback: 'Группа #{id}', rechargeAccount: 'Пополнить аккаунт', activeSubscription: 'Активная подписка', - noActiveSubscription: 'No active subscription', + noActiveSubscription: 'Нет активной подписки', tabTopUp: 'Пополнить', tabSubscribe: 'Подписаться', noPlans: 'Нет доступных планов подписки', - notAvailable: 'Top-up is currently unavailable', + notAvailable: 'Пополнение сейчас недоступно', confirmSubscription: 'Подтвердить подписку', - confirmCancel: 'Are you sure you want to cancel this order?', - amountTooLow: 'Minimum amount is {min}', - amountTooHigh: 'Maximum amount is {max}', - amountNoMethod: 'No payment method available for this amount', - rechargeRatePreview: 'Current rate: 1 CNY = {usd} USD', - refundReason: 'Refund Reason', - refundReasonPlaceholder: 'Please describe your refund reason', - stripeLoadFailed: 'Failed to load payment component. Please refresh and try again.', - stripeMissingParams: 'Missing order ID or client secret', - stripeNotConfigured: 'Stripe is not configured', - airwallexLoadFailed: 'Failed to load Airwallex payment component. Please refresh and try again.', - airwallexMissingParams: 'Missing Airwallex payment parameters', + confirmCancel: 'Отменить этот заказ?', + amountTooLow: 'Минимальная сумма: {min}', + amountTooHigh: 'Максимальная сумма: {max}', + amountNoMethod: 'Для этой суммы нет доступного способа оплаты', + rechargeRatePreview: 'Текущий курс: 1 CNY = {usd} USD', + refundReason: 'Причина возврата', + refundReasonPlaceholder: 'Опишите причину возврата', + stripeLoadFailed: 'Не удалось загрузить платёжный компонент. Обновите страницу и попробуйте снова.', + stripeMissingParams: 'Отсутствует ID заказа или client secret', + stripeNotConfigured: 'Stripe не настроен', + airwallexLoadFailed: 'Не удалось загрузить компонент Airwallex. Обновите страницу и попробуйте снова.', + airwallexMissingParams: 'Отсутствуют параметры платежа Airwallex', errors: { tooManyPending: 'Too many pending orders (max {max}). Please complete or cancel existing orders first.', cancelRateLimited: 'Too many cancellations. Please try again later.', @@ -6831,14 +6831,14 @@ export default { REFUND_AMOUNT_EXCEEDED: 'Refund amount exceeds the recharge amount.', REFUND_FAILED: 'Refund failed.', }, - airwallexPay: 'Airwallex Payment', - stripePay: 'Pay Now', - stripeSuccessProcessing: 'Payment successful, processing your order...', + airwallexPay: 'Оплата Airwallex', + stripePay: 'Оплатить', + stripeSuccessProcessing: 'Платёж успешен, обрабатываем заказ...', stripePopup: { - redirecting: 'Redirecting to payment page...', - loadingQr: 'Loading WeChat Pay QR code...', - timeout: 'Timed out waiting for payment credentials, please retry', - qrFailed: 'Failed to get WeChat Pay QR code', + redirecting: 'Переход на страницу оплаты...', + loadingQr: 'Загрузка QR-кода WeChat Pay...', + timeout: 'Истекло время ожидания платёжных данных, повторите попытку', + qrFailed: 'Не удалось получить QR-код WeChat Pay', }, subscribeNow: 'Подписаться', renewNow: 'Продлить', @@ -6846,10 +6846,10 @@ export default { planFeatures: 'Возможности', planCard: { rate: 'Тариф', - dailyLimit: 'Daily', - weeklyLimit: 'Weekly', - monthlyLimit: 'Monthly', - quota: 'Quota', + dailyLimit: 'Дневной', + weeklyLimit: 'Недельный', + monthlyLimit: 'Месячный', + quota: 'Квота', unlimited: 'Безлимитно', models: 'Модели', }, @@ -6886,7 +6886,7 @@ export default { allPaymentTypes: 'All Payment Types', allOrderTypes: 'All Order Types', orderDetail: 'Order Detail', - orderType: 'Order Type', + orderType: 'Тип заказа', orders: 'Заказы', balanceOrder: 'Balance Top-Up', subscriptionOrder: 'Subscription', @@ -6898,7 +6898,7 @@ export default { refundOrder: 'Refund Order', refundAmount: 'Refund Amount', maxRefundable: 'Max Refundable', - refundReason: 'Refund Reason', + refundReason: 'Причина возврата', refundReasonPlaceholder: 'Please enter refund reason', confirmRefund: 'Confirm Refund', refundSuccess: 'Refund successful', @@ -6914,7 +6914,7 @@ export default { noDeduction: 'Will NOT deduct user balance', forceRefund: 'Force refund (ignore balance check)', orderCancelled: 'Заказ отменён', - retry: 'Retry', + retry: 'Повторить', retrySuccess: 'Retry successful', approveRefund: 'Approve Refund', retryRefund: 'Retry Refund', @@ -6979,7 +6979,7 @@ export default { validityDaysRequired: 'Validity days must be greater than 0', groupMissing: 'Missing', groupInfo: 'Group Info', - platform: 'Platform', + platform: 'Платформа', rateMultiplierLabel: 'Тариф', dailyLimit: 'Дневной лимит', weeklyLimit: 'Недельный лимит', @@ -6992,7 +6992,7 @@ export default { subsStatus: { active: 'Активен', expired: 'Истекла', - revoked: 'Revoked', + revoked: 'Отозвана', }, }, }, From 01e4f45be08859a4fa3722bba7160aa849c936aa Mon Sep 17 00:00:00 2001 From: Slavx Date: Thu, 28 May 2026 21:46:38 +0300 Subject: [PATCH 3/4] fix: complete Russian locale refinements Co-authored-by: Qwen-Coder --- frontend/src/i18n/locales/ru.ts | 7028 +++++++++++++++---------------- 1 file changed, 3514 insertions(+), 3514 deletions(-) diff --git a/frontend/src/i18n/locales/ru.ts b/frontend/src/i18n/locales/ru.ts index 6c64276fc8f..6cec530b071 100644 --- a/frontend/src/i18n/locales/ru.ts +++ b/frontend/src/i18n/locales/ru.ts @@ -738,14 +738,14 @@ export default { 'Добавьте эти переменные окружения в профиль терминала или выполните в терминале для настройки API-доступа.', copy: 'Копировать', copied: 'Скопировано', - note: 'These environment variables will be active in the current terminal session. For permanent configuration, add them to ~/.bashrc, ~/.zshrc, or the appropriate configuration file.', + note: 'Эти переменные окружения будут активны в текущей сессии терминала. Для постоянной настройки добавьте их в ~/.bashrc, ~/.zshrc или соответствующий файл конфигурации.', noGroupTitle: 'Сначала назначьте группу', noGroupDescription: 'Этот API-ключ не назначен группе. Сначала выберите группу в списке ключей.', openai: { - description: 'Add the following configuration files to your Codex CLI config directory.', - configTomlHint: 'Make sure the following content is at the beginning of the config.toml file', - note: 'Make sure the config directory exists. macOS/Linux users can run mkdir -p ~/.codex to create it.', - noteWindows: 'Press Win+R and enter %userprofile%\\.codex to open the config directory. Create it manually if it does not exist.', + description: 'Добавьте следующие файлы конфигурации в каталог настроек Codex CLI.', + configTomlHint: 'Убедитесь, что следующий контент находится в начале файла config.toml', + note: 'Убедитесь, что каталог конфигурации существует. Пользователи macOS/Linux могут выполнить mkdir -p ~/.codex, чтобы создать его.', + noteWindows: 'Нажмите Win+R и введите %userprofile%\\.codex, чтобы открыть каталог конфигурации. Создайте его вручную, если он не существует.', }, cliTabs: { claudeCode: 'Claude Code', @@ -755,21 +755,21 @@ export default { opencode: 'OpenCode', }, antigravity: { - description: 'Configure API access for Antigravity group. Select the configuration method based on your client.', + description: 'Настройте API-доступ для группы Antigravity. Выберите способ настройки под ваш клиент.', claudeCode: 'Claude Code', geminiCli: 'Gemini CLI', - claudeNote: 'These environment variables will be active in the current terminal session. For permanent configuration, add them to ~/.bashrc, ~/.zshrc, or the appropriate configuration file.', - geminiNote: 'These environment variables will be active in the current terminal session. For permanent configuration, add them to ~/.bashrc, ~/.zshrc, or the appropriate configuration file.', + claudeNote: 'Эти переменные окружения будут активны в текущей сессии терминала. Для постоянной настройки добавьте их в ~/.bashrc, ~/.zshrc или соответствующий файл конфигурации.', + geminiNote: 'Эти переменные окружения будут активны в текущей сессии терминала. Для постоянной настройки добавьте их в ~/.bashrc, ~/.zshrc или соответствующий файл конфигурации.', }, gemini: { - description: 'Add the following environment variables to your terminal profile or run directly in terminal to configure Gemini CLI access.', - modelComment: 'If you have Gemini 3 access, you can use: gemini-3-pro-preview', - note: 'These environment variables will be active in the current terminal session. For permanent configuration, add them to ~/.bashrc, ~/.zshrc, or the appropriate configuration file.', + description: 'Добавьте следующие переменные окружения в профиль терминала или выполните их в терминале для настройки доступа Gemini CLI.', + modelComment: 'Если у вас есть доступ к Gemini 3, можно использовать: gemini-3-pro-preview', + note: 'Эти переменные окружения будут активны в текущей сессии терминала. Для постоянной настройки добавьте их в ~/.bashrc, ~/.zshrc или соответствующий файл конфигурации.', }, opencode: { - title: 'OpenCode Example', + title: 'Пример OpenCode', subtitle: 'opencode.json', - hint: 'Config path: ~/.config/opencode/opencode.json (or opencode.jsonc), create if not exists. Use default providers (openai/anthropic/google) or custom provider_id. API Key can be configured directly or via /connect command. This is an example, adjust models and options as needed.', + hint: 'Путь конфигурации: ~/.config/opencode/opencode.json (или opencode.jsonc), создайте файл при отсутствии. Используйте провайдеры по умолчанию (openai/anthropic/google) или свой provider_id. API-ключ можно настроить напрямую или через команду /connect. Это пример — при необходимости измените модели и параметры.', }, }, customKeyLabel: 'Свой ключ', @@ -786,7 +786,7 @@ export default { ipBlacklistPlaceholder: '1.2.3.4\n5.6.0.0/16', ipBlacklistHint: 'Один IP или CIDR на строку. Эти IP будут заблокированы для ключа.', ipRestrictionEnabled: 'Ограничение по IP включено', - ccSwitchNotInstalled: 'CC-Switch is not installed or the protocol handler is not registered. Please install CC-Switch first or manually copy the API key.', + ccSwitchNotInstalled: 'CC-Switch не установлен или обработчик протокола не зарегистрирован. Сначала установите CC-Switch или скопируйте API-ключ вручную.', ccsClientSelect: { title: 'Выберите клиент', description: 'Выберите тип клиента для импорта в CC-Switch:', @@ -817,7 +817,7 @@ export default { rateLimitUsage: 'Использование лимита частоты', resetRateLimitUsage: 'Сбросить расход лимита частоты', resetRateLimitTitle: 'Подтвердите сброс лимита частоты', - resetRateLimitConfirmMessage: 'Are you sure you want to reset the rate limit usage for key "{name}"? All time window usage will be reset to zero. This action cannot be undone.', + resetRateLimitConfirmMessage: 'Сбросить расход лимита частоты для ключа "{name}"? Расход во всех окнах будет обнулён. Это действие нельзя отменить.', rateLimitResetSuccess: 'Расход лимита частоты сброшен', failedToResetRateLimit: 'Не удалось сбросить расход лимита частоты', resetNow: 'Скоро сброс', @@ -1371,22 +1371,22 @@ export default { admin: { // Dashboard dashboard: { - title: 'Admin Dashboard', - description: 'System overview and real-time statistics', + title: 'Панель администратора', + description: 'Обзор системы и статистика в реальном времени', apiKeys: 'API-ключи', accounts: 'Аккаунты', users: 'Пользователи', todayRequests: 'Запросов сегодня', - newUsersToday: 'New Users Today', + newUsersToday: 'Новых пользователей сегодня', todayTokens: 'Токенов сегодня', totalTokens: 'Всего токенов', cacheToday: 'Кэш сегодня', performance: 'Производительность', avgResponse: 'Средний ответ', - active: 'active', + active: 'активно', ok: 'ok', err: 'err', - activeUsers: 'active users', + activeUsers: 'активных пользователей', create: 'Создать', timeRange: 'Период', granularity: 'Детализация', @@ -1394,10 +1394,10 @@ export default { hour: 'Час', modelDistribution: 'Распределение моделей', groupDistribution: 'Распределение расхода по группам', - metricTokens: 'By Tokens', - metricActualCost: 'By Actual Cost', + metricTokens: 'По токенам', + metricActualCost: 'По фактической стоимости', tokenUsageTrend: 'Динамика расхода токенов', - userUsageTrend: 'User Usage Trend (Top 12)', + userUsageTrend: 'Динамика расхода пользователей (топ-12)', model: 'Модель', group: 'Группа', noGroup: 'Без группы', @@ -1409,192 +1409,192 @@ export default { noDataAvailable: 'Нет данных', recentUsage: 'Последний расход', viewModelDistribution: 'Распределение моделей', - viewSpendingRanking: 'User Spending Ranking', - spendingRankingTitle: 'User Spending Ranking', + viewSpendingRanking: 'Рейтинг расходов пользователей', + spendingRankingTitle: 'Рейтинг расходов пользователей', spendingRankingUser: 'Пользователь', spendingRankingRequests: 'Запросы', spendingRankingTokens: 'Токены', - spendingRankingSpend: 'Spend', - spendingRankingOther: 'Others', + spendingRankingSpend: 'Расход', + spendingRankingOther: 'Остальные', spendingRankingUsage: 'Расход', - spendShort: 'Spend', - requestsShort: 'Req', - tokensShort: 'Tok', - failedToLoad: 'Failed to load dashboard statistics' + spendShort: 'Расход', + requestsShort: 'Запр.', + tokensShort: 'Ток.', + failedToLoad: 'Не удалось загрузить статистику панели' }, backup: { - title: 'Database Backup', - description: 'Full database backup to S3-compatible storage with scheduled backup and restore', + title: 'Резервная копия базы данных', + description: 'Полная резервная копия базы в S3-совместимое хранилище с расписанием и восстановлением', s3: { - title: 'S3 Storage Configuration', - description: 'Configure S3-compatible storage (supports Cloudflare R2)', - descriptionPrefix: 'Configure S3-compatible storage (supports', + title: 'Настройка S3-хранилища', + description: 'Настройте S3-совместимое хранилище (поддерживается Cloudflare R2)', + descriptionPrefix: 'Настройте S3-совместимое хранилище (поддерживается', descriptionSuffix: ')', - enabled: 'Enable S3 Storage', + enabled: 'Включить S3-хранилище', endpoint: 'Endpoint', - region: 'Region', + region: 'Регион', bucket: 'Bucket', - prefix: 'Key Prefix', + prefix: 'Префикс ключей', accessKeyId: 'Access Key ID', secretAccessKey: 'Secret Access Key', - secretConfigured: 'Already configured, leave empty to keep', - forcePathStyle: 'Force Path Style', + secretConfigured: 'Уже настроено, оставьте пустым, чтобы сохранить', + forcePathStyle: 'Принудительный Path Style', testConnection: 'Проверить подключение', - testSuccess: 'S3 connection test successful', - testFailed: 'S3 connection test failed', - saved: 'S3 configuration saved' + testSuccess: 'Проверка подключения S3 успешна', + testFailed: 'Проверка подключения S3 не удалась', + saved: 'Настройки S3 сохранены' }, schedule: { - title: 'Scheduled Backup', - description: 'Configure automatic scheduled backups', - enabled: 'Enable Scheduled Backup', - cronExpr: 'Cron Expression', - cronHint: 'e.g. "0 2 * * *" means every day at 2:00 AM', - retainDays: 'Backup Expire Days', - retainDaysHint: 'Backup files auto-delete after this many days, 0 = never expire', - retainCount: 'Max Retain Count', - retainCountHint: 'Maximum number of backups to keep, 0 = unlimited', - saved: 'Schedule configuration saved' + title: 'Резервное копирование по расписанию', + description: 'Настройте автоматические резервные копии по расписанию', + enabled: 'Включить резервные копии по расписанию', + cronExpr: 'Cron-выражение', + cronHint: 'например, "0 2 * * *" означает каждый день в 02:00', + retainDays: 'Срок хранения резервных копий', + retainDaysHint: 'Файлы резервных копий удаляются через указанное число дней, 0 = не удалять', + retainCount: 'Максимум копий', + retainCountHint: 'Максимальное число резервных копий, 0 = без лимита', + saved: 'Расписание сохранено' }, operations: { - title: 'Backup Records', - description: 'Create manual backups and manage existing backup records', - createBackup: 'Create Backup', - backing: 'Backing up...', - backupCreated: 'Backup created successfully', - expireDays: 'Expire Days', - alreadyInProgress: 'A backup is already in progress', - backupRunning: 'Backup in progress...', - backupFailed: 'Backup failed', - restoreRunning: 'Restore in progress...', - restoreFailed: 'Restore failed', + title: 'Записи резервных копий', + description: 'Создавайте резервные копии вручную и управляйте существующими записями', + createBackup: 'Создать резервную копию', + backing: 'Создание резервной копии...', + backupCreated: 'Резервная копия создана', + expireDays: 'Срок хранения', + alreadyInProgress: 'Резервное копирование уже выполняется', + backupRunning: 'Выполняется резервное копирование...', + backupFailed: 'Резервное копирование не удалось', + restoreRunning: 'Выполняется восстановление...', + restoreFailed: 'Восстановление не удалось', }, columns: { status: 'Статус', - fileName: 'File Name', - size: 'Size', + fileName: 'Имя файла', + size: 'Размер', expiresAt: 'Истекает', - triggeredBy: 'Triggered By', - startedAt: 'Started At', + triggeredBy: 'Запущено', + startedAt: 'Начато', actions: 'Действия' }, status: { pending: 'Ожидает', - running: 'Running', + running: 'Выполняется', completed: 'Завершено', failed: 'Ошибка' }, progress: { - pending: 'Preparing', - dumping: 'Dumping database', - uploading: 'Uploading', + pending: 'Подготовка', + dumping: 'Дамп базы данных', + uploading: 'Загрузка', }, trigger: { - manual: 'Manual', - scheduled: 'Scheduled' + manual: 'Вручную', + scheduled: 'По расписанию' }, neverExpire: 'Никогда', - empty: 'No backup records', + empty: 'Записей резервных копий нет', actions: { - download: 'Download', - restore: 'Restore', - restoreConfirm: 'Are you sure you want to restore from this backup? This will overwrite the current database!', - restorePasswordPrompt: 'Please enter your admin password to confirm the restore operation', - restoreSuccess: 'Database restored successfully', - deleteConfirm: 'Are you sure you want to delete this backup?', - deleted: 'Backup deleted' + download: 'Скачать', + restore: 'Восстановить', + restoreConfirm: 'Восстановить из этой резервной копии? Текущая база данных будет перезаписана!', + restorePasswordPrompt: 'Введите пароль администратора для подтверждения восстановления', + restoreSuccess: 'База данных восстановлена', + deleteConfirm: 'Удалить эту резервную копию?', + deleted: 'Резервная копия удалена' }, r2Guide: { - title: 'Cloudflare R2 Setup Guide', - intro: 'Cloudflare R2 provides S3-compatible object storage with a free tier of 10GB storage + 1M Class A requests/month, ideal for database backups.', + title: 'Инструкция по настройке Cloudflare R2', + intro: 'Cloudflare R2 предоставляет S3-совместимое объектное хранилище с бесплатным уровнем 10GB + 1M запросов Class A в месяц — удобно для резервных копий базы.', step1: { - title: 'Create an R2 Bucket', - line1: 'Log in to the Cloudflare Dashboard (dash.cloudflare.com), select "R2 Object Storage" from the sidebar', - line2: 'Click "Create bucket", enter a name (e.g. sub2api-backups), choose a region', - line3: 'Click create to finish' + title: 'Создайте R2 Bucket', + line1: 'Войдите в Cloudflare Dashboard (dash.cloudflare.com) и выберите "R2 Object Storage" в боковом меню', + line2: 'Нажмите "Create bucket", введите имя (например, sub2api-backups) и выберите регион', + line3: 'Нажмите create для завершения' }, step2: { - title: 'Create an API Token', - line1: 'On the R2 page, click "Manage R2 API Tokens" in the top right', - line2: 'Click "Create API token", set permission to "Object Read & Write"', - line3: 'Recommended: restrict to specific bucket for better security', - line4: 'After creation, you will see the Access Key ID and Secret Access Key', - warning: 'The Secret Access Key is only shown once — copy and save it immediately!' + title: 'Создайте API Token', + line1: 'На странице R2 нажмите "Manage R2 API Tokens" справа сверху', + line2: 'Нажмите "Create API token" и задайте право "Object Read & Write"', + line3: 'Рекомендуется ограничить доступ конкретным bucket для безопасности', + line4: 'После создания вы увидите Access Key ID и Secret Access Key', + warning: 'Secret Access Key показывается только один раз — сразу скопируйте и сохраните его!' }, step3: { - title: 'Get the S3 Endpoint', - desc: 'Find your Account ID on the R2 overview page (in the URL or the right panel). The endpoint format is:', + title: 'Получите S3 Endpoint', + desc: 'Найдите Account ID на странице обзора R2 (в URL или правой панели). Формат endpoint:', accountId: 'your_account_id' }, step4: { - title: 'Fill in the Configuration', - checkEnabled: 'Checked', - bucketValue: 'Your bucket name', - fromStep2: 'Value from Step 2', - unchecked: 'Unchecked' + title: 'Заполните конфигурацию', + checkEnabled: 'Отмечено', + bucketValue: 'Имя вашего bucket', + fromStep2: 'Значение из шага 2', + unchecked: 'Не отмечено' }, - freeTier: 'R2 Free Tier: 10GB storage + 1M Class A requests + 10M Class B requests per month — more than enough for database backups.' + freeTier: 'Бесплатный уровень R2: 10GB хранилища + 1M запросов Class A + 10M запросов Class B в месяц — более чем достаточно для резервных копий базы.' } }, dataManagement: { - title: 'Data Management', - description: 'Manage data management agent status, object storage settings, and backup jobs in one place', + title: 'Управление данными', + description: 'Управляйте статусом агента, объектным хранилищем и задачами резервного копирования в одном месте', agent: { - title: 'Data Management Agent Status', - description: 'The system probes a fixed Unix socket and enables data management only when reachable.', - enabled: 'Data management agent is ready. Data management operations are available.', - disabled: 'Data management agent is unavailable. Only diagnostic information is available now.', - socketPath: 'Socket Path', + title: 'Статус агента управления данными', + description: 'Система проверяет фиксированный Unix-сокет и включает управление данными только при доступности.', + enabled: 'Агент управления данными готов. Операции доступны.', + disabled: 'Агент управления данными недоступен. Сейчас доступна только диагностика.', + socketPath: 'Путь сокета', version: 'Version', status: 'Статус', - uptime: 'Uptime', - reasonLabel: 'Unavailable Reason', + uptime: 'Время работы', + reasonLabel: 'Причина недоступности', reason: { - DATA_MANAGEMENT_AGENT_SOCKET_MISSING: 'Data management socket file is missing', - DATA_MANAGEMENT_AGENT_UNAVAILABLE: 'Data management agent is unreachable', - BACKUP_AGENT_SOCKET_MISSING: 'Backup socket file is missing', - BACKUP_AGENT_UNAVAILABLE: 'Backup agent is unreachable', - UNKNOWN: 'Unknown reason' + DATA_MANAGEMENT_AGENT_SOCKET_MISSING: 'Файл сокета управления данными отсутствует', + DATA_MANAGEMENT_AGENT_UNAVAILABLE: 'Агент управления данными недоступен', + BACKUP_AGENT_SOCKET_MISSING: 'Файл сокета резервного копирования отсутствует', + BACKUP_AGENT_UNAVAILABLE: 'Агент резервного копирования недоступен', + UNKNOWN: 'Неизвестная причина' } }, sections: { config: { - title: 'Backup Configuration', - description: 'Configure backup source, retention policy, and S3 settings.' + title: 'Настройка резервных копий', + description: 'Настройте источник резервных копий, срок хранения и параметры S3.' }, s3: { - title: 'S3 Object Storage', - description: 'Configure and test uploads of backup artifacts to a standard S3-compatible storage.' + title: 'Объектное хранилище S3', + description: 'Настройте и проверьте загрузку артефактов резервных копий в S3-совместимое хранилище.' }, backup: { - title: 'Backup Operations', - description: 'Trigger PostgreSQL, Redis, and full backup jobs.' + title: 'Операции резервного копирования', + description: 'Запускайте задачи резервного копирования PostgreSQL, Redis и полного бэкапа.' }, history: { - title: 'Backup History', - description: 'Review backup job status, errors, and artifact metadata.' + title: 'История резервных копий', + description: 'Просматривайте статусы задач, ошибки и метаданные артефактов.' } }, form: { - sourceMode: 'Source Mode', - backupRoot: 'Backup Root', - activePostgresProfile: 'Active PostgreSQL Profile', - activeRedisProfile: 'Active Redis Profile', - activeS3Profile: 'Active S3 Profile', - retentionDays: 'Retention Days', - keepLast: 'Keep Last Jobs', - uploadToS3: 'Upload to S3', - useActivePostgresProfile: 'Use Active PostgreSQL Profile', - useActiveRedisProfile: 'Use Active Redis Profile', - useActiveS3Profile: 'Use Active Profile', - idempotencyKey: 'Idempotency Key (Optional)', - secretConfigured: 'Configured already, leave empty to keep unchanged', + sourceMode: 'Режим источника', + backupRoot: 'Корень резервных копий', + activePostgresProfile: 'Активный профиль PostgreSQL', + activeRedisProfile: 'Активный профиль Redis', + activeS3Profile: 'Активный профиль S3', + retentionDays: 'Дней хранения', + keepLast: 'Хранить последние задачи', + uploadToS3: 'Загружать в S3', + useActivePostgresProfile: 'Использовать активный профиль PostgreSQL', + useActiveRedisProfile: 'Использовать активный профиль Redis', + useActiveS3Profile: 'Использовать активный профиль', + idempotencyKey: 'Ключ идемпотентности (необязательно)', + secretConfigured: 'Уже настроено, оставьте пустым без изменений', source: { - profileID: 'Profile ID (Unique)', - profileName: 'Profile Name', - setActive: 'Set as active after creation' + profileID: 'ID профиля (уникальный)', + profileName: 'Имя профиля', + setActive: 'Сделать активным после создания' }, postgres: { title: 'PostgreSQL', @@ -1604,468 +1604,468 @@ export default { password: 'Пароль', database: 'База данных', sslMode: 'Режим SSL', - containerName: 'Container Name (docker_exec mode)' + containerName: 'Имя контейнера (режим docker_exec)' }, redis: { title: 'Redis', - addr: 'Address (host:port)', + addr: 'Адрес (host:port)', username: 'Имя пользователя', password: 'Пароль', - db: 'Database Index', - containerName: 'Container Name (docker_exec mode)' + db: 'Индекс базы данных', + containerName: 'Имя контейнера (режим docker_exec)' }, s3: { - enabled: 'Enable S3 Upload', - profileID: 'Profile ID (Unique)', - profileName: 'Profile Name', - endpoint: 'Endpoint (Optional)', - region: 'Region', + enabled: 'Включить загрузку в S3', + profileID: 'ID профиля (уникальный)', + profileName: 'Имя профиля', + endpoint: 'Endpoint (необязательно)', + region: 'Регион', bucket: 'Bucket', accessKeyID: 'Access Key ID', secretAccessKey: 'Secret Access Key', - prefix: 'Object Prefix', - forcePathStyle: 'Force Path Style', - useSSL: 'Use SSL', - setActive: 'Set as active after creation' + prefix: 'Префикс объектов', + forcePathStyle: 'Принудительный Path Style', + useSSL: 'Использовать SSL', + setActive: 'Сделать активным после создания' } }, sourceProfiles: { - createTitle: 'Create Source Profile', - editTitle: 'Edit Source Profile', - empty: 'No source profiles yet, create one first', - deleteConfirm: 'Delete source profile {profileID}?', + createTitle: 'Создать профиль источника', + editTitle: 'Изменить профиль источника', + empty: 'Профилей источника пока нет, создайте первый', + deleteConfirm: 'Удалить профиль источника {profileID}?', columns: { profile: 'Профиль', active: 'Активен', - connection: 'Connection', + connection: 'Подключение', database: 'База данных', - updatedAt: 'Updated At', + updatedAt: 'Обновлено', actions: 'Действия' } }, s3Profiles: { - createTitle: 'Create S3 Profile', - editTitle: 'Edit S3 Profile', - empty: 'No S3 profiles yet, create one first', + createTitle: 'Создать профиль S3', + editTitle: 'Изменить профиль S3', + empty: 'Профилей S3 пока нет, создайте первый', editHint: 'Click "Изменить" to modify profile details in the right drawer.', - deleteConfirm: 'Delete S3 profile {profileID}?', + deleteConfirm: 'Удалить профиль S3 {profileID}?', columns: { profile: 'Профиль', active: 'Активен', - storage: 'Storage', - updatedAt: 'Updated At', + storage: 'Хранилище', + updatedAt: 'Обновлено', actions: 'Действия' } }, history: { - total: '{count} jobs', - empty: 'No backup jobs yet', + total: 'Задач: {count}', + empty: 'Задач резервного копирования пока нет', columns: { - jobID: 'Job ID', + jobID: 'ID задачи', type: 'Тип', status: 'Статус', - triggeredBy: 'Triggered By', - pgProfile: 'PostgreSQL Profile', - redisProfile: 'Redis Profile', - s3Profile: 'S3 Profile', - finishedAt: 'Finished At', - artifact: 'Artifact', + triggeredBy: 'Запущено', + pgProfile: 'Профиль PostgreSQL', + redisProfile: 'Профиль Redis', + s3Profile: 'Профиль S3', + finishedAt: 'Завершено', + artifact: 'Артефакт', error: 'Ошибка' }, status: { - queued: 'Queued', - running: 'Running', - succeeded: 'Succeeded', + queued: 'В очереди', + running: 'Выполняется', + succeeded: 'Успешно', failed: 'Ошибка', - partial_succeeded: 'Partial Succeeded' + partial_succeeded: 'Частично успешно' } }, actions: { - refresh: 'Refresh Status', - disabledHint: 'Start datamanagementd first and ensure the socket is reachable.', - reloadConfig: 'Reload Config', - reloadSourceProfiles: 'Reload Source Profiles', - reloadProfiles: 'Reload Profiles', - newSourceProfile: 'New Source Profile', - saveConfig: 'Save Config', - configSaved: 'Configuration saved', - testS3: 'Test S3 Connection', - s3TestOK: 'S3 connection test succeeded', - s3TestFailed: 'S3 connection test failed', - newProfile: 'New Profile', - saveProfile: 'Save Profile', - activateProfile: 'Activate', - profileIDRequired: 'Profile ID is required', - profileNameRequired: 'Profile name is required', - profileSelectRequired: 'Select a profile to edit first', - profileCreated: 'S3 profile created', - profileSaved: 'S3 profile saved', - profileActivated: 'S3 profile activated', - profileDeleted: 'S3 profile deleted', - sourceProfileCreated: 'Source profile created', - sourceProfileSaved: 'Source profile saved', - sourceProfileActivated: 'Source profile activated', - sourceProfileDeleted: 'Source profile deleted', - createBackup: 'Create Backup Job', - jobCreated: 'Backup job created: {jobID} ({status})', - refreshJobs: 'Refresh Jobs', - loadMore: 'Load More' + refresh: 'Обновить статус', + disabledHint: 'Сначала запустите datamanagementd и убедитесь, что сокет доступен.', + reloadConfig: 'Перезагрузить конфиг', + reloadSourceProfiles: 'Перезагрузить профили источников', + reloadProfiles: 'Перезагрузить профили', + newSourceProfile: 'Новый профиль источника', + saveConfig: 'Сохранить конфиг', + configSaved: 'Конфигурация сохранена', + testS3: 'Проверить подключение S3', + s3TestOK: 'Проверка подключения S3 успешна', + s3TestFailed: 'Проверка подключения S3 не удалась', + newProfile: 'Новый профиль', + saveProfile: 'Сохранить профиль', + activateProfile: 'Активировать', + profileIDRequired: 'ID профиля обязателен', + profileNameRequired: 'Имя профиля обязательно', + profileSelectRequired: 'Сначала выберите профиль для изменения', + profileCreated: 'Профиль S3 создан', + profileSaved: 'Профиль S3 сохранён', + profileActivated: 'Профиль S3 активирован', + profileDeleted: 'Профиль S3 удалён', + sourceProfileCreated: 'Профиль источника создан', + sourceProfileSaved: 'Профиль источника сохранён', + sourceProfileActivated: 'Профиль источника активирован', + sourceProfileDeleted: 'Профиль источника удалён', + createBackup: 'Создать задачу резервного копирования', + jobCreated: 'Задача резервного копирования создана: {jobID} ({status})', + refreshJobs: 'Обновить задачи', + loadMore: 'Загрузить ещё' } }, affiliates: { - invitesDescription: 'View site-wide inviter and invitee relationships', - rebatesDescription: 'View recharge orders that generated affiliate rebates', - transfersDescription: 'View affiliate quota transfers into account balance', + invitesDescription: 'Просмотр связей пригласивших и приглашённых по всему сайту', + rebatesDescription: 'Просмотр заказов пополнения, по которым начислены affiliate-вознаграждения', + transfersDescription: 'Просмотр переводов affiliate-квоты на баланс аккаунта', errors: { - loadFailed: 'Failed to load affiliate records' + loadFailed: 'Не удалось загрузить affiliate-записи' }, records: { search: 'Поиск', - searchPlaceholder: 'Email, username, user ID, or order number', - startAt: 'Start date', - endAt: 'End date', - inviter: 'Inviter', - invitee: 'Invitee', + searchPlaceholder: 'Email, имя пользователя, ID пользователя или номер заказа', + startAt: 'Дата начала', + endAt: 'Дата окончания', + inviter: 'Пригласивший', + invitee: 'Приглашённый', user: 'Пользователь', - affCode: 'Invite Code', - order: 'Order', - totalRebate: 'Total Rebate', - orderAmount: 'Top-up Amount', - payAmount: 'Paid Amount', - rebateAmount: 'Rebate Amount', + affCode: 'Код приглашения', + order: 'Заказ', + totalRebate: 'Всего вознаграждений', + orderAmount: 'Сумма пополнения', + payAmount: 'Оплаченная сумма', + rebateAmount: 'Сумма вознаграждения', paymentType: 'Способ оплаты', - orderStatus: 'Order Status', - transferAmount: 'Transfer Amount', - balanceAfter: 'Balance After', - availableQuotaAfter: 'Available After', - frozenQuotaAfter: 'Frozen After', - historyQuotaAfter: 'Historical Rebate After', - invitedAt: 'Invited At', - rebatedAt: 'Rebated At', - transferredAt: 'Transferred At' + orderStatus: 'Статус заказа', + transferAmount: 'Сумма перевода', + balanceAfter: 'Баланс после', + availableQuotaAfter: 'Доступно после', + frozenQuotaAfter: 'Заморожено после', + historyQuotaAfter: 'Историческое вознаграждение после', + invitedAt: 'Приглашён', + rebatedAt: 'Начислено', + transferredAt: 'Переведено' }, overview: { - title: 'Affiliate User Overview', - affCode: 'Invite Code', - rebateRate: 'Rebate Rate', + title: 'Обзор affiliate-пользователя', + affCode: 'Код приглашения', + rebateRate: 'Ставка вознаграждения', invitedCount: 'Приглашённые пользователи', - rebatedInviteeCount: 'Rebated Invitees', - availableQuota: 'Available Quota', - historyQuota: 'Historical Rebate' + rebatedInviteeCount: 'Приглашённые с вознаграждением', + availableQuota: 'Доступная квота', + historyQuota: 'Историческое вознаграждение' } }, // Users users: { - title: 'User Management', - description: 'Manage users and their permissions', - createUser: 'Create User', - editUser: 'Edit User', - deleteUser: 'Delete User', - searchUsers: 'Search by email, username, notes, or API key...', - allRoles: 'All Roles', + title: 'Управление пользователями', + description: 'Управляйте пользователями и их правами', + createUser: 'Создать пользователя', + editUser: 'Изменить пользователя', + deleteUser: 'Удалить пользователя', + searchUsers: 'Поиск по email, имени, заметкам или API-ключу...', + allRoles: 'Все роли', allStatus: 'Все статусы', allGroups: 'Все группы', searchGroups: 'Поиск групп...', - fuzzySearch: 'Fuzzy search', - admin: 'Admin', + fuzzySearch: 'Нечёткий поиск', + admin: 'Администратор', user: 'Пользователь', disabled: 'Отключено', email: 'Email', password: 'Пароль', username: 'Имя пользователя', - notes: 'Notes', - enterEmail: 'Enter email', - enterPassword: 'Enter password', - enterUsername: 'Enter username (optional)', - enterNotes: 'Enter notes (admin only)', - notesHint: 'This note is only visible to administrators', - enterNewPassword: 'Enter new password (optional)', - leaveEmptyToKeep: 'Leave empty to keep current password', - generatePassword: 'Generate random password', - copyPassword: 'Copy password', - creating: 'Creating...', + notes: 'Заметки', + enterEmail: 'Введите email', + enterPassword: 'Введите пароль', + enterUsername: 'Введите имя пользователя (необязательно)', + enterNotes: 'Введите заметки (только для админов)', + notesHint: 'Эта заметка видна только администраторам', + enterNewPassword: 'Введите новый пароль (необязательно)', + leaveEmptyToKeep: 'Оставьте пустым, чтобы сохранить текущий пароль', + generatePassword: 'Сгенерировать случайный пароль', + copyPassword: 'Скопировать пароль', + creating: 'Создание...', updating: 'Обновление...', form: { - rpmLimit: 'Requests Per Minute (RPM)', - rpmLimitPlaceholder: '0 = unlimited', - rpmLimitHint: 'Max requests per minute for this user; 0 = unlimited. Acts as a fallback only when the group has no rpm_limit set.' + rpmLimit: 'Запросов в минуту (RPM)', + rpmLimitPlaceholder: '0 = без лимита', + rpmLimitHint: 'Максимум запросов в минуту для пользователя; 0 = без лимита. Используется как резерв, если у группы не задан rpm_limit.' }, columns: { user: 'Пользователь', id: 'ID', email: 'Email', username: 'Имя пользователя', - notes: 'Notes', + notes: 'Заметки', role: 'Роль', groups: 'Группы', subscriptions: 'Подписки', balance: 'Баланс', - balancePlatformQuota: 'Balance (Platform Quota)', + balancePlatformQuota: 'Баланс (квота платформ)', usage: 'Расход', - usageAnthropic: 'Usage (Claude)', - usageOpenAI: 'Usage (OpenAI)', - usageGemini: 'Usage (Gemini)', - usageAntigravity: 'Usage (Antigravity)', + usageAnthropic: 'Расход (Claude)', + usageOpenAI: 'Расход (OpenAI)', + usageGemini: 'Расход (Gemini)', + usageAntigravity: 'Расход (Antigravity)', concurrency: 'Параллелизм', status: 'Статус', - lastActive: 'Last Active', + lastActive: 'Последняя активность', lastUsed: 'Последнее использование', created: 'Создан', actions: 'Действия' }, today: 'Сегодня', total: 'Последние 30 дн.', - sortBy: 'Sort By', - sortCurrentPageOnly: 'Sorts current page only', - noSubscription: 'No subscription', - publicGroupCount: '+{count} public', - exclusiveLabel: 'exclusive', - publicLabel: 'public', + sortBy: 'Сортировать по', + sortCurrentPageOnly: 'Сортирует только текущую страницу', + noSubscription: 'Нет подписки', + publicGroupCount: '+{count} публичных', + exclusiveLabel: 'эксклюзивная', + publicLabel: 'публичная', daysRemaining: '{days}d', expired: 'Истекла', disable: 'Отключить', enable: 'Включить', - disableUser: 'Disable User', - enableUser: 'Enable User', - viewApiKeys: 'View API Keys', + disableUser: 'Отключить пользователя', + enableUser: 'Включить пользователя', + viewApiKeys: 'Показать API-ключи', groups: 'Группы', apiKeys: 'API-ключи', - userApiKeys: 'User API Keys', - noApiKeys: 'This user has no API keys', + userApiKeys: 'API-ключи пользователя', + noApiKeys: 'У пользователя нет API-ключей', group: 'Группа', none: 'Нет', - groupChangedSuccess: 'Group updated successfully', - groupChangedWithGrant: 'Group updated. User auto-granted access to "{group}"', - groupChangeFailed: 'Failed to update group', - noUsersYet: 'No users yet', - createFirstUser: 'Create your first user to get started.', - userCreated: 'User created successfully', - userUpdated: 'User updated successfully', - userDeleted: 'User deleted successfully', - userEnabled: 'User enabled successfully', - userDisabled: 'User disabled successfully', - failedToLoad: 'Failed to load users', - failedToCreate: 'Failed to create user', - failedToUpdate: 'Failed to update user', - failedToDelete: 'Failed to delete user', - failedToToggle: 'Failed to update user status', - failedToLoadApiKeys: 'Failed to load user API keys', - emailRequired: 'Please enter email', - concurrencyMin: 'Concurrency must be at least 1', - soraStorageQuota: 'Sora Storage Quota', - soraStorageQuotaHint: 'In GB, 0 means use group or system default quota', - amountRequired: 'Please enter a valid amount', - insufficientBalance: 'Insufficient balance', - deleteConfirm: "Are you sure you want to delete '{email}'? This action cannot be undone.", - setAllowedGroups: 'Set Allowed Groups', + groupChangedSuccess: 'Группа обновлена', + groupChangedWithGrant: 'Группа обновлена. Пользователю автоматически выдан доступ к "{group}"', + groupChangeFailed: 'Не удалось обновить группу', + noUsersYet: 'Пользователей пока нет', + createFirstUser: 'Создайте первого пользователя, чтобы начать.', + userCreated: 'Пользователь создан', + userUpdated: 'Пользователь обновлён', + userDeleted: 'Пользователь удалён', + userEnabled: 'Пользователь включён', + userDisabled: 'Пользователь отключён', + failedToLoad: 'Не удалось загрузить пользователей', + failedToCreate: 'Не удалось создать пользователя', + failedToUpdate: 'Не удалось обновить пользователя', + failedToDelete: 'Не удалось удалить пользователя', + failedToToggle: 'Не удалось обновить статус пользователя', + failedToLoadApiKeys: 'Не удалось загрузить API-ключи пользователя', + emailRequired: 'Введите email', + concurrencyMin: 'Параллелизм должен быть не меньше 1', + soraStorageQuota: 'Квота хранилища Sora', + soraStorageQuotaHint: 'В GB, 0 = использовать квоту группы или системы по умолчанию', + amountRequired: 'Введите корректную сумму', + insufficientBalance: 'Недостаточно баланса', + deleteConfirm: "Удалить пользователя '{email}'? Это действие нельзя отменить.", + setAllowedGroups: 'Разрешённые группы', allowedGroupsHint: - 'Select which standard groups this user can use. Subscription groups are managed separately.', - noStandardGroups: 'No standard groups available', - allowAllGroups: 'Allow All Groups', - allowAllGroupsHint: 'User can use any non-exclusive group', - allowedGroupsUpdated: 'Allowed groups updated successfully', - failedToLoadGroups: 'Failed to load groups', - failedToUpdateAllowedGroups: 'Failed to update allowed groups', + 'Выберите стандартные группы, доступные пользователю. Группы подписки управляются отдельно.', + noStandardGroups: 'Нет доступных стандартных групп', + allowAllGroups: 'Разрешить все группы', + allowAllGroupsHint: 'Пользователь может использовать любую неэксклюзивную группу', + allowedGroupsUpdated: 'Разрешённые группы обновлены', + failedToLoadGroups: 'Не удалось загрузить группы', + failedToUpdateAllowedGroups: 'Не удалось обновить разрешённые группы', // User Group Configuration - groupConfig: 'User Group Configuration', - groupConfigHint: 'Configure custom rate multipliers for user {email} (overrides group defaults)', - exclusiveGroups: 'Exclusive Groups', - publicGroups: 'Public Groups (Default Available)', - defaultRate: 'Default Rate', - customRate: 'Custom Rate', - useDefaultRate: 'Use Default', - customRatePlaceholder: 'Leave empty for default', - groupConfigUpdated: 'Group configuration updated successfully', - replaceGroup: 'Replace Group', - clickToReplace: 'Click to replace', - replaceGroupTitle: 'Replace Exclusive Group', - replaceGroupHint: 'Select a new group to replace "{old}". Keys will be migrated and permissions updated automatically.', - replaceGroupConfirm: 'Confirm Replace', - replaceGroupSuccess: 'Group replaced successfully, {count} key(s) migrated', - selectNewGroup: 'Select target group', - noOtherGroups: 'No other exclusive groups available', - deposit: 'Deposit', - withdraw: 'Withdraw', - depositAmount: 'Deposit Amount', - withdrawAmount: 'Withdraw Amount', + groupConfig: 'Настройки групп пользователя', + groupConfigHint: 'Настройте свои коэффициенты тарифа для пользователя {email} (переопределяют настройки групп)', + exclusiveGroups: 'Эксклюзивные группы', + publicGroups: 'Публичные группы (доступны по умолчанию)', + defaultRate: 'Тариф по умолчанию', + customRate: 'Свой тариф', + useDefaultRate: 'По умолчанию', + customRatePlaceholder: 'Оставьте пустым для значения по умолчанию', + groupConfigUpdated: 'Настройки групп обновлены', + replaceGroup: 'Заменить группу', + clickToReplace: 'Нажмите для замены', + replaceGroupTitle: 'Заменить эксклюзивную группу', + replaceGroupHint: 'Выберите новую группу вместо "{old}". Ключи будут перенесены, права обновятся автоматически.', + replaceGroupConfirm: 'Подтвердить замену', + replaceGroupSuccess: 'Группа заменена, перенесено ключей: {count}', + selectNewGroup: 'Выберите целевую группу', + noOtherGroups: 'Нет других эксклюзивных групп', + deposit: 'Пополнить', + withdraw: 'Списать', + depositAmount: 'Сумма пополнения', + withdrawAmount: 'Сумма списания', withdrawAll: 'Все', currentBalance: 'Текущий баланс', depositNotesPlaceholder: - 'e.g., New user registration bonus, promotional credit, compensation, etc.', + 'например: бонус за регистрацию, промокредит, компенсация и т. п.', withdrawNotesPlaceholder: - 'e.g., Service issue refund, incorrect charge reversal, account closure refund, etc.', - notesOptional: 'Notes are optional but helpful for record keeping', - amountHint: 'Please enter a positive amount', + 'например: возврат за сбой сервиса, отмена ошибочного начисления, закрытие аккаунта и т. п.', + notesOptional: 'Заметки необязательны, но полезны для истории', + amountHint: 'Введите положительную сумму', newBalance: 'Новый баланс', - depositing: 'Depositing...', - withdrawing: 'Withdrawing...', - confirmDeposit: 'Confirm Deposit', - confirmWithdraw: 'Confirm Withdraw', - depositSuccess: 'Deposit successful', - withdrawSuccess: 'Withdraw successful', - failedToDeposit: 'Failed to deposit', - failedToWithdraw: 'Failed to withdraw', - useDepositWithdrawButtons: 'Please use deposit/withdraw buttons to adjust balance', + depositing: 'Пополнение...', + withdrawing: 'Списание...', + confirmDeposit: 'Подтвердить пополнение', + confirmWithdraw: 'Подтвердить списание', + depositSuccess: 'Пополнение выполнено', + withdrawSuccess: 'Списание выполнено', + failedToDeposit: 'Не удалось пополнить', + failedToWithdraw: 'Не удалось списать', + useDepositWithdrawButtons: 'Используйте кнопки пополнения/списания для изменения баланса', // Balance History - balanceHistory: 'Recharge History', - balanceHistoryTip: 'Click to open recharge history', - columnAlwaysVisible: 'This column is always visible', + balanceHistory: 'История пополнений', + balanceHistoryTip: 'Нажмите, чтобы открыть историю пополнений', + columnAlwaysVisible: 'Этот столбец всегда виден', // Per-platform usage breakdown (hover tooltip) - platformBreakdown: 'Per-platform breakdown', + platformBreakdown: 'Разбивка по платформам', platformBreakdownEmpty: 'Расхода по платформам пока нет', - platformBreakdownHint: 'Hover for per-platform usage', + platformBreakdownHint: 'Наведите, чтобы увидеть расход по платформам', platformOther: 'Другое', - balanceHistoryTitle: 'User Recharge & Concurrency History', - noBalanceHistory: 'No records found for this user', - allTypes: 'All Types', - typeBalance: 'Balance (Redeem)', - typeAffiliateBalance: 'Balance (Affiliate Transfer)', - typeAdminBalance: 'Balance (Admin)', - typeConcurrency: 'Concurrency (Redeem)', - typeAdminConcurrency: 'Concurrency (Admin)', - typeSubscription: 'Subscription', - failedToLoadBalanceHistory: 'Failed to load balance history', + balanceHistoryTitle: 'История пополнений и параллелизма пользователя', + noBalanceHistory: 'Записей для этого пользователя нет', + allTypes: 'Все типы', + typeBalance: 'Баланс по коду', + typeAffiliateBalance: 'Баланс (партнёрский перевод)', + typeAdminBalance: 'Баланс (админ)', + typeConcurrency: 'Параллелизм по коду', + typeAdminConcurrency: 'Параллелизм (админ)', + typeSubscription: 'Подписка', + failedToLoadBalanceHistory: 'Не удалось загрузить историю баланса', createdAt: 'Создан', - totalRecharged: 'Total Recharged', + totalRecharged: 'Всего пополнено', roles: { - admin: 'Admin', + admin: 'Администратор', user: 'Пользователь' }, // Settings Dropdowns - filterSettings: 'Filter Settings', - columnSettings: 'Column Settings', - filterValue: 'Enter value', + filterSettings: 'Настройки фильтра', + columnSettings: 'Настройки столбцов', + filterValue: 'Введите значение', // User Attributes attributes: { - title: 'User Attributes', - description: 'Configure custom user attribute fields', - configButton: 'Attributes', - addAttribute: 'Add Attribute', - editAttribute: 'Edit Attribute', - deleteAttribute: 'Delete Attribute', - deleteConfirm: "Are you sure you want to delete attribute '{name}'? All user values for this attribute will be deleted.", - noAttributes: 'No custom attributes', - noAttributesHint: 'Click the button above to add custom attributes', - key: 'Attribute Key', - keyHint: 'For programmatic reference, only letters, numbers and underscores', - name: 'Display Name', - nameHint: 'Name shown in forms', - type: 'Attribute Type', + title: 'Атрибуты пользователя', + description: 'Настройте пользовательские поля атрибутов', + configButton: 'Атрибуты', + addAttribute: 'Добавить атрибут', + editAttribute: 'Изменить атрибут', + deleteAttribute: 'Удалить атрибут', + deleteConfirm: "Удалить атрибут '{name}'? Все значения пользователей для него будут удалены.", + noAttributes: 'Нет пользовательских атрибутов', + noAttributesHint: 'Нажмите кнопку выше, чтобы добавить атрибуты', + key: 'Ключ атрибута', + keyHint: 'Для программного доступа: только буквы, цифры и подчёркивания', + name: 'Отображаемое имя', + nameHint: 'Имя, отображаемое в формах', + type: 'Тип атрибута', fieldDescription: 'Описание', - fieldDescriptionHint: 'Description text for the attribute', + fieldDescriptionHint: 'Описание атрибута', placeholder: 'Placeholder', - placeholderHint: 'Placeholder text for input field', - required: 'Required', + placeholderHint: 'Текст placeholder для поля ввода', + required: 'Обязательно', enabled: 'Включено', - options: 'Options', - optionsHint: 'For select/multi-select types', - addOption: 'Add Option', - optionValue: 'Option Value', - optionLabel: 'Display Text', - validation: 'Validation Rules', - minLength: 'Min Length', - maxLength: 'Max Length', - min: 'Min Value', - max: 'Max Value', - pattern: 'Regex Pattern', - patternMessage: 'Validation Error Message', + options: 'Варианты', + optionsHint: 'Для типов select/multi-select', + addOption: 'Добавить вариант', + optionValue: 'Значение варианта', + optionLabel: 'Отображаемый текст', + validation: 'Правила валидации', + minLength: 'Мин. длина', + maxLength: 'Макс. длина', + min: 'Мин. значение', + max: 'Макс. значение', + pattern: 'Regex-шаблон', + patternMessage: 'Сообщение ошибки валидации', types: { - text: 'Text', - textarea: 'Textarea', - number: 'Number', + text: 'Текст', + textarea: 'Многострочный текст', + number: 'Число', email: 'Email', url: 'URL', date: 'Дата', - select: 'Select', - multi_select: 'Multi-Select' + select: 'Список', + multi_select: 'Мультивыбор' }, - created: 'Attribute created successfully', - updated: 'Attribute updated successfully', - deleted: 'Attribute deleted successfully', - reordered: 'Attribute order updated successfully', - failedToLoad: 'Failed to load attributes', - failedToCreate: 'Failed to create attribute', - failedToUpdate: 'Failed to update attribute', - keyRequired: 'Please enter attribute key', - nameRequired: 'Please enter display name', - optionsRequired: 'Please add at least one option', - failedToDelete: 'Failed to delete attribute', - failedToReorder: 'Failed to update order', - keyExists: 'Attribute key already exists', - dragToReorder: 'Drag to reorder' + created: 'Атрибут создан', + updated: 'Атрибут обновлён', + deleted: 'Атрибут удалён', + reordered: 'Порядок атрибутов обновлён', + failedToLoad: 'Не удалось загрузить атрибуты', + failedToCreate: 'Не удалось создать атрибут', + failedToUpdate: 'Не удалось обновить атрибут', + keyRequired: 'Введите ключ атрибута', + nameRequired: 'Введите отображаемое имя', + optionsRequired: 'Добавьте хотя бы один вариант', + failedToDelete: 'Не удалось удалить атрибут', + failedToReorder: 'Не удалось обновить порядок', + keyExists: 'Ключ атрибута уже существует', + dragToReorder: 'Перетащите для изменения порядка' }, platformQuota: { - menuItem: 'Platform Quotas', - title: 'Platform Quotas', - subtitle: 'Configure daily / weekly / monthly USD usage limits for each upstream platform for user {email}', + menuItem: 'Квоты платформ', + title: 'Квоты платформ', + subtitle: 'Настройте дневные, недельные и месячные лимиты расхода в USD для каждой апстрим-платформы пользователя {email}', columns: { platform: 'Платформа', - daily: 'Daily (USD)', - weekly: 'Weekly (USD)', - monthly: 'Monthly (USD, 30-day rolling)', - usage: 'Current Usage', + daily: 'День (USD)', + weekly: 'Неделя (USD)', + monthly: 'Месяц (USD, скользящие 30 дней)', + usage: 'Текущий расход', }, placeholder: 'безлимитно', save: 'Сохранить', saving: 'Сохранение...', cancel: 'Отмена', - clearAll: 'Clear All (remove all limits)', - clearAllConfirm: 'Clear daily / weekly / monthly limits for ALL platforms? All platforms will become "безлимитно" with no local undo — you must manually re-enter values before saving.', + clearAll: 'Очистить всё (убрать все лимиты)', + clearAllConfirm: 'Очистить дневные, недельные и месячные лимиты для ВСЕХ платформ? Все платформы станут "безлимитно" без локальной отмены — перед сохранением значения нужно будет ввести заново.', reset: { - button: 'Reset window', - confirm: 'Reset the {window} usage for {platform} for this user? This is effective immediately.', - success: 'Reset {platform} {window} usage', - failed: 'Reset failed', + button: 'Сбросить окно', + confirm: 'Сбросить расход за {window} для {platform} у этого пользователя? Изменение вступит в силу сразу.', + success: 'Расход {platform} за {window} сброшен', + failed: 'Сброс не удался', }, - updateSuccess: 'Platform quotas updated', - updateFailed: 'Save failed', - loadFailed: 'Load failed', - hint: 'Empty = no limit for that window.', - windowDaily: 'daily', - windowWeekly: 'weekly', - windowMonthly: 'monthly', - cellNotConfigured: 'Not configured', - cellColumnTooltip: 'Only platforms with a limit are shown', - subscriptionWarning: 'This user has an active subscription. Platform quotas only apply to balance (standard) mode requests; subscription mode requests are not subject to these limits.', - invalidNumber: 'The following fields contain invalid numbers. Please fix them before saving: {fields}', + updateSuccess: 'Квоты платформ обновлены', + updateFailed: 'Не удалось сохранить', + loadFailed: 'Не удалось загрузить', + hint: 'Пусто = без лимита для этого окна.', + windowDaily: 'день', + windowWeekly: 'неделя', + windowMonthly: 'месяц', + cellNotConfigured: 'Не настроено', + cellColumnTooltip: 'Показаны только платформы с лимитом', + subscriptionWarning: 'У пользователя активная подписка. Квоты платформ применяются только к запросам в режиме баланса (standard); запросы по подписке этими лимитами не ограничены.', + invalidNumber: 'В этих полях некорректные числа. Исправьте перед сохранением: {fields}', } }, // Groups groups: { - title: 'Group Management', - description: 'Manage API key groups and rate multipliers', + title: 'Управление группами', + description: 'Управляйте группами API-ключей и коэффициентами тарифа', searchGroups: 'Поиск групп...', - createGroup: 'Create Group', - editGroup: 'Edit Group', - deleteGroup: 'Delete Group', - sortOrder: 'Sort', - sortOrderHint: 'Drag groups to adjust display order, groups at the top will be displayed first', - sortOrderUpdated: 'Sort order updated', - failedToUpdateSortOrder: 'Failed to update sort order', - allPlatforms: 'All Platforms', + createGroup: 'Создать группу', + editGroup: 'Изменить группу', + deleteGroup: 'Удалить группу', + sortOrder: 'Сортировка', + sortOrderHint: 'Перетащите группы, чтобы изменить порядок показа. Верхние группы отображаются первыми', + sortOrderUpdated: 'Порядок сортировки обновлён', + failedToUpdateSortOrder: 'Не удалось обновить порядок сортировки', + allPlatforms: 'Все платформы', allStatus: 'Все статусы', allGroups: 'Все группы', exclusive: 'Эксклюзивная', - nonExclusive: 'Non-Exclusive', + nonExclusive: 'Неэксклюзивная', public: 'Публичная', columns: { name: 'Имя', platform: 'Платформа', - rateMultiplier: 'Rate Multiplier', - rpmOverride: 'RPM Override', - rpmOverrideHint: 'Per-user RPM cap in this group; empty = group default; 0 = unlimited', - rateDefault: 'default', - rpmDefault: 'default', + rateMultiplier: 'Коэффициент тарифа', + rpmOverride: 'Переопределение RPM', + rpmOverrideHint: 'Лимит RPM пользователя в этой группе; пусто = значение группы; 0 = без лимита', + rateDefault: 'по умолчанию', + rpmDefault: 'по умолчанию', type: 'Тип', accounts: 'Аккаунты', - capacity: 'Capacity', + capacity: 'Ёмкость', usage: 'Расход', status: 'Статус', actions: 'Действия', - billingType: 'Billing Type', + billingType: 'Тип списания', userName: 'Имя пользователя', userEmail: 'Email', userNotes: 'Notes', @@ -2073,76 +2073,76 @@ export default { }, usageToday: 'Сегодня', usageTotal: 'Итого', - accountsAvailable: 'Avail:', - accountsRateLimited: 'Limited:', - accountsTotal: 'Total:', + accountsAvailable: 'Доступно:', + accountsRateLimited: 'Ограничено:', + accountsTotal: 'Всего:', accountsUnit: '', - rateAndAccounts: '{rate}x rate · {count} accounts', - accountsCount: '{count} accounts', + rateAndAccounts: 'тариф {rate}x · аккаунтов: {count}', + accountsCount: 'Аккаунтов: {count}', form: { name: 'Имя', description: 'Описание', platform: 'Платформа', - rateMultiplier: 'Rate Multiplier', + rateMultiplier: 'Коэффициент тарифа', status: 'Статус', - exclusive: 'Exclusive Group', - rpmLimit: 'Requests Per Minute (RPM)', - rpmLimitPlaceholder: '0 = unlimited', - rpmLimitHint: 'Max requests per minute for each user in this group; 0 = unlimited. Once set, it takes over per-user rate limiting in this group (overrides the user-level rpm_limit fallback).' - }, - enterGroupName: 'Enter group name', - optionalDescription: 'Optional description', - platformHint: 'Select the platform this group is associated with', - platformNotEditable: 'Platform cannot be changed after creation', - rateMultiplierHint: 'Cost multiplier for this group (e.g., 1.5 = 150% of base cost)', - exclusiveHint: 'Exclusive group, manually assign to specific users', + exclusive: 'Эксклюзивная группа', + rpmLimit: 'Запросов в минуту (RPM)', + rpmLimitPlaceholder: '0 = без лимита', + rpmLimitHint: 'Максимум запросов в минуту для каждого пользователя в группе; 0 = без лимита. Если задано, переопределяет пользовательский rpm_limit в этой группе.' + }, + enterGroupName: 'Введите имя группы', + optionalDescription: 'Описание (необязательно)', + platformHint: 'Выберите платформу, связанную с группой', + platformNotEditable: 'Платформу нельзя изменить после создания', + rateMultiplierHint: 'Коэффициент стоимости для группы (например, 1.5 = 150% базовой стоимости)', + exclusiveHint: 'Эксклюзивная группа, назначается пользователям вручную', exclusiveTooltip: { - title: 'What is an exclusive group?', - description: 'When enabled, users cannot see this group when creating API Keys. Only after an admin manually assigns a user to this group can they use it.', - example: 'Use case:', - exampleContent: 'Public group rate is 0.8. Create an exclusive group with 0.7 rate, manually assign VIP users to give them better pricing.' - }, - noGroupsYet: 'No groups yet', - createFirstGroup: 'Create your first group to organize API keys.', - creating: 'Creating...', + title: 'Что такое эксклюзивная группа?', + description: 'Если включено, пользователи не видят эту группу при создании API-ключей. Использовать её можно только после ручного назначения администратором.', + example: 'Пример:', + exampleContent: 'Тариф публичной группы 0.8. Создайте эксклюзивную группу с тарифом 0.7 и вручную назначьте VIP-пользователей для лучшей цены.' + }, + noGroupsYet: 'Групп пока нет', + createFirstGroup: 'Создайте первую группу для организации API-ключей.', + creating: 'Создание...', updating: 'Обновление...', limitDay: 'd', limitWeek: 'w', limitMonth: 'mo', - groupCreated: 'Group created successfully', - groupUpdated: 'Group updated successfully', - groupDeleted: 'Group deleted successfully', - failedToLoad: 'Failed to load groups', - failedToCreate: 'Failed to create group', - failedToUpdate: 'Failed to update group', - failedToDelete: 'Failed to delete group', - nameRequired: 'Please enter group name', - rateMultipliers: 'Rate Multipliers', - rateMultipliersTitle: 'Group Rate Multipliers', - addUserRate: 'Add User Rate Multiplier', - rpmOverrides: 'RPM Overrides', - rpmOverridesTitle: 'Group RPM Overrides', - addUserRpm: 'Add User RPM Override', - noRpmOverrides: 'No users have an RPM override yet', - rpmSaved: 'RPM overrides saved', - groupRpmDefault: 'Group default RPM', - searchUserPlaceholder: 'Search user email...', - noRateMultipliers: 'No user rate multipliers configured', - rateUpdated: 'Rate multiplier updated', - rateDeleted: 'Rate multiplier removed', - rateAdded: 'Rate multiplier added', - clearAll: 'Clear All', - confirmClearAll: 'Are you sure you want to clear all rate multiplier settings for this group? This cannot be undone.', - rateCleared: 'All rate multipliers cleared', - batchAdjust: 'Batch Adjust Rates', - multiplierFactor: 'Factor', + groupCreated: 'Группа создана', + groupUpdated: 'Группа обновлена', + groupDeleted: 'Группа удалена', + failedToLoad: 'Не удалось загрузить группы', + failedToCreate: 'Не удалось создать группу', + failedToUpdate: 'Не удалось обновить группу', + failedToDelete: 'Не удалось удалить группу', + nameRequired: 'Введите имя группы', + rateMultipliers: 'Коэффициенты тарифа', + rateMultipliersTitle: 'Коэффициенты тарифа группы', + addUserRate: 'Добавить коэффициент тарифа пользователя', + rpmOverrides: 'Переопределения RPM', + rpmOverridesTitle: 'Переопределения RPM группы', + addUserRpm: 'Добавить переопределение RPM пользователя', + noRpmOverrides: 'Переопределений RPM пока нет', + rpmSaved: 'Переопределения RPM сохранены', + groupRpmDefault: 'RPM группы по умолчанию', + searchUserPlaceholder: 'Поиск email пользователя...', + noRateMultipliers: 'Коэффициенты тарифа пользователей не настроены', + rateUpdated: 'Коэффициент тарифа обновлён', + rateDeleted: 'Коэффициент тарифа удалён', + rateAdded: 'Коэффициент тарифа добавлен', + clearAll: 'Очистить всё', + confirmClearAll: 'Очистить все настройки коэффициентов тарифа для этой группы? Это действие нельзя отменить.', + rateCleared: 'Все коэффициенты тарифа очищены', + batchAdjust: 'Массово изменить тарифы', + multiplierFactor: 'Коэффициент', applyMultiplier: 'Применить', - rateAdjusted: 'Rates adjusted successfully', - rateSaved: 'Rate multipliers saved', - finalRate: 'Final Rate', - unsavedChanges: 'Unsaved changes', - revertChanges: 'Revert', - userInfo: 'User Info', + rateAdjusted: 'Тарифы изменены', + rateSaved: 'Коэффициенты тарифа сохранены', + finalRate: 'Итоговый тариф', + unsavedChanges: 'Несохранённые изменения', + revertChanges: 'Откатить', + userInfo: 'Информация о пользователе', platforms: { all: 'All Platforms', anthropic: 'Anthropic', @@ -2151,125 +2151,125 @@ export default { antigravity: 'Antigravity', }, deleteConfirm: - "Are you sure you want to delete '{name}'? All associated API keys will no longer belong to any group.", + "Удалить группу '{name}'? Все связанные API-ключи больше не будут принадлежать группе.", deleteConfirmSubscription: - "Are you sure you want to delete subscription group '{name}'? This will invalidate all API keys bound to this subscription and delete all related subscription records. This action cannot be undone.", + "Удалить группу подписки '{name}'? Все API-ключи этой подписки станут недействительными, а связанные записи подписок будут удалены. Это действие нельзя отменить.", subscription: { - title: 'Subscription Settings', - type: 'Billing Type', + title: 'Настройки подписки', + type: 'Тип списания', typeHint: - 'Standard billing deducts from user balance. Subscription mode uses quota limits instead.', - typeNotEditable: 'Billing type cannot be changed after group creation.', - standard: 'Standard (Balance)', - subscription: 'Subscription (Quota)', + 'В стандартном режиме списание идёт с баланса пользователя. Режим подписки использует лимиты квоты.', + typeNotEditable: 'Тип списания нельзя изменить после создания группы.', + standard: 'Стандартный режим (баланс)', + subscription: 'Подписка (квота)', dailyLimit: 'Дневной лимит (USD)', - weeklyLimit: 'Weekly Limit (USD)', - monthlyLimit: 'Monthly Limit (USD)', - defaultValidityDays: 'Default Validity (Days)', - validityHint: 'Number of days the subscription is valid when assigned to a user', - noLimit: 'No limit' + weeklyLimit: 'Недельный лимит (USD)', + monthlyLimit: 'Месячный лимит (USD)', + defaultValidityDays: 'Срок действия по умолчанию (дни)', + validityHint: 'Сколько дней действует подписка после назначения пользователю', + noLimit: 'Без лимита' }, imagePricing: { - title: 'Image Generation Pricing', - description: 'Configure image generation access and base image prices. Leave empty to use default prices.', - allowImageGeneration: 'Allow image generation for this group', - independentMultiplier: 'Use independent image multiplier', - imageMultiplier: 'Image multiplier', - modeHint: 'By default, image billing uses image price × current effective group multiplier. Independent mode uses image price × image multiplier.', - finalPricePreview: 'Final per-image price preview', - notConfigured: 'Not configured' + title: 'Цены генерации изображений', + description: 'Настройте доступ к генерации изображений и базовые цены. Оставьте пустым для цен по умолчанию.', + allowImageGeneration: 'Разрешить генерацию изображений для этой группы', + independentMultiplier: 'Использовать отдельный коэффициент изображений', + imageMultiplier: 'Коэффициент изображений', + modeHint: 'По умолчанию списание за изображения: цена изображения × текущий коэффициент группы. Отдельный режим: цена изображения × коэффициент изображений.', + finalPricePreview: 'Итоговая цена за изображение', + notConfigured: 'Не настроено' }, modelsList: { - title: 'Custom /v1/models Model List', - hint: 'Only changes the /v1/models response. Whitelist model calls and account routing are unchanged.', - loading: 'Loading model list...', - empty: 'No displayable models' + title: 'Свой список моделей /v1/models', + hint: 'Меняет только ответ /v1/models. Белый список вызовов моделей и маршрутизация аккаунтов не меняются.', + loading: 'Загрузка списка моделей...', + empty: 'Нет моделей для отображения' }, claudeCode: { - title: 'Claude Code Client Restriction', - tooltip: 'When enabled, this group only allows official Claude Code clients. Non-Claude Code requests will be rejected or fallback to the specified group.', - enabled: 'Claude Code Only', - disabled: 'Allow All Clients', - fallbackGroup: 'Fallback Group', - fallbackHint: 'Non-Claude Code requests will use this group. Leave empty to reject directly.', - noFallback: 'No Fallback (Reject)' + title: 'Ограничение клиента Claude Code', + tooltip: 'Если включено, группа разрешает только официальные клиенты Claude Code. Запросы не от Claude Code будут отклонены или отправлены в резервную группу.', + enabled: 'Только Claude Code', + disabled: 'Разрешить все клиенты', + fallbackGroup: 'Резервная группа', + fallbackHint: 'Запросы не от Claude Code будут использовать эту группу. Оставьте пустым для прямого отклонения.', + noFallback: 'Без резервной группы (отклонять)' }, openaiMessages: { - title: 'OpenAI Messages Dispatch', - allowDispatch: 'Allow /v1/messages dispatch', - allowDispatchHint: 'When enabled, API keys in this OpenAI group can dispatch requests through /v1/messages endpoint', - familyMappingTitle: 'Family Default Mapping', - familyMappingHint: 'Requests that match the Opus, Sonnet, or Haiku families will prefer the target model configured here.', - opusModel: 'Opus Target Model', + title: 'Диспетчеризация OpenAI Messages', + allowDispatch: 'Разрешить диспетчеризацию /v1/messages', + allowDispatchHint: 'Если включено, API-ключи в этой группе OpenAI могут отправлять запросы через endpoint /v1/messages', + familyMappingTitle: 'Маппинг семейств по умолчанию', + familyMappingHint: 'Запросы семейств Opus, Sonnet или Haiku будут предпочитать целевую модель, заданную здесь.', + opusModel: 'Целевая модель Opus', opusModelPlaceholder: 'e.g., gpt-5.4', - sonnetModel: 'Sonnet Target Model', + sonnetModel: 'Целевая модель Sonnet', sonnetModelPlaceholder: 'e.g., gpt-5.3-codex', - haikuModel: 'Haiku Target Model', + haikuModel: 'Целевая модель Haiku', haikuModelPlaceholder: 'e.g., gpt-5.4-mini', - exactMappingTitle: 'Exact Model Overrides', - exactMappingHint: 'Exact Claude model overrides take priority over the family defaults and can route a specific Claude model to a different target model.', - noExactMappings: 'No exact model overrides yet', - addExactMapping: 'Add Exact Mapping', + exactMappingTitle: 'Точные переопределения моделей', + exactMappingHint: 'Точные переопределения моделей Claude имеют приоритет над семействами и могут направлять конкретную модель Claude на другую целевую модель.', + noExactMappings: 'Точных переопределений моделей пока нет', + addExactMapping: 'Добавить точный маппинг', claudeModel: 'Claude Model', claudeModelPlaceholder: 'e.g., claude-sonnet-4-5-20250929', - targetModel: 'Target Model', + targetModel: 'Целевая модель', targetModelPlaceholder: 'e.g., gpt-5.4', - removeExactMapping: 'Remove Exact Mapping' + removeExactMapping: 'Удалить точный маппинг' }, invalidRequestFallback: { - title: 'Invalid Request Fallback Group', - hint: 'Triggered only when upstream explicitly returns prompt too long. Leave empty to disable fallback.', - noFallback: 'No Fallback' + title: 'Резервная группа для некорректного запроса', + hint: 'Срабатывает только когда апстрим явно возвращает слишком длинный prompt. Оставьте пустым, чтобы отключить резерв.', + noFallback: 'Без резервной группы' }, copyAccounts: { - title: 'Copy Accounts from Groups', - tooltip: 'Select one or more groups of the same platform. After creation, all accounts from these groups will be automatically bound to the new group (deduplicated).', - tooltipEdit: 'Select one or more groups of the same platform. After saving, current group accounts will be replaced with accounts from these groups (deduplicated).', - selectPlaceholder: 'Select groups to copy accounts from...', - hint: 'Multiple groups can be selected, accounts will be deduplicated', - hintEdit: '⚠️ Warning: This will replace all existing account bindings' + title: 'Скопировать аккаунты из групп', + tooltip: 'Выберите одну или несколько групп той же платформы. После создания все аккаунты из этих групп будут автоматически привязаны к новой группе (без дублей).', + tooltipEdit: 'Выберите одну или несколько групп той же платформы. После сохранения аккаунты текущей группы будут заменены аккаунтами из этих групп (без дублей).', + selectPlaceholder: 'Выберите группы, из которых копировать аккаунты...', + hint: 'Можно выбрать несколько групп, аккаунты будут дедуплицированы', + hintEdit: '⚠️ Внимание: это заменит все текущие привязки аккаунтов' }, modelRouting: { - title: 'Model Routing', - tooltip: 'Configure specific model requests to be routed to designated accounts. Supports wildcard matching, e.g., claude-opus-* matches all opus models.', + title: 'Маршрутизация моделей', + tooltip: 'Настройте маршрутизацию запросов конкретных моделей на выбранные аккаунты. Поддерживаются wildcard, например claude-opus-* соответствует всем моделям opus.', enabled: 'Включено', disabled: 'Отключено', - disabledHint: 'Routing rules will only take effect when enabled', - addRule: 'Add Routing Rule', - modelPattern: 'Model Pattern', + disabledHint: 'Правила маршрутизации действуют только при включении', + addRule: 'Добавить правило маршрутизации', + modelPattern: 'Шаблон модели', modelPatternPlaceholder: 'claude-opus-*', - modelPatternHint: 'Supports * wildcard, e.g., claude-opus-* matches all opus models', - accounts: 'Priority Accounts', - selectAccounts: 'Select accounts', - noAccounts: 'No accounts in this group', - loadingAccounts: 'Loading accounts...', - removeRule: 'Remove Rule', - noRules: 'No routing rules', - noRulesHint: 'Add routing rules to route specific model requests to designated accounts', - searchAccountPlaceholder: 'Search accounts...', - accountsHint: 'Select accounts to prioritize for this model pattern' + modelPatternHint: 'Поддерживает wildcard *, например claude-opus-* соответствует всем моделям opus', + accounts: 'Приоритетные аккаунты', + selectAccounts: 'Выберите аккаунты', + noAccounts: 'В этой группе нет аккаунтов', + loadingAccounts: 'Загрузка аккаунтов...', + removeRule: 'Удалить правило', + noRules: 'Нет правил маршрутизации', + noRulesHint: 'Добавьте правила маршрутизации для отправки запросов конкретных моделей на выбранные аккаунты', + searchAccountPlaceholder: 'Поиск аккаунтов...', + accountsHint: 'Выберите аккаунты с приоритетом для этого шаблона модели' }, mcpXml: { - title: 'MCP XML Protocol Injection', - tooltip: 'When enabled, if the request contains MCP tools, an XML format call protocol prompt will be injected into the system prompt. Disable this to avoid interference with certain clients.', + title: 'Вставка протокола MCP XML', + tooltip: 'Если включено и запрос содержит MCP-инструменты, в системный prompt будет вставлен prompt протокола вызова в формате XML. Отключите, чтобы избежать конфликтов с некоторыми клиентами.', enabled: 'Включено', disabled: 'Отключено' }, claudeMaxSimulation: { - title: 'Claude Max Usage Simulation', + title: 'Симуляция расхода Claude Max', tooltip: - 'When enabled, for Claude models without upstream cache-write usage, the system deterministically maps tokens to a small input plus 1h cache creation while keeping total tokens unchanged.', - enabled: 'Enabled (simulate 1h cache)', + 'Если включено, для моделей Claude без расхода cache-write от апстрима система детерминированно распределяет токены на небольшой input и создание кэша 1h, сохраняя общий расход токенов.', + enabled: 'Включено (симуляция кэша 1h)', disabled: 'Отключено', - hint: 'Only token categories in usage billing logs are adjusted. No per-request mapping state is persisted.' + hint: 'Меняются только категории токенов в логах списаний. Состояние маппинга по запросам не сохраняется.' }, supportedScopes: { - title: 'Supported Model Families', - tooltip: 'Select the model families this group supports. Unchecked families will not be routed to this group.', + title: 'Поддерживаемые семейства моделей', + tooltip: 'Выберите семейства моделей, поддерживаемые группой. Невыбранные семейства не будут маршрутизироваться в эту группу.', claude: 'Claude', geminiText: 'Gemini Text', geminiImage: 'Gemini Image', - hint: 'Select at least one model family' + hint: 'Выберите хотя бы одно семейство моделей' } }, @@ -2315,344 +2315,344 @@ export default { // Channel Management channels: { - title: 'Channel Management', - description: 'Manage channels and custom model pricing', + title: 'Управление каналами', + description: 'Управляйте каналами и своими ценами моделей', searchChannels: 'Поиск каналов...', - createChannel: 'Create Channel', - editChannel: 'Edit Channel', - deleteChannel: 'Delete Channel', + createChannel: 'Создать канал', + editChannel: 'Изменить канал', + deleteChannel: 'Удалить канал', statusActive: 'Активен', statusDisabled: 'Отключено', allStatus: 'Все статусы', - groupsUnit: 'groups', - pricingUnit: 'pricing rules', - noChannelsYet: 'No Channels Yet', - createFirstChannel: 'Create your first channel to manage model pricing', - loadError: 'Failed to load channels', - createSuccess: 'Channel created', - updateSuccess: 'Channel updated', - deleteSuccess: 'Channel deleted', - createError: 'Failed to create channel', - updateError: 'Failed to update channel', - deleteError: 'Failed to delete channel', - nameRequired: 'Please enter a channel name', - duplicateModels: 'Model "{0}" appears in multiple pricing entries', - modelConflict: "Model patterns '{model1}' and '{model2}' conflict: overlapping match range", - mappingConflict: "Mapping source patterns '{model1}' and '{model2}' conflict: overlapping match range", - deleteConfirm: 'Are you sure you want to delete channel "{name}"? This cannot be undone.', + groupsUnit: 'групп', + pricingUnit: 'правил цен', + noChannelsYet: 'Каналов пока нет', + createFirstChannel: 'Создайте первый канал для управления ценами моделей', + loadError: 'Не удалось загрузить каналы', + createSuccess: 'Канал создан', + updateSuccess: 'Канал обновлён', + deleteSuccess: 'Канал удалён', + createError: 'Не удалось создать канал', + updateError: 'Не удалось обновить канал', + deleteError: 'Не удалось удалить канал', + nameRequired: 'Введите имя канала', + duplicateModels: 'Модель "{0}" указана в нескольких ценовых записях', + modelConflict: "Шаблоны моделей '{model1}' и '{model2}' конфликтуют: диапазоны совпадений пересекаются", + mappingConflict: "Исходные шаблоны маппинга '{model1}' и '{model2}' конфликтуют: диапазоны совпадений пересекаются", + deleteConfirm: 'Удалить канал "{name}"? Это действие нельзя отменить.', columns: { name: 'Имя', description: 'Описание', status: 'Статус', groups: 'Группы', - pricing: 'Оплата', + pricing: 'Цены', createdAt: 'Создан', actions: 'Действия' }, billingMode: { - token: 'Token', + token: 'Токены', perRequest: 'За запрос', - image: 'Image (Per Request)' + image: 'Изображение (за запрос)' }, form: { name: 'Имя', - namePlaceholder: 'Enter channel name', + namePlaceholder: 'Введите имя канала', description: 'Описание', - descriptionPlaceholder: 'Optional description', + descriptionPlaceholder: 'Описание (необязательно)', status: 'Статус', - groups: 'Associated Groups', + groups: 'Связанные группы', noGroupsAvailable: 'Нет доступных групп', - inOtherChannel: 'In "{name}"', - modelPricing: 'Model Pricing', + inOtherChannel: 'В "{name}"', + modelPricing: 'Цены моделей', models: 'Модели', - modelsPlaceholder: 'Type full model name and press Enter', - modelInputHint: 'Press Enter to add, supports paste for batch import.', + modelsPlaceholder: 'Введите полное имя модели и нажмите Enter', + modelInputHint: 'Нажмите Enter для добавления; поддерживается вставка списком.', billingMode: 'Режим биллинга', - defaultPrices: 'Default prices (fallback when no interval matches)', + defaultPrices: 'Цены по умолчанию (резерв, если интервал не подошёл)', inputPrice: 'Вход', outputPrice: 'Выход', cacheWritePrice: 'Запись кэша', cacheReadPrice: 'Чтение кэша', imageTokenPrice: 'Выход изображения', - imageOutputPrice: 'Image Output Price', + imageOutputPrice: 'Цена выходного изображения', pricePlaceholder: 'По умолчанию', - intervals: 'Context Intervals (optional)', - addInterval: 'Add Interval', - requestTiers: 'Request Tiers', - imageTiers: 'Image Tiers (Per Request)', - addTier: 'Add Tier', - noTiersYet: 'No tiers yet. Click add to configure per-request pricing.', - noPricingRules: 'No pricing rules yet. Click "Добавить" to create one.', - perRequestPrice: 'Price per Request', - perRequestPriceRequired: 'Per-request price or billing tiers required for per-request/image billing mode', - tierLabel: 'Tier', - resolution: 'Resolution', - modelMapping: 'Model Mapping', - modelMappingHint: 'Map request model names to actual model names. Runs before account-level mapping.', - noMappingRules: 'No mapping rules. Click "Добавить" to create one.', - mappingSource: 'Source model', - mappingTarget: 'Target model', - billingModelSource: 'Billing Model', - billingModelSourceChannelMapped: 'Bill by channel-mapped model', - billingModelSourceRequested: 'Bill by requested model', - billingModelSourceUpstream: 'Bill by final upstream model', - billingModelSourceHint: 'Controls which model name is used for pricing lookup', - selectedCount: '{count} selected', + intervals: 'Интервалы контекста (необязательно)', + addInterval: 'Добавить интервал', + requestTiers: 'Тарифы запросов', + imageTiers: 'Тарифы изображений (за запрос)', + addTier: 'Добавить тариф', + noTiersYet: 'Тарифов пока нет. Нажмите добавить, чтобы настроить цену за запрос.', + noPricingRules: 'Правил цен пока нет. Нажмите "Добавить", чтобы создать первое.', + perRequestPrice: 'Цена за запрос', + perRequestPriceRequired: 'Для режима за запрос/изображение нужна цена за запрос или тарифные уровни', + tierLabel: 'Тариф', + resolution: 'Разрешение', + modelMapping: 'Маппинг моделей', + modelMappingHint: 'Сопоставляет имена моделей в запросах с фактическими именами. Выполняется до маппинга аккаунта.', + noMappingRules: 'Правил маппинга нет. Нажмите "Добавить", чтобы создать первое.', + mappingSource: 'Исходная модель', + mappingTarget: 'Целевая модель', + billingModelSource: 'Модель тарификации', + billingModelSourceChannelMapped: 'Считать по модели после маппинга канала', + billingModelSourceRequested: 'Считать по запрошенной модели', + billingModelSourceUpstream: 'Считать по итоговой апстрим-модели', + billingModelSourceHint: 'Определяет, какое имя модели используется для поиска цены', + selectedCount: 'Выбрано: {count}', searchGroups: 'Поиск групп...', - noGroupsMatch: 'No groups match your search', - restrictModels: 'Restrict Models', - restrictModelsHint: 'When enabled, only models in the pricing list are allowed. Others will be rejected.', - defaultPerRequestPrice: 'Default per-request price (fallback when no tier matches)', - defaultImagePrice: 'Default image price (fallback when no tier matches)', - platformConfig: 'Platform Configuration', - webSearchEmulation: 'Web Search Emulation', - webSearchEmulationHint: '⚠️ When enabled, all accounts in this channel\'s Anthropic groups will intercept web_search requests. Use with caution.', - webSearchEmulationGlobalDisabled: 'Please enable the global switch first in Settings → Gateway → Web Search Emulation', - codexImageGenerationBridge: 'Codex Image Generation Bridge', - codexImageGenerationBridgeHint: 'When enabled, Codex /responses text requests in OpenAI groups may be automatically given the image_generation tool. Keep off unless the routed accounts support image generation.', - bedrockCCCompat: 'Bedrock CC Compatibility', - bedrockCCCompatHint: '⚠️ When enabled, requests to Bedrock accounts in this channel will be transformed for Claude Code compatibility (thinking type conversion, tool_use ID sanitization).', - basicSettings: 'Basic Settings', - addPlatform: 'Add Platform', - noPlatforms: 'Click "Add Platform" to start configuring the channel', - mappingCount: 'mappings', - pricingEntry: 'Pricing Entry', - noModels: 'No models added', - applyPricingToAccountStats: 'Apply Pricing to Account Stats', - applyPricingToAccountStatsDesc: 'When enabled, requests not matched by custom rules will use standard model pricing for account stats calculation', - accountStatsPricingRules: 'Custom Account Stats Pricing Rules', - addRule: 'Add Rule', - noRulesConfigured: 'No custom rules configured. Channel model pricing above will be used.', - ruleName: 'Rule name (optional)', + noGroupsMatch: 'Подходящих групп не найдено', + restrictModels: 'Ограничить модели', + restrictModelsHint: 'Если включено, разрешены только модели из списка цен. Остальные будут отклонены.', + defaultPerRequestPrice: 'Цена за запрос по умолчанию (резерв, если тариф не подошёл)', + defaultImagePrice: 'Цена изображения по умолчанию (резерв, если тариф не подошёл)', + platformConfig: 'Настройки платформы', + webSearchEmulation: 'Эмуляция Web Search', + webSearchEmulationHint: '⚠️ Если включено, все аккаунты в Anthropic-группах этого канала будут перехватывать запросы web_search. Используйте осторожно.', + webSearchEmulationGlobalDisabled: 'Сначала включите глобальный переключатель в Настройки → Gateway → Web Search Emulation', + codexImageGenerationBridge: 'Мост генерации изображений Codex', + codexImageGenerationBridgeHint: 'Если включено, текстовым запросам Codex /responses в группах OpenAI может автоматически добавляться инструмент image_generation. Не включайте, если маршрутизируемые аккаунты не поддерживают генерацию изображений.', + bedrockCCCompat: 'Совместимость Bedrock CC', + bedrockCCCompatHint: '⚠️ Если включено, запросы к аккаунтам Bedrock в этом канале будут преобразованы для совместимости с Claude Code (конвертация thinking type, очистка tool_use ID).', + basicSettings: 'Основные настройки', + addPlatform: 'Добавить платформу', + noPlatforms: 'Нажмите "Добавить платформу", чтобы начать настройку канала', + mappingCount: 'маппингов', + pricingEntry: 'Ценовая запись', + noModels: 'Модели не добавлены', + applyPricingToAccountStats: 'Применять цены к статистике аккаунтов', + applyPricingToAccountStatsDesc: 'Если включено, запросы без совпадений в пользовательских правилах будут использовать стандартные цены моделей для расчёта статистики аккаунтов', + accountStatsPricingRules: 'Свои правила цен для статистики аккаунтов', + addRule: 'Добавить правило', + noRulesConfigured: 'Свои правила не настроены. Будут использоваться цены моделей канала выше.', + ruleName: 'Имя правила (необязательно)', ruleGroups: 'Группы', ruleAccounts: 'Аккаунты', - searchAccountPlaceholder: 'Search accounts...', - ruleAccountsHint: 'Leave empty to match all accounts', - ruleModelPricing: 'Model Pricing', - noGroupsInChannel: 'No groups selected in platform tabs above', - unnamed: 'Unnamed', - syncLatestModels: 'Sync Latest Models', - syncingModels: 'Syncing...', - syncModelsSuccess: 'Synced {count} new model(s)', - syncModelsAlreadyUpToDate: 'Models already up to date', - syncModelsError: 'Failed to sync models' + searchAccountPlaceholder: 'Поиск аккаунтов...', + ruleAccountsHint: 'Оставьте пустым, чтобы совпадали все аккаунты', + ruleModelPricing: 'Цены моделей', + noGroupsInChannel: 'Вкладки платформ выше не содержат выбранных групп', + unnamed: 'Без имени', + syncLatestModels: 'Синхронизировать новые модели', + syncingModels: 'Синхронизация...', + syncModelsSuccess: 'Синхронизировано новых моделей: {count}', + syncModelsAlreadyUpToDate: 'Модели уже актуальны', + syncModelsError: 'Не удалось синхронизировать модели' } }, riskControl: { title: 'Риск-контроль', - description: 'Configure content moderation and review audit records', - loadFailed: 'Failed to load risk control', - saveFailed: 'Failed to save content moderation config', - logsFailed: 'Failed to load audit records', - saved: 'Content moderation config saved', + description: 'Настройте модерацию контента и просматривайте записи проверки', + loadFailed: 'Не удалось загрузить контроль рисков', + saveFailed: 'Не удалось сохранить настройки модерации контента', + logsFailed: 'Не удалось загрузить записи проверки', + saved: 'Настройки модерации контента сохранены', refresh: 'Обновить', - config: 'Content Moderation Config', - configHint: 'Use OpenAI Moderations to score request content and handle threshold hits by mode.', - openSettings: 'Moderation Settings', - settingsTitle: 'Content Moderation Settings', - refreshStatus: 'Refresh Status', - records: 'Audit Records', - recordsHint: 'Shows hits, blocks, errors, and sampled records.', - saveConfig: 'Save Moderation Config', - statusFailed: 'Failed to load runtime status', - enabled: 'Enable Content Moderation', - enabledHint: 'When off, gateway requests are not moderated even if the menu is enabled.', - mode: 'Global Mode', - modePreBlock: 'Pre-Block', - modePreBlockDesc: 'Synchronously reviews the latest user input before every request and rejects hits immediately.', - modeObserve: 'Observe Only', - modeObserveDesc: 'Requests pass through while the latest user input is queued for async review; hits are recorded, notified, and counted.', - modeOff: 'Off', - modeOffDesc: 'Content moderation is disabled and no audit records are written.', + config: 'Настройки модерации контента', + configHint: 'Используйте OpenAI Moderations для оценки контента запроса и обработки срабатываний порогов по режиму.', + openSettings: 'Настройки модерации', + settingsTitle: 'Настройки модерации контента', + refreshStatus: 'Обновить статус', + records: 'Записи проверки', + recordsHint: 'Показывает срабатывания, блокировки, ошибки и выборочные записи.', + saveConfig: 'Сохранить настройки модерации', + statusFailed: 'Не удалось загрузить статус выполнения', + enabled: 'Включить модерацию контента', + enabledHint: 'Если выключено, запросы gateway не модерируются, даже если меню включено.', + mode: 'Глобальный режим', + modePreBlock: 'Предблокировка', + modePreBlockDesc: 'Синхронно проверяет последний ввод пользователя перед каждым запросом и сразу отклоняет срабатывания.', + modeObserve: 'Только наблюдение', + modeObserveDesc: 'Запросы проходят, а последний ввод пользователя ставится в очередь на асинхронную проверку; срабатывания записываются, уведомляются и считаются.', + modeOff: 'Выключено', + modeOffDesc: 'Модерация контента отключена, записи проверки не создаются.', baseUrl: 'OpenAI Base URL', model: 'Модель', - apiKey: 'OpenAI API Key', - apiKeys: 'OpenAI API Keys', - apiKeyCount: '{count} keys', - apiKeyPlaceholder: 'Enter API Key', - apiKeysPlaceholder: 'Add API Keys, one per line. They will be appended on save.', - apiKeysPlaceholderReplace: 'Replace API Keys, one per line. Stored keys will be replaced on save.', - apiKeysPlaceholderKeep: 'Add API Keys, one per line. They will be appended on save.', - apiKeysHint: '{count} keys are currently stored. This input only adds keys; save appends and de-duplicates them.', - apiKeysWriteMode: 'Write mode', + apiKey: 'OpenAI API-ключ', + apiKeys: 'OpenAI API-ключи', + apiKeyCount: 'Ключей: {count}', + apiKeyPlaceholder: 'Введите API-ключ', + apiKeysPlaceholder: 'Добавьте API-ключи, по одному на строку. При сохранении они будут добавлены.', + apiKeysPlaceholderReplace: 'Замените API-ключи, по одному на строку. При сохранении сохранённые ключи будут заменены.', + apiKeysPlaceholderKeep: 'Добавьте API-ключи, по одному на строку. При сохранении они будут добавлены.', + apiKeysHint: 'Сейчас сохранено ключей: {count}. Это поле только добавляет ключи; сохранение добавит их и удалит дубли.', + apiKeysWriteMode: 'Режим записи', apiKeysModeAppend: 'Добавить', - apiKeysModeReplace: 'Replace', - apiKeysModeAppendHint: 'Default: save appends input keys and keeps stored keys.', - apiKeysModeReplaceHint: 'Replace mode: save replaces all stored keys with input keys.', - apiKeysReplaceWarning: 'Replace mode', - apiKeysReplaceNoInput: 'Replace mode requires at least 1 API Key', - apiKeyPlaceholderKeep: 'Leave empty to keep current key', - apiKeyWillClear: 'Configured key will be cleared on save', - apiKeyConfigured: 'Configured', + apiKeysModeReplace: 'Заменить', + apiKeysModeAppendHint: 'По умолчанию: сохранение добавляет введённые ключи и оставляет сохранённые.', + apiKeysModeReplaceHint: 'Режим замены: сохранение заменяет все сохранённые ключи введёнными.', + apiKeysReplaceWarning: 'Режим замены', + apiKeysReplaceNoInput: 'Для режима замены нужен хотя бы 1 API-ключ', + apiKeyPlaceholderKeep: 'Оставьте пустым, чтобы сохранить текущий ключ', + apiKeyWillClear: 'Настроенный ключ будет очищен при сохранении', + apiKeyConfigured: 'Настроено', apiKeyTemporary: 'Ожидает', - apiKeyPendingDelete: 'Pending delete', - apiKeyPendingDeleteCount: '{count} keys pending deletion', - deleteApiKey: 'Delete this key', - undoDeleteApiKey: 'Undo delete', - inputApiKeyCount: '{count} keys in input', - storedApiKeyCount: '{count} stored keys', - testInputApiKeys: 'Test input keys', - testStoredApiKeys: 'Test stored keys', - testContentWithStoredApiKey: 'Test content with stored key', - testingApiKeys: 'Testing', - apiKeyTestNoInput: 'Enter OpenAI API Keys to test first', - apiKeyTestDone: 'Key test completed for {count} keys', - apiKeyTestFailed: 'Failed to test OpenAI API Keys', - apiKeyHealth: 'Key Availability', - apiKeyFreezeRule: '400 does not freeze; 401/403 freeze for 10 minutes; 429/529 freeze for 1 minute; other HTTP errors freeze for 10 seconds.', - apiKeyRows: '{count} keys', - apiKeyRowsCollapsed: '{count} keys hidden', - apiKeyRowsExpanded: 'Showing all {count} keys', + apiKeyPendingDelete: 'Ожидает удаления', + apiKeyPendingDeleteCount: 'Ключей ожидает удаления: {count}', + deleteApiKey: 'Удалить этот ключ', + undoDeleteApiKey: 'Отменить удаление', + inputApiKeyCount: 'Ключей во вводе: {count}', + storedApiKeyCount: 'Сохранённых ключей: {count}', + testInputApiKeys: 'Проверить введённые ключи', + testStoredApiKeys: 'Проверить сохранённые ключи', + testContentWithStoredApiKey: 'Проверить контент сохранённым ключом', + testingApiKeys: 'Проверка', + apiKeyTestNoInput: 'Сначала введите OpenAI API-ключи для проверки', + apiKeyTestDone: 'Проверка ключей завершена, ключей: {count}', + apiKeyTestFailed: 'Не удалось проверить OpenAI API-ключи', + apiKeyHealth: 'Доступность ключей', + apiKeyFreezeRule: '400 не замораживает; 401/403 замораживают на 10 минут; 429/529 — на 1 минуту; другие HTTP-ошибки — на 10 секунд.', + apiKeyRows: 'Ключей: {count}', + apiKeyRowsCollapsed: 'Скрыто ключей: {count}', + apiKeyRowsExpanded: 'Показаны все ключи: {count}', expandApiKeyRows: 'Развернуть', collapseApiKeyRows: 'Свернуть', - apiKeyHealthEmpty: 'No key status yet', - apiKeyHealthEmptyHint: 'Save keys or test input keys to see availability.', + apiKeyHealthEmpty: 'Статуса ключей пока нет', + apiKeyHealthEmptyHint: 'Сохраните ключи или проверьте введённые, чтобы увидеть доступность.', apiKeyStatusOk: 'Доступно', apiKeyStatusError: 'Ошибка', apiKeyStatusFrozen: 'Заморожено', - apiKeyStatusUnknown: 'Untested', - apiKeyFailureCount: '{count} failures', + apiKeyStatusUnknown: 'Не проверено', + apiKeyFailureCount: 'Ошибок: {count}', apiKeyLatency: '{ms} ms', apiKeyHTTPStatus: 'HTTP {status}', - apiKeyFrozenUntil: 'Frozen until {time}', - apiKeyLastChecked: 'Checked at {time}', - apiKeyNotTested: 'Not tested', - auditTestInput: 'Audit Test Input', - auditTestInputHint: 'Enter a prompt and upload or paste images; images are sent as base64 and are not stored.', - auditTestPromptPlaceholder: 'Enter a user prompt to test; leave empty to only test key availability.', - auditTestImages: 'Test Images', - auditTestImagesHint: 'Upload, drag, or paste images. Up to 1 image, 8MB each.', - addAuditTestImage: 'Add image', - clearAuditTest: 'Clear test', - auditTestImageLimit: 'You can add up to {count} test images', - auditTestImageTooLarge: 'Each test image must be 8MB or smaller', - auditTestImageReadFailed: 'Failed to read test image', - auditTestResult: 'Audit Test Result', - auditTestHighest: 'Top category {category}, score {score}', - auditTestComposite: 'Composite score', - auditTestFlagged: 'Threshold hit', - auditTestPassed: 'Pass', - notConfigured: 'Not configured', - clearApiKey: 'Clear stored key', - keepApiKey: 'Keep stored key', + apiKeyFrozenUntil: 'Заморожен до {time}', + apiKeyLastChecked: 'Проверено в {time}', + apiKeyNotTested: 'Не проверялся', + auditTestInput: 'Тестовый ввод проверки', + auditTestInputHint: 'Введите prompt и загрузите или вставьте изображения; изображения отправляются как base64 и не сохраняются.', + auditTestPromptPlaceholder: 'Введите пользовательский prompt для теста; оставьте пустым, чтобы проверить только доступность ключей.', + auditTestImages: 'Тестовые изображения', + auditTestImagesHint: 'Загрузите, перетащите или вставьте изображения. До 1 изображения, 8MB каждое.', + addAuditTestImage: 'Добавить изображение', + clearAuditTest: 'Очистить тест', + auditTestImageLimit: 'Можно добавить тестовых изображений: {count}', + auditTestImageTooLarge: 'Каждое тестовое изображение должно быть не больше 8MB', + auditTestImageReadFailed: 'Не удалось прочитать тестовое изображение', + auditTestResult: 'Результат тестовой проверки', + auditTestHighest: 'Главная категория {category}, оценка {score}', + auditTestComposite: 'Итоговая оценка', + auditTestFlagged: 'Порог сработал', + auditTestPassed: 'Пройдено', + notConfigured: 'Не настроено', + clearApiKey: 'Очистить сохранённый ключ', + keepApiKey: 'Оставить сохранённый ключ', timeoutMs: 'HTTP Timeout (ms)', - retryCount: 'Retry Count', - sampleRate: 'Sample Rate', - recordNonHits: 'Record Non-Hits', - recordNonHitsHint: 'When enabled, sampled non-hit request summaries are redacted before storage.', - preHashCheck: 'Enable Pre-Hash Check', - preHashCheckHint: 'Hashes from async hits are blocked before moderation; this does not send email or increment ban counters.', - flaggedHashCount: 'Current hash collection size: {count}', - flaggedHashHint: 'Hashes are stored permanently in Redis; paste a full 64-character hash to remove a false block, or clear all stored hashes.', - flaggedHashPlaceholder: 'Paste full 64-character input hash', - deleteFlaggedHash: 'Delete hash', - clearFlaggedHashes: 'Clear all', - clearFlaggedHashesConfirm: 'Clear all risk input hashes? This does not delete audit records, but removes all historical hash blocks.', - flaggedHashDeleted: 'Risk hash deleted', - flaggedHashNotFound: 'Risk hash not found', - flaggedHashDeleteFailed: 'Failed to delete risk hash', - flaggedHashesCleared: 'Cleared {count} risk hashes', - flaggedHashesClearFailed: 'Failed to clear risk hashes', - workerCount: 'Worker Count', - queueSize: 'Async Queue Size', - blockStatus: 'Block HTTP Status', - blockMessage: 'Custom Block Message', - emailOnHit: 'Email on Hit', - emailOnHitHint: 'When enabled, send a risk-control email on every hit; auto-ban notices are always sent.', - autoBan: 'Auto Ban User', - autoBanHint: 'Disable the user, invalidate auth cache, and send a ban notice after the hit threshold is reached.', - banThreshold: 'Ban Threshold', - violationWindowHours: 'Count Window (hours)', - hitRetentionDays: 'Hit Record Retention (days)', - nonHitRetentionDays: 'Non-Hit Record Retention (days, max 3)', + retryCount: 'Число повторов', + sampleRate: 'Доля выборки', + recordNonHits: 'Записывать несрабатывания', + recordNonHitsHint: 'Если включено, выборочные сводки запросов без срабатываний редактируются перед сохранением.', + preHashCheck: 'Включить предварительную проверку хэша', + preHashCheckHint: 'Хэши из асинхронных срабатываний блокируются до модерации; email не отправляется, счётчики банов не увеличиваются.', + flaggedHashCount: 'Текущий размер коллекции хэшей: {count}', + flaggedHashHint: 'Хэши постоянно хранятся в Redis; вставьте полный 64-символьный хэш, чтобы убрать ложную блокировку, или очистите все сохранённые хэши.', + flaggedHashPlaceholder: 'Вставьте полный 64-символьный хэш ввода', + deleteFlaggedHash: 'Удалить хэш', + clearFlaggedHashes: 'Очистить всё', + clearFlaggedHashesConfirm: 'Очистить все хэши риск-ввода? Записи проверки не удаляются, но все исторические блокировки по хэшам будут сняты.', + flaggedHashDeleted: 'Риск-хэш удалён', + flaggedHashNotFound: 'Риск-хэш не найден', + flaggedHashDeleteFailed: 'Не удалось удалить риск-хэш', + flaggedHashesCleared: 'Очищено риск-хэшей: {count}', + flaggedHashesClearFailed: 'Не удалось очистить риск-хэши', + workerCount: 'Количество worker-ов', + queueSize: 'Размер async-очереди', + blockStatus: 'HTTP-статус блокировки', + blockMessage: 'Пользовательское сообщение блокировки', + emailOnHit: 'Email при срабатывании', + emailOnHitHint: 'Когда включено, при каждом срабатывании отправляется email risk-control; уведомления об авто-бане отправляются всегда.', + autoBan: 'Автоматически банить пользователя', + autoBanHint: 'Отключает пользователя, сбрасывает auth cache и отправляет уведомление о бане после достижения порога срабатываний.', + banThreshold: 'Порог бана', + violationWindowHours: 'Окно подсчёта (часы)', + hitRetentionDays: 'Хранение записей срабатываний (дни)', + nonHitRetentionDays: 'Хранение записей без срабатываний (дни, макс. 3)', violationCount: '{count} hits', - emailSent: 'Email sent', - emailNotSent: 'No email', - autoBanned: 'Banned', - unbanUser: 'Unban', - unbanSuccess: 'User has been unbanned', - unbanFailed: 'Failed to unban user', - inputDetailTitle: 'Input Summary Detail', - inputDetailContent: 'Full Content', + emailSent: 'Email отправлен', + emailNotSent: 'Без email', + autoBanned: 'Забанен', + unbanUser: 'Разбанить', + unbanSuccess: 'Пользователь разбанен', + unbanFailed: 'Не удалось разбанить пользователя', + inputDetailTitle: 'Детали сводки ввода', + inputDetailContent: 'Полный контент', queueDelay: 'Queued {ms} ms', allGroups: 'Все группы', - allGroupsHint: 'Auditing all groups', - selectedGroupsHint: 'Auditing selected groups', - groupScope: 'Audit Groups', - groupScopeHint: 'Switch on for all groups, or turn off to choose specific groups.', - selectedGroups: 'Selected Groups', - searchGroups: 'Search group name or platform', + allGroupsHint: 'Проверяются все группы', + selectedGroupsHint: 'Проверяются выбранные группы', + groupScope: 'Проверяемые группы', + groupScopeHint: 'Включите для всех групп или отключите, чтобы выбрать конкретные группы.', + selectedGroups: 'Выбранные группы', + searchGroups: 'Поиск по названию группы или платформе', noGroups: 'Нет доступных групп', - modelFilter: 'Model scope', - modelFilterHint: 'Moderate by the client-requested model name; channel model mappings do not change this match.', - modelFilterAll: 'All models', - modelFilterAllDesc: 'All model requests go through content moderation.', - modelFilterInclude: 'Only selected', - modelFilterIncludeDesc: 'Only listed models go through content moderation.', - modelFilterExclude: 'Exclude selected', - modelFilterExcludeDesc: 'Listed models skip content moderation; other models are moderated.', - modelFilterModels: 'Model list', - modelFilterModelCount: '{count} models configured', - modelFilterModelsRequired: 'This model scope requires at least 1 model', - modelFilterAllSummary: 'Applies to all models', - modelFilterIncludeSummary: 'Applies to {count} models', - modelFilterExcludeSummary: 'Excludes {count} models', - emptyLogs: 'No audit records', - workerStatus: 'Worker Runtime', - workerStatusHint: 'Queue and worker pool status for asynchronous observation tasks.', - workerPool: 'Worker Pool', - workerPoolMeta: '{active} processing, {idle} idle and ready, {total} total', - queueUsage: 'Queue Usage', - activeWorkers: 'Processing', - idleWorkers: 'Idle Ready', - workerActive: 'Processing an asynchronous audit task', - workerIdle: 'Started, idle and ready', - workerDisabled: 'Risk control or content audit is disabled', - processed: 'Processed', - droppedErrors: 'Dropped / Errors', - autoRefresh: 'Auto refresh every 15s', - lastCleanup: 'Last cleanup: {time}', - cleanupStats: 'Last cleanup deleted {hit} hits and {nonHit} non-hits', - riskSwitchOff: 'System switch off', - riskThresholds: 'Risk Thresholds', - riskThresholdsHint: 'Adjust hit thresholds by OpenAI Moderations category. Scores greater than or equal to the threshold count as hits.', - riskThresholdDefault: 'Default {value}', - riskThresholdReset: 'Restore defaults', - riskThresholdPercent: 'Threshold percentage', + modelFilter: 'Область моделей', + modelFilterHint: 'Модерация по имени модели, запрошенной клиентом; сопоставления моделей канала не меняют это совпадение.', + modelFilterAll: 'Все модели', + modelFilterAllDesc: 'Все запросы моделей проходят модерацию контента.', + modelFilterInclude: 'Только выбранные', + modelFilterIncludeDesc: 'Только указанные модели проходят модерацию контента.', + modelFilterExclude: 'Исключить выбранные', + modelFilterExcludeDesc: 'Указанные модели пропускают модерацию контента; остальные модели модерируются.', + modelFilterModels: 'Список моделей', + modelFilterModelCount: 'Настроено моделей: {count}', + modelFilterModelsRequired: 'Для этой области моделей нужна хотя бы 1 модель', + modelFilterAllSummary: 'Применяется ко всем моделям', + modelFilterIncludeSummary: 'Применяется к моделям: {count}', + modelFilterExcludeSummary: 'Исключает моделей: {count}', + emptyLogs: 'Записей проверки нет', + workerStatus: 'Runtime worker-ов', + workerStatusHint: 'Статус очереди и пула worker-ов для асинхронных задач наблюдения.', + workerPool: 'Пул worker-ов', + workerPoolMeta: 'Обрабатывается: {active}, простаивает и готово: {idle}, всего: {total}', + queueUsage: 'Использование очереди', + activeWorkers: 'Обработка', + idleWorkers: 'Готовы в простое', + workerActive: 'Обрабатывает асинхронную задачу проверки', + workerIdle: 'Запущен, простаивает и готов', + workerDisabled: 'Risk control или аудит контента отключён', + processed: 'Обработано', + droppedErrors: 'Сброшено / ошибки', + autoRefresh: 'Автообновление каждые 15s', + lastCleanup: 'Последняя очистка: {time}', + cleanupStats: 'Последняя очистка удалила срабатываний: {hit}, несрабатываний: {nonHit}', + riskSwitchOff: 'Системный переключатель выключен', + riskThresholds: 'Пороги риска', + riskThresholdsHint: 'Настройте пороги срабатывания по категориям OpenAI Moderations. Оценки больше или равные порогу считаются срабатываниями.', + riskThresholdDefault: 'По умолчанию {value}', + riskThresholdReset: 'Восстановить значения по умолчанию', + riskThresholdPercent: 'Процент порога', tabs: { - basic: 'Basic', - scope: 'Scope', + basic: 'Основное', + scope: 'Область', runtime: 'Runtime', - response: 'Hit Notice', - riskThresholds: 'Risk Thresholds', - keywords: 'Keyword Block', - retention: 'Retention', + response: 'Уведомление о срабатывании', + riskThresholds: 'Пороги риска', + keywords: 'Блокировка по ключевым словам', + retention: 'Хранение', }, - blockedKeywords: 'Blocked keywords', + blockedKeywords: 'Заблокированные ключевые слова', blockedKeywordsPlaceholder: 'One keyword per line, e.g.:\nbadword1\nbadword2', - blockedKeywordsDescription: 'Matching is case-insensitive. Whether the upstream moderation API is invoked after a hit depends on the strategy below.', - blockedKeywordsPreBlockHint: 'Keyword blocking only takes effect in "Pre-block" mode.', - blockedKeywordsModeWarning: 'Current mode is "{mode}". Keyword blocking will not run until you switch to "Pre-block" mode.', - blockedKeywordCount: '{count} keywords configured', - blockedKeywordsLimit: 'Up to {max} keywords, each no longer than 200 characters. Duplicates are removed automatically.', - keywordBlockingMode: 'Moderation strategy', + blockedKeywordsDescription: 'Сопоставление без учёта регистра. Будет ли upstream moderation API вызван после срабатывания, зависит от стратегии ниже.', + blockedKeywordsPreBlockHint: 'Блокировка по ключевым словам действует только в режиме "Pre-block".', + blockedKeywordsModeWarning: 'Текущий режим: "{mode}". Блокировка по ключевым словам не будет работать, пока вы не переключитесь в режим "Pre-block".', + blockedKeywordCount: 'Настроено ключевых слов: {count}', + blockedKeywordsLimit: 'До {max} ключевых слов, каждое не длиннее 200 символов. Дубликаты удаляются автоматически.', + keywordBlockingMode: 'Стратегия модерации', keywordModeKeywordAndApi: 'Keyword + API', - keywordModeKeywordAndApiDesc: 'Block on keyword hit; otherwise fall through to the upstream moderation API.', - keywordModeKeywordOnly: 'Keyword only', - keywordModeKeywordOnlyDesc: 'Decide using keywords only; misses are allowed without calling the API, saving upstream cost.', - keywordModeKeywordOnlyNotice: 'Keyword-only strategy: requests that do not match any keyword are allowed without calling the upstream moderation API.', - keywordModeApiOnly: 'API only', - keywordModeApiOnlyDesc: 'Use the upstream moderation API only; the keyword list configured here is not consulted.', - keywordModeApiOnlyNotice: 'API-only strategy: the keyword list is not consulted; all requests go through the upstream moderation API.', + keywordModeKeywordAndApiDesc: 'Блокировать при срабатывании ключевого слова; иначе передавать в upstream moderation API.', + keywordModeKeywordOnly: 'Только ключевые слова', + keywordModeKeywordOnlyDesc: 'Принимать решение только по ключевым словам; промахи разрешаются без вызова API, экономя upstream-расходы.', + keywordModeKeywordOnlyNotice: 'Стратегия только по ключевым словам: запросы без совпадения с ключевыми словами разрешаются без вызова upstream moderation API.', + keywordModeApiOnly: 'Только API', + keywordModeApiOnlyDesc: 'Использовать только upstream moderation API; настроенный здесь список ключевых слов не учитывается.', + keywordModeApiOnlyNotice: 'Стратегия только через API: список ключевых слов не учитывается; все запросы проходят через upstream moderation API.', overview: { status: 'Статус', enabled: 'Включено', disabled: 'Отключено', apiKey: 'API-ключ', - groupScope: 'Scope', - logs: 'Audit Records', - currentFilter: 'Current filter', + groupScope: 'Область', + logs: 'Записи проверки', + currentFilter: 'Текущий фильтр', }, filters: { search: 'Search user/key/summary', @@ -2690,27 +2690,27 @@ export default { // Channel Monitor channelMonitor: { title: 'Монитор каналов', - description: 'Monitor channel availability, latency and status', - searchPlaceholder: 'Search monitor name...', + description: 'Отслеживайте доступность, задержку и статус каналов', + searchPlaceholder: 'Поиск имени монитора...', allProviders: 'Все провайдеры', allStatus: 'Все статусы', enabledFilter: 'Включено', - onlyEnabled: 'Enabled only', - onlyDisabled: 'Disabled only', - createButton: 'Create Monitor', - createTitle: 'Create Channel Monitor', - editTitle: 'Edit Channel Monitor', - runNow: 'Run Now', - runSuccess: 'Check completed', - runFailed: 'Check failed', - apiKeyDecryptFailed: 'API Key decryption failed. Please re-edit this monitor with a fresh key.', - createSuccess: 'Monitor created', - updateSuccess: 'Monitor updated', - deleteSuccess: 'Monitor deleted', - loadError: 'Failed to load monitors', - deleteConfirm: 'Are you sure you want to delete monitor "{name}"? This action cannot be undone.', - nameRequired: 'Please enter a monitor name', - primaryModelRequired: 'Please enter a primary model', + onlyEnabled: 'Только включённые', + onlyDisabled: 'Только отключённые', + createButton: 'Создать монитор', + createTitle: 'Создать монитор канала', + editTitle: 'Изменить монитор канала', + runNow: 'Запустить сейчас', + runSuccess: 'Проверка завершена', + runFailed: 'Проверка не удалась', + apiKeyDecryptFailed: 'Не удалось расшифровать API-ключ. Отредактируйте монитор и укажите новый ключ.', + createSuccess: 'Монитор создан', + updateSuccess: 'Монитор обновлён', + deleteSuccess: 'Монитор удалён', + loadError: 'Не удалось загрузить мониторы', + deleteConfirm: 'Удалить монитор "{name}"? Это действие нельзя отменить.', + nameRequired: 'Введите имя монитора', + primaryModelRequired: 'Введите основную модель', columns: { name: 'Имя', provider: 'Провайдер', @@ -2722,125 +2722,125 @@ export default { }, form: { name: 'Имя', - namePlaceholder: 'Enter monitor name', + namePlaceholder: 'Введите имя монитора', provider: 'Платформа', - apiMode: 'OpenAI protocol', - apiModeChatCompletions: 'OpenAI Compatible', - apiModeChatCompletionsHint: 'Use /v1/chat/completions with messages; works for most compatible providers.', + apiMode: 'Протокол OpenAI', + apiModeChatCompletions: 'OpenAI-совместимый', + apiModeChatCompletionsHint: 'Использует /v1/chat/completions с messages; работает с большинством совместимых провайдеров.', apiModeResponses: 'Responses API', - apiModeResponsesHint: 'Use /v1/responses with default instructions + input; best for self-check/Codex paths.', + apiModeResponsesHint: 'Использует /v1/responses с default instructions + input; лучше для self-check/Codex путей.', endpoint: 'Endpoint', endpointPlaceholder: 'https://api.example.com', - useCurrentDomain: 'Use current service', + useCurrentDomain: 'Использовать текущий сервис', apiKey: 'API-ключ', - apiKeyPlaceholder: 'Enter API Key', - apiKeyEditPlaceholder: 'Leave blank to keep current key', - useMyKey: 'Use my key', - selectKeyTitle: 'Select my API Key', - selectKeyHint: 'Only your active, non-expired keys are listed.', - noActiveKey: 'No active API keys available', + apiKeyPlaceholder: 'Введите API-ключ', + apiKeyEditPlaceholder: 'Оставьте пустым, чтобы сохранить текущий ключ', + useMyKey: 'Использовать мой ключ', + selectKeyTitle: 'Выберите мой API-ключ', + selectKeyHint: 'Показаны только ваши активные ключи без истёкшего срока.', + noActiveKey: 'Нет доступных активных API-ключей', primaryModel: 'Основная модель', primaryModelPlaceholder: 'gpt-4o-mini', extraModels: 'Дополнительные модели', - extraModelsPlaceholder: 'Press Enter to add extra model', - groupName: 'Group Name', - groupNamePlaceholder: 'Optional, used to group rows in user view', - intervalSeconds: 'Interval (seconds)', - intervalSecondsHint: 'Range: 15 - 3600 seconds', - enabled: 'Enable monitor', - kindRequired: 'Please select a provider' - }, - runResultTitle: 'Check Result', - noMonitorsYet: 'No monitors yet', - createFirstMonitor: 'Create your first monitor to track channel availability', + extraModelsPlaceholder: 'Нажмите Enter, чтобы добавить модель', + groupName: 'Имя группы', + groupNamePlaceholder: 'Необязательно, используется для группировки строк в пользовательском виде', + intervalSeconds: 'Интервал (секунды)', + intervalSecondsHint: 'Диапазон: 15–3600 секунд', + enabled: 'Включить монитор', + kindRequired: 'Выберите провайдера' + }, + runResultTitle: 'Результат проверки', + noMonitorsYet: 'Мониторов пока нет', + createFirstMonitor: 'Создайте первый монитор для отслеживания доступности канала', advanced: { - section: 'Advanced (optional)', - sectionHint: 'Customize request headers and body to bypass upstream client-detection (e.g. "only Claude Code clients allowed").', - headers: 'Custom request headers', + section: 'Дополнительно (необязательно)', + sectionHint: 'Настройте headers и body запроса, чтобы обойти определение клиента апстримом (например, "only Claude Code clients allowed").', + headers: 'Свои headers запроса', headersPlaceholder: 'User-Agent: claude-cli/1.0.83 (external, cli)\nx-app: cli\nanthropic-beta: claude-code-20250219', - headerNamePlaceholder: 'Header name', - headerValuePlaceholder: 'Value', - headerAddRow: 'Add header', - headerNameInvalid: 'Header name cannot contain whitespace or colon: {name}', - headersHint: 'Merged on top of adapter defaults (user wins). Hop-by-hop headers (Host / Content-Length / ...) are ignored.', - headersParseError: 'Cannot parse line: {line}', - bodyMode: 'Body handling', + headerNamePlaceholder: 'Имя header', + headerValuePlaceholder: 'Значение', + headerAddRow: 'Добавить header', + headerNameInvalid: 'Имя header не может содержать пробелы или двоеточие: {name}', + headersHint: 'Объединяются поверх значений адаптера (пользовательские имеют приоритет). Hop-by-hop headers (Host / Content-Length / ...) игнорируются.', + headersParseError: 'Не удалось разобрать строку: {line}', + bodyMode: 'Обработка body', bodyModeOff: 'По умолчанию', - bodyModeMerge: 'Merge', - bodyModeReplace: 'Replace', - bodyModeHintOff: 'Use the adapter default body (includes challenge validation).', - bodyModeHintMerge: 'Shallow-merge with the default body; user fields win but model / messages / contents are protected (use Replace to change those).', - bodyModeHintReplace: 'Use the JSON below as the complete body. Challenge validation is skipped; HTTP 2xx + non-empty response text is treated as operational.', + bodyModeMerge: 'Объединить', + bodyModeReplace: 'Заменить', + bodyModeHintOff: 'Использовать body адаптера по умолчанию (включает challenge validation).', + bodyModeHintMerge: 'Поверхностно объединить с body по умолчанию; пользовательские поля имеют приоритет, но model / messages / contents защищены (для изменения используйте Заменить).', + bodyModeHintReplace: 'Использовать JSON ниже как полный body. Challenge validation пропускается; HTTP 2xx + непустой текст ответа считается рабочим.', bodyJson: 'Body JSON', - bodyJsonFormat: 'Format', - bodyJsonHint: 'Parsed on blur. Empty means no override.', - bodyJsonError: 'JSON parse failed', - bodyJsonObjectError: 'Body must be a JSON object (no arrays or primitives)' + bodyJsonFormat: 'Форматировать', + bodyJsonHint: 'Разбирается при потере фокуса. Пусто = без переопределения.', + bodyJsonError: 'Не удалось разобрать JSON', + bodyJsonObjectError: 'Body должен быть JSON object (не массив и не примитив)' }, templateField: { - label: 'Request template', - none: 'No template', - placeholder: 'Pick a template (filtered by current provider)', - applyHint: 'Picking a template copies its headers and body to this monitor (snapshot). Later template edits are not auto-synced.' + label: 'Шаблон запроса', + none: 'Без шаблона', + placeholder: 'Выберите шаблон (фильтр по текущему провайдеру)', + applyHint: 'Выбор шаблона копирует его headers и body в этот монитор (снимок). Последующие изменения шаблона не синхронизируются автоматически.' }, template: { - manageButton: 'Templates', - managerTitle: 'Request template manager', - createButton: 'New template', - emptyState: 'No templates for this provider yet', - missingName: 'Template name is required', - createSuccess: 'Template created', - updateSuccess: 'Template updated', - deleteSuccess: 'Template deleted', - applyButton: 'Apply to monitors', - applyTooltip: 'Overwrite snapshot fields on associated monitors', - applyTitle: 'Apply template', + manageButton: 'Шаблоны', + managerTitle: 'Менеджер шаблонов запросов', + createButton: 'Новый шаблон', + emptyState: 'Шаблонов для этого провайдера пока нет', + missingName: 'Имя шаблона обязательно', + createSuccess: 'Шаблон создан', + updateSuccess: 'Шаблон обновлён', + deleteSuccess: 'Шаблон удалён', + applyButton: 'Применить к мониторам', + applyTooltip: 'Перезаписать поля снимка у связанных мониторов', + applyTitle: 'Применить шаблон', applyConfirm: 'Применить', - applyConfirmMessage: 'Overwrite {n} associated monitor(s) with the current configuration of "{name}"? Any local customizations on those monitors will be discarded.', - applySuccess: 'Applied to {n} monitor(s)', - applyPickerTitle: 'Apply template "{name}"', - applyPickerHint: 'Select which monitors to overwrite (all selected by default). Any local customizations will be discarded.', - applyPickerEmpty: 'No monitors are currently associated to this template', - applyPickerConfirm: 'Apply to {n} monitor(s)', - selectNone: 'Select none', - selectedCount: 'Selected {n} / {total}', - deleteConfirm: 'Delete template "{name}"? {n} associated monitor(s) will be disassociated but keep their current snapshot and continue running.', - associatedCount: '{n} associated monitor(s)', - headersSummary: '{n} custom header(s)', + applyConfirmMessage: 'Перезаписать {n} связанных мониторов текущей конфигурацией "{name}"? Все локальные изменения этих мониторов будут сброшены.', + applySuccess: 'Применено к мониторам: {n}', + applyPickerTitle: 'Применить шаблон "{name}"', + applyPickerHint: 'Выберите мониторы для перезаписи (по умолчанию выбраны все). Локальные изменения будут сброшены.', + applyPickerEmpty: 'С этим шаблоном пока не связан ни один монитор', + applyPickerConfirm: 'Применить к мониторам: {n}', + selectNone: 'Снять выбор', + selectedCount: 'Выбрано {n} / {total}', + deleteConfirm: 'Удалить шаблон "{name}"? {n} связанных мониторов будут отвязаны, но сохранят текущий снимок и продолжат работать.', + associatedCount: 'Связанных мониторов: {n}', + headersSummary: 'Пользовательских headers: {n}', form: { - name: 'Template name', - namePlaceholder: 'e.g. Claude Code mimicry', + name: 'Имя шаблона', + namePlaceholder: 'например, Claude Code mimicry', description: 'Описание', - descriptionPlaceholder: 'Optional: what this template is for, capture date, etc.' + descriptionPlaceholder: 'Необязательно: для чего шаблон, дата фиксации и т. п.' } } }, // Subscriptions subscriptions: { - title: 'Subscription Management', - description: 'Manage user subscriptions and quota limits', - assignSubscription: 'Assign Subscription', - adjustSubscription: 'Adjust Subscription', - revokeSubscription: 'Revoke Subscription', + title: 'Управление подписками', + description: 'Управляйте подписками пользователей и лимитами квот', + assignSubscription: 'Назначить подписку', + adjustSubscription: 'Изменить подписку', + revokeSubscription: 'Отозвать подписку', allStatus: 'Все статусы', allGroups: 'Все группы', - allPlatforms: 'All Platforms', + allPlatforms: 'Все платформы', daily: 'Дневная', weekly: 'Недельная', monthly: 'Месячная', - noLimits: 'No limits configured', + noLimits: 'Лимиты не настроены', unlimited: 'Безлимитно', resetNow: 'Скоро сброс', - windowNotActive: 'Window not active', - resetInMinutes: 'Resets in {minutes}m', - resetInHoursMinutes: 'Resets in {hours}h {minutes}m', - resetInDaysHours: 'Resets in {days}d {hours}h', - quotaEndsInMinutes: 'Quota ends in {minutes}m', - quotaEndsInHoursMinutes: 'Quota ends in {hours}h {minutes}m', - quotaEndsInDaysHours: 'Quota ends in {days}d {hours}h', - daysRemaining: 'days remaining', - remainingDays: 'Remaining days', + windowNotActive: 'Окно неактивно', + resetInMinutes: 'Сброс через {minutes} мин', + resetInHoursMinutes: 'Сброс через {hours} ч {minutes} мин', + resetInDaysHours: 'Сброс через {days} д {hours} ч', + quotaEndsInMinutes: 'Квота закончится через {minutes} мин', + quotaEndsInHoursMinutes: 'Квота закончится через {hours} ч {minutes} мин', + quotaEndsInDaysHours: 'Квота закончится через {days} д {hours} ч', + daysRemaining: 'дней осталось', + remainingDays: 'Осталось дней', noExpiration: 'Без срока', status: { active: 'Активен', @@ -2857,80 +2857,80 @@ export default { }, form: { user: 'Пользователь', - group: 'Subscription Group', - validityDays: 'Validity (Days)', - adjustDays: 'Adjust by (Days)' - }, - selectUser: 'Select a user', - selectGroup: 'Select a subscription group', - groupHint: 'Only groups with subscription billing type are shown', - validityHint: 'Number of days the subscription will be valid', - adjustingFor: 'Adjusting subscription for', + group: 'Группа подписки', + validityDays: 'Срок действия (дни)', + adjustDays: 'Изменить на (дни)' + }, + selectUser: 'Выберите пользователя', + selectGroup: 'Выберите группу подписки', + groupHint: 'Показаны только группы с типом списания подписки', + validityHint: 'Количество дней действия подписки', + adjustingFor: 'Изменение подписки для', currentExpiration: 'Текущий срок действия', - adjustDaysPlaceholder: 'Positive to extend, negative to shorten', - adjustHint: 'Enter positive number to extend, negative to shorten (remaining days must be > 0)', - assign: 'Assign', - assigning: 'Assigning...', - adjust: 'Adjust', - adjusting: 'Adjusting...', - revoke: 'Revoke', - resetQuota: 'Reset Quota', - resetQuotaTitle: 'Reset Usage Quota', - resetQuotaConfirm: "Reset the daily, weekly, and monthly usage quota for '{user}'? Usage will be zeroed and windows restarted from today.", + adjustDaysPlaceholder: 'Положительное — продлить, отрицательное — сократить', + adjustHint: 'Введите положительное число для продления, отрицательное для сокращения (остаток дней должен быть > 0)', + assign: 'Назначить', + assigning: 'Назначение...', + adjust: 'Изменить', + adjusting: 'Изменение...', + revoke: 'Отозвать', + resetQuota: 'Сбросить квоту', + resetQuotaTitle: 'Сбросить квоту расхода', + resetQuotaConfirm: "Сбросить дневную, недельную и месячную квоту расхода для '{user}'? Расход будет обнулён, окна начнутся заново с сегодняшнего дня.", quotaResetSuccess: 'Квота сброшена', failedToResetQuota: 'Не удалось сбросить квоту', - noSubscriptionsYet: 'No subscriptions yet', - assignFirstSubscription: 'Assign a subscription to get started.', - subscriptionAssigned: 'Subscription assigned successfully', - subscriptionAdjusted: 'Subscription adjusted successfully', - subscriptionRevoked: 'Subscription revoked successfully', + noSubscriptionsYet: 'Подписок пока нет', + assignFirstSubscription: 'Назначьте подписку, чтобы начать.', + subscriptionAssigned: 'Подписка назначена', + subscriptionAdjusted: 'Подписка изменена', + subscriptionRevoked: 'Подписка отозвана', failedToLoad: 'Не удалось загрузить подписки', - failedToAssign: 'Failed to assign subscription', - failedToAdjust: 'Failed to adjust subscription', - failedToRevoke: 'Failed to revoke subscription', - adjustWouldExpire: 'Remaining days after adjustment must be greater than 0', - adjustOutOfRange: 'Adjustment days must be between -36500 and 36500', - pleaseSelectUser: 'Please select a user', + failedToAssign: 'Не удалось назначить подписку', + failedToAdjust: 'Не удалось изменить подписку', + failedToRevoke: 'Не удалось отозвать подписку', + adjustWouldExpire: 'После изменения должно остаться больше 0 дней', + adjustOutOfRange: 'Изменение дней должно быть от -36500 до 36500', + pleaseSelectUser: 'Выберите пользователя', pleaseSelectGroup: 'Выберите группу', - validityDaysRequired: 'Please enter a valid number of days (at least 1)', + validityDaysRequired: 'Введите корректное число дней (минимум 1)', revokeConfirm: - "Are you sure you want to revoke the subscription for '{user}'? This action cannot be undone.", + "Отозвать подписку пользователя '{user}'? Это действие нельзя отменить.", guide: { - title: 'Subscription Management Guide', - subtitle: 'Subscription mode lets you assign time-based usage quotas to users, with daily/weekly/monthly limits. Follow these steps to get started.', - showGuide: 'Usage Guide', + title: 'Руководство по управлению подписками', + subtitle: 'Режим подписки позволяет назначать пользователям временные квоты расхода с дневными, недельными и месячными лимитами. Выполните эти шаги, чтобы начать.', + showGuide: 'Руководство', step1: { - title: 'Create a Subscription Group', - line1: 'Go to "Group Management" page, click "Create Group"', - line2: 'Set billing type to "Subscription", configure daily/weekly/monthly quota limits', - line3: 'Save the group and ensure its status is "Активен"', - link: 'Go to Group Management' + title: 'Создайте группу подписки', + line1: 'Перейдите на страницу "Управление группами" и нажмите "Создать группу"', + line2: 'Установите тип списания "Подписка" и настройте дневные, недельные и месячные лимиты квоты', + line3: 'Сохраните группу и убедитесь, что её статус "Активен"', + link: 'Перейти к управлению группами' }, step2: { - title: 'Assign Subscription to User', - line1: 'Click the "Assign Subscription" button in the top right', - line2: 'Search for a user by email and select them', - line3: 'Choose a subscription group, set validity days, then click "Assign"' + title: 'Назначьте подписку пользователю', + line1: 'Нажмите кнопку "Назначить подписку" справа сверху', + line2: 'Найдите пользователя по email и выберите его', + line3: 'Выберите группу подписки, задайте срок действия и нажмите "Назначить"' }, step3: { - title: 'Manage Existing Subscriptions' + title: 'Управляйте существующими подписками' }, actions: { - adjust: 'Adjust', - adjustDesc: 'Extend or shorten the subscription validity period', - resetQuota: 'Reset Quota', - resetQuotaDesc: 'Reset daily/weekly/monthly usage to zero', - revoke: 'Revoke', - revokeDesc: 'Immediately terminate the subscription (irreversible)' + adjust: 'Изменить', + adjustDesc: 'Продлить или сократить срок действия подписки', + resetQuota: 'Сбросить квоту', + resetQuotaDesc: 'Обнулить дневной, недельный и месячный расход', + revoke: 'Отозвать', + revokeDesc: 'Немедленно завершить подписку (необратимо)' }, - tip: 'Tip: Only groups with billing type "Subscription" and status "Активен" appear in the group dropdown. If no options are available, create one in Group Management first.' + tip: 'Совет: в списке групп отображаются только группы с типом списания "Подписка" и статусом "Активен". Если вариантов нет, сначала создайте группу в управлении группами.' } }, // Accounts accounts: { title: 'Управление аккаунтами', - description: 'Manage AI platform accounts and credentials', + description: 'Управляйте аккаунтами AI-платформ и учётными данными', createAccount: 'Создать аккаунт', autoRefresh: 'Автообновление', enableAutoRefresh: 'Включить автообновление', @@ -2939,89 +2939,89 @@ export default { refreshInterval15s: '15 seconds', refreshInterval30s: '30 seconds', autoRefreshCountdown: 'Автообновление: {seconds}с', - listPendingSyncHint: 'List changes are pending sync. Click sync to load latest rows.', - listPendingSyncAction: 'Sync now', - syncFromCrs: 'Sync from CRS', + listPendingSyncHint: 'Есть несинхронизированные изменения списка. Нажмите синхронизацию, чтобы загрузить актуальные строки.', + listPendingSyncAction: 'Синхронизировать сейчас', + syncFromCrs: 'Синхронизация из CRS', dataExport: 'Экспорт', - dataExportSelected: 'Export Selected', - dataExportIncludeProxies: 'Include proxies linked to the exported accounts', + dataExportSelected: 'Экспортировать выбранное', + dataExportIncludeProxies: 'Включить proxy, привязанные к экспортируемым аккаунтам', dataImport: 'Импорт', - moreActions: 'More Actions', - dataActions: 'Data', - toolActions: 'Tools', - viewColumns: 'Columns', - selectedCount: '{count} selected', - dataExportConfirmMessage: 'The exported data contains sensitive account and proxy information. Store it securely.', - dataExportConfirm: 'Confirm Export', - dataExported: 'Data exported successfully', - dataExportFailed: 'Failed to export data', - dataImportTitle: 'Import Data', - dataImportHint: 'Upload the exported JSON file to import accounts and proxies.', - dataImportWarning: 'Import will create new accounts/proxies; groups must be bound manually. Ensure existing data does not conflict.', - dataImportFile: 'Data file', - dataImportButton: 'Start Import', - dataImporting: 'Importing...', - dataImportSelectFile: 'Please select a data file', - dataImportParseFailed: 'Failed to parse data file', - dataImportFailed: 'Data import failed', - dataImportResult: 'Import Result', - dataImportResultSummary: 'Proxies created {proxy_created}, reused {proxy_reused}, failed {proxy_failed}; Accounts created {account_created}, failed {account_failed}', - dataImportErrors: 'Error Details', - dataImportSuccess: 'Import completed: accounts {account_created}, failed {account_failed}', - dataImportCompletedWithErrors: 'Import completed with errors: account failed {account_failed}, proxy failed {proxy_failed}', - syncFromCrsTitle: 'Sync Accounts from CRS', + moreActions: 'Дополнительно', + dataActions: 'Данные', + toolActions: 'Инструменты', + viewColumns: 'Столбцы', + selectedCount: 'Выбрано: {count}', + dataExportConfirmMessage: 'Экспортированные данные содержат чувствительную информацию об аккаунтах и proxy. Храните их в безопасном месте.', + dataExportConfirm: 'Подтвердить экспорт', + dataExported: 'Данные экспортированы', + dataExportFailed: 'Не удалось экспортировать данные', + dataImportTitle: 'Импорт данных', + dataImportHint: 'Загрузите экспортированный JSON-файл для импорта аккаунтов и proxy.', + dataImportWarning: 'Импорт создаст новые аккаунты/proxy; группы нужно привязать вручную. Убедитесь, что нет конфликта с текущими данными.', + dataImportFile: 'Файл данных', + dataImportButton: 'Начать импорт', + dataImporting: 'Импорт...', + dataImportSelectFile: 'Выберите файл данных', + dataImportParseFailed: 'Не удалось разобрать файл данных', + dataImportFailed: 'Импорт данных не удался', + dataImportResult: 'Результат импорта', + dataImportResultSummary: 'proxy: создано {proxy_created}, переиспользовано {proxy_reused}, ошибок {proxy_failed}; аккаунтов создано {account_created}, ошибок {account_failed}', + dataImportErrors: 'Детали ошибок', + dataImportSuccess: 'Импорт завершён: аккаунтов {account_created}, ошибок {account_failed}', + dataImportCompletedWithErrors: 'Импорт завершён с ошибками: аккаунтов с ошибкой {account_failed}, proxy с ошибкой {proxy_failed}', + syncFromCrsTitle: 'Синхронизация аккаунтов из CRS', syncFromCrsDesc: - 'Sync accounts from claude-relay-service (CRS) into this system (CRS is called server-to-server).', - crsVersionRequirement: '⚠️ Note: CRS version must be ≥ v1.1.240 to support this feature', + 'Синхронизируйте аккаунты из claude-relay-service (CRS) в эту систему (CRS вызывается server-to-server).', + crsVersionRequirement: '⚠️ Примечание: для этой функции нужна версия CRS ≥ v1.1.240', crsBaseUrl: 'CRS Base URL', crsBaseUrlPlaceholder: 'e.g. http://127.0.0.1:3000', crsUsername: 'Имя пользователя', crsPassword: 'Пароль', - syncProxies: 'Also sync proxies (match by host/port/auth or create)', - syncNow: 'Sync Now', - syncing: 'Syncing...', - syncMissingFields: 'Please fill base URL, username and password', - syncResult: 'Sync Result', - syncResultSummary: 'Created {created}, updated {updated}, skipped {skipped}, failed {failed}', - syncErrors: 'Errors / Skipped Details', - syncCompleted: 'Sync completed: created {created}, updated {updated}, skipped {skipped}', + syncProxies: 'Также синхронизировать proxy (сопоставить по host/port/auth или создать)', + syncNow: 'Синхронизировать', + syncing: 'Синхронизация...', + syncMissingFields: 'Заполните Base URL, имя пользователя и пароль', + syncResult: 'Результат синхронизации', + syncResultSummary: 'Создано {created}, обновлено {updated}, пропущено {skipped}, ошибок {failed}', + syncErrors: 'Ошибки / детали пропусков', + syncCompleted: 'Синхронизация завершена: создано {created}, обновлено {updated}, пропущено {skipped}', syncCompletedWithErrors: - 'Sync completed with errors: failed {failed} (created {created}, updated {updated}, skipped {skipped})', - syncFailed: 'Sync failed', - crsPreview: 'Preview', - crsPreviewing: 'Previewing...', - crsPreviewFailed: 'Preview failed', - crsExistingAccounts: 'Existing accounts (will be updated)', - crsNewAccounts: 'New accounts (select to sync)', + 'Синхронизация завершена с ошибками: ошибок {failed} (создано {created}, обновлено {updated}, пропущено {skipped})', + syncFailed: 'Синхронизация не удалась', + crsPreview: 'Предпросмотр', + crsPreviewing: 'Предпросмотр...', + crsPreviewFailed: 'Предпросмотр не удался', + crsExistingAccounts: 'Существующие аккаунты (будут обновлены)', + crsNewAccounts: 'Новые аккаунты (выберите для синхронизации)', crsSelectAll: 'Выбрать все', - crsSelectNone: 'Select none', - crsNoNewAccounts: 'All CRS accounts are already synced.', - crsWillUpdate: 'Will update {count} existing accounts.', - crsSelectedCount: '{count} new accounts selected', + crsSelectNone: 'Снять выбор', + crsNoNewAccounts: 'Все аккаунты CRS уже синхронизированы.', + crsWillUpdate: 'Будут обновлены существующие аккаунты: {count}.', + crsSelectedCount: 'Выбрано новых аккаунтов: {count}', crsUpdateBehaviorNote: - 'Existing accounts only sync fields returned by CRS; missing fields keep their current values. Credentials are merged by key — keys not returned by CRS are preserved. Proxies are kept when "Sync proxies" is unchecked.', + 'Для существующих аккаунтов синхронизируются только поля, которые вернул CRS; отсутствующие поля сохраняют текущие значения. Учётные данные объединяются по ключу — ключи, не возвращённые CRS, сохраняются. proxy сохраняются, если "Синхронизировать proxy" выключено.', crsBack: 'Назад', - editAccount: 'Edit Account', - deleteAccount: 'Delete Account', - searchAccounts: 'Search accounts...', - notes: 'Notes', - notesPlaceholder: 'Enter notes', - notesHint: 'Notes are optional', - allPlatforms: 'All Platforms', - allTypes: 'All Types', + editAccount: 'Изменить аккаунт', + deleteAccount: 'Удалить аккаунт', + searchAccounts: 'Поиск аккаунтов...', + notes: 'Заметки', + notesPlaceholder: 'Введите заметки', + notesHint: 'Заметки необязательны', + allPlatforms: 'Все платформы', + allTypes: 'Все типы', allStatus: 'Все статусы', allGroups: 'Все группы', - ungroupedGroup: 'Ungrouped', + ungroupedGroup: 'Без группы', oauthType: 'OAuth', setupToken: 'Setup Token', apiKey: 'API-ключ', // Schedulable toggle - schedulable: 'Schedulable', - schedulableHint: 'Enable to include this account in API request scheduling', - schedulableEnabled: 'Scheduling enabled', - schedulableDisabled: 'Scheduling disabled', - failedToToggleSchedulable: 'Failed to toggle scheduling status', - groupCountTotal: '{count} groups total', + schedulable: 'Доступен для маршрутизации', + schedulableHint: 'Включите, чтобы аккаунт участвовал в маршрутизации API-запросов', + schedulableEnabled: 'Участие в маршрутизации включено', + schedulableDisabled: 'Участие в маршрутизации отключено', + failedToToggleSchedulable: 'Не удалось изменить статус участия в маршрутизации', + groupCountTotal: 'Всего групп: {count}', platforms: { anthropic: 'Anthropic', claude: 'Claude', @@ -3044,289 +3044,289 @@ export default { active: 'Активен', inactive: 'Неактивен', error: 'Ошибка', - cooldown: 'Cooldown', - paused: 'Paused', - limited: 'Limited', - rateLimited: 'Rate Limited', - overloaded: 'Overloaded', - tempUnschedulable: 'Temp Unschedulable', - quotaExceeded: 'Quota Exceeded', - unschedulable: 'Unschedulable', - rateLimitedUntil: 'Rate limited and removed from scheduling. Auto resumes at {time}', - rateLimitedAutoResume: 'Auto resumes in {time}', - modelRateLimitedUntil: '{model} rate limited until {time}', - modelCreditOveragesUntil: '{model} using AI Credits until {time}', - creditsExhausted: 'Credits Exhausted', - creditsExhaustedUntil: 'AI Credits exhausted, expected recovery at {time}', - overloadedUntil: 'Overloaded until {time}', - viewTempUnschedDetails: 'View temp unschedulable details' + cooldown: 'Ожидание', + paused: 'Пауза', + limited: 'Ограничен', + rateLimited: 'Ограничен по лимиту', + overloaded: 'Перегружен', + tempUnschedulable: 'Временно исключён из маршрутизации', + quotaExceeded: 'Квота превышена', + unschedulable: 'Исключён из маршрутизации', + rateLimitedUntil: 'Ограничен по лимиту и исключён из маршрутизации. Автовозврат в {time}', + rateLimitedAutoResume: 'Автовозврат через {time}', + modelRateLimitedUntil: '{model} ограничена по лимиту до {time}', + modelCreditOveragesUntil: '{model} использует AI Credits до {time}', + creditsExhausted: 'AI Credits исчерпаны', + creditsExhaustedUntil: 'AI Credits исчерпаны, ожидаемое восстановление в {time}', + overloadedUntil: 'Перегружен до {time}', + viewTempUnschedDetails: 'Показать детали временного исключения из маршрутизации' }, columns: { name: 'Имя', - platformType: 'Platform/Type', + platformType: 'Платформа/тип', platform: 'Платформа', type: 'Тип', - capacity: 'Capacity', - notes: 'Notes', - priority: 'Priority', - billingRateMultiplier: 'Billing Rate', - weight: 'Weight', + capacity: 'Ёмкость', + notes: 'Заметки', + priority: 'Приоритет', + billingRateMultiplier: 'Тарифный коэффициент', + weight: 'Вес', status: 'Статус', - schedulable: 'Schedulable', - todayStats: 'Today Stats', + schedulable: 'Доступен для маршрутизации', + todayStats: 'Статистика за сегодня', groups: 'Группы', - usageWindows: 'Usage Windows', + usageWindows: 'Окна расхода', proxy: 'Proxy', lastUsed: 'Последнее использование', createdAt: 'Создан', expiresAt: 'Истекает', actions: 'Действия' }, - allPrivacyModes: 'All Privacy States', - privacyUnset: 'Unset', - privacyTrainingOff: 'Training data sharing disabled', - privacyCfBlocked: 'Blocked by Cloudflare, training may still be on', - privacyFailed: 'Failed to disable training', - privacyAntigravitySet: 'Telemetry and marketing emails disabled', - privacyAntigravityFailed: 'Privacy setting failed', - setPrivacy: 'Set Privacy', - subscriptionAbnormal: 'Abnormal', + allPrivacyModes: 'Все состояния приватности', + privacyUnset: 'Не задано', + privacyTrainingOff: 'Передача данных для обучения отключена', + privacyCfBlocked: 'Заблокировано Cloudflare, обучение может быть всё ещё включено', + privacyFailed: 'Не удалось отключить обучение', + privacyAntigravitySet: 'Телеметрия и маркетинговые письма отключены', + privacyAntigravityFailed: 'Не удалось настроить приватность', + setPrivacy: 'Настроить приватность', + subscriptionAbnormal: 'Аномалия', subscriptionExpires: 'Истекает', // Capacity status tooltips capacity: { windowCost: { - blocked: '5h window cost exceeded, account scheduling paused', - stickyOnly: '5h window cost at threshold, only sticky sessions allowed', - normal: '5h window cost normal' + blocked: 'Расход за окно 5h превышен, маршрутизация аккаунта приостановлена', + stickyOnly: 'Расход за окно 5h на пороге, разрешены только sticky-сессии', + normal: 'Расход за окно 5h в норме' }, sessions: { - full: 'Active sessions full, new sessions must wait (idle timeout: {idle} min)', - normal: 'Active sessions normal (idle timeout: {idle} min)' + full: 'Активные сессии заполнены, новые сессии ждут (idle timeout: {idle} мин)', + normal: 'Активные сессии в норме (idle timeout: {idle} мин)' }, rpm: { - full: 'RPM limit reached', - warning: 'RPM approaching limit', - normal: 'RPM normal', - tieredNormal: 'RPM limit (Tiered) - Normal', - tieredWarning: 'RPM limit (Tiered) - Approaching limit', - tieredStickyOnly: 'RPM limit (Tiered) - Sticky only | Buffer: {buffer}', - tieredBlocked: 'RPM limit (Tiered) - Blocked | Buffer: {buffer}', - stickyExemptNormal: 'RPM limit (Sticky Exempt) - Normal', - stickyExemptWarning: 'RPM limit (Sticky Exempt) - Approaching limit', - stickyExemptOver: 'RPM limit (Sticky Exempt) - Over limit, sticky only' + full: 'Лимит RPM достигнут', + warning: 'RPM приближается к лимиту', + normal: 'RPM в норме', + tieredNormal: 'Лимит RPM (Tiered) — норма', + tieredWarning: 'Лимит RPM (Tiered) — приближается к лимиту', + tieredStickyOnly: 'Лимит RPM (Tiered) — только sticky | Buffer: {buffer}', + tieredBlocked: 'Лимит RPM (Tiered) — блокировка | Buffer: {buffer}', + stickyExemptNormal: 'Лимит RPM (Sticky Exempt) — норма', + stickyExemptWarning: 'Лимит RPM (Sticky Exempt) — приближается к лимиту', + stickyExemptOver: 'Лимит RPM (Sticky Exempt) — превышен, только sticky' }, quota: { - exceeded: 'Quota exceeded, account paused', - normal: 'Quota normal' + exceeded: 'Квота превышена, аккаунт приостановлен', + normal: 'Квота в норме' }, }, tempUnschedulable: { - title: 'Temp Unschedulable', - statusTitle: 'Temp Unschedulable Status', - hint: 'Disable accounts temporarily when error code and keyword both match.', - notice: 'Rules are evaluated in order and require both error code and keyword match.', - addRule: 'Add Rule', - ruleOrder: 'Rule Order', - ruleIndex: 'Rule #{index}', - errorCode: 'Error Code', + title: 'Временно исключён из маршрутизации', + statusTitle: 'Статус временного исключения из маршрутизации', + hint: 'Временно отключайте аккаунты, когда совпадают код ошибки и ключевое слово.', + notice: 'Правила проверяются по порядку и требуют совпадения кода ошибки и ключевого слова.', + addRule: 'Добавить правило', + ruleOrder: 'Порядок правил', + ruleIndex: 'Правило #{index}', + errorCode: 'Код ошибки', errorCodePlaceholder: 'e.g. 429', - durationMinutes: 'Duration (minutes)', + durationMinutes: 'Длительность (минуты)', durationPlaceholder: 'e.g. 30', - keywords: 'Keywords', + keywords: 'Ключевые слова', keywordsPlaceholder: 'e.g. overloaded, too many requests', - keywordsHint: 'Separate keywords with commas; any keyword match will trigger.', + keywordsHint: 'Разделяйте ключевые слова запятыми; любое совпадение сработает.', description: 'Описание', - descriptionPlaceholder: 'Optional note for this rule', - rulesInvalid: 'Add at least one rule with error code, keywords, and duration.', - viewDetails: 'View temp unschedulable details', - accountName: 'Account', - triggeredAt: 'Triggered At', - until: 'Until', - remaining: 'Remaining', - matchedKeyword: 'Matched Keyword', - errorMessage: 'Error Details', - reset: 'Recover State', - resetSuccess: 'Account state recovered successfully', - resetFailed: 'Failed to recover account state', - failedToLoad: 'Failed to load temp unschedulable status', - notActive: 'This account is not temporarily unschedulable.', + descriptionPlaceholder: 'Заметка к правилу (необязательно)', + rulesInvalid: 'Добавьте хотя бы одно правило с кодом ошибки, ключевыми словами и длительностью.', + viewDetails: 'Показать детали временного исключения из маршрутизации', + accountName: 'Аккаунт', + triggeredAt: 'Сработало', + until: 'До', + remaining: 'Осталось', + matchedKeyword: 'Совпавшее ключевое слово', + errorMessage: 'Детали ошибки', + reset: 'Восстановить состояние', + resetSuccess: 'Состояние аккаунта восстановлено', + resetFailed: 'Не удалось восстановить состояние аккаунта', + failedToLoad: 'Не удалось загрузить статус временного исключения', + notActive: 'Этот аккаунт не исключён временно из маршрутизации.', expired: 'Истекла', - remainingMinutes: 'About {minutes} minutes', - remainingHours: 'About {hours} hours', - remainingHoursMinutes: 'About {hours} hours {minutes} minutes', + remainingMinutes: 'Около {minutes} мин', + remainingHours: 'Около {hours} ч', + remainingHoursMinutes: 'Около {hours} ч {minutes} мин', presets: { overloadLabel: '529 Overloaded', - overloadDesc: 'Overloaded - pause 60 minutes', + overloadDesc: 'Перегрузка — пауза 60 минут', rateLimitLabel: '429 Rate Limit', - rateLimitDesc: 'Rate limited - pause 10 minutes', + rateLimitDesc: 'Ограничен по лимиту — пауза 10 минут', unavailableLabel: '503 Unavailable', - unavailableDesc: 'Unavailable - pause 30 minutes' + unavailableDesc: 'Недоступен — пауза 30 минут' } }, - clearRateLimit: 'Clear Rate Limit', - resetQuota: 'Reset Quota', + clearRateLimit: 'Очистить лимит частоты', + resetQuota: 'Сбросить квоту', quotaLimit: 'Лимит квоты', - quotaLimitPlaceholder: '0 means unlimited', - quotaLimitHint: 'Set daily/weekly/total spending limits (USD). Anthropic API key accounts can also configure client affinity. Changing limits won\'t reset usage.', - quotaLimitToggle: 'Enable Quota Limit', - quotaLimitToggleHint: 'When enabled, account will be paused when usage reaches the set limit', + quotaLimitPlaceholder: '0 = безлимитно', + quotaLimitHint: 'Задайте дневные, недельные и общие лимиты расхода (USD). Аккаунты Anthropic API key также могут настраивать client affinity. Изменение лимитов не сбрасывает расход.', + quotaLimitToggle: 'Включить лимит квоты', + quotaLimitToggleHint: 'Если включено, аккаунт будет приостановлен при достижении заданного лимита расхода', quotaDailyLimit: 'Дневной лимит', - quotaDailyLimitHint: 'Automatically resets every 24 hours from first usage.', + quotaDailyLimitHint: 'Автоматически сбрасывается каждые 24 часа с первого использования.', quotaWeeklyLimit: 'Недельный лимит', - quotaWeeklyLimitHint: 'Automatically resets every 7 days from first usage.', - quotaTotalLimit: 'Total Limit', - quotaTotalLimitHint: 'Cumulative spending limit. Does not auto-reset — use "Reset Quota" to clear.', - quotaResetMode: 'Reset Mode', - quotaResetModeRolling: 'Rolling Window', - quotaResetModeFixed: 'Fixed Time', - quotaResetHour: 'Reset Hour', - quotaWeeklyResetDay: 'Reset Day', - quotaResetTimezone: 'Reset Timezone', - quotaDailyLimitHintFixed: 'Resets daily at {hour}:00 ({timezone}).', - quotaWeeklyLimitHintFixed: 'Resets every {day} at {hour}:00 ({timezone}).', + quotaWeeklyLimitHint: 'Автоматически сбрасывается каждые 7 дней с первого использования.', + quotaTotalLimit: 'Общий лимит', + quotaTotalLimitHint: 'Накопительный лимит расхода. Не сбрасывается автоматически — используйте "Сбросить квоту" для очистки.', + quotaResetMode: 'Режим сброса', + quotaResetModeRolling: 'Скользящее окно', + quotaResetModeFixed: 'Фиксированное время', + quotaResetHour: 'Час сброса', + quotaWeeklyResetDay: 'День сброса', + quotaResetTimezone: 'Часовой пояс сброса', + quotaDailyLimitHintFixed: 'Сбрасывается ежедневно в {hour}:00 ({timezone}).', + quotaWeeklyLimitHintFixed: 'Сбрасывается каждый {day} в {hour}:00 ({timezone}).', dayOfWeek: { - monday: 'Monday', - tuesday: 'Tuesday', - wednesday: 'Wednesday', - thursday: 'Thursday', - friday: 'Friday', - saturday: 'Saturday', - sunday: 'Sunday', - }, - quotaLimitAmount: 'Total Limit', - quotaLimitAmountHint: 'Cumulative spending limit. Does not auto-reset.', + monday: 'Понедельник', + tuesday: 'Вторник', + wednesday: 'Среда', + thursday: 'Четверг', + friday: 'Пятница', + saturday: 'Суббота', + sunday: 'Воскресенье', + }, + quotaLimitAmount: 'Общий лимит', + quotaLimitAmountHint: 'Накопительный лимит расхода. Не сбрасывается автоматически.', quotaNotify: { - alert: 'Alert', - enabled: 'Enable Alert', - threshold: 'Alert Amount', - thresholdPlaceholder: 'Enter percentage', + alert: 'Оповещение', + enabled: 'Включить оповещение', + threshold: 'Порог оповещения', + thresholdPlaceholder: 'Введите процент', }, testConnection: 'Проверить подключение', - reAuthorize: 'Re-Authorize', - refreshToken: 'Refresh Token', - noAccountsYet: 'No accounts yet', - createFirstAccount: 'Create your first account to start using AI services.', - tokenRefreshed: 'Token refreshed successfully', - accountDeleted: 'Account deleted successfully', - rateLimitCleared: 'Rate limit cleared successfully', - bulkSchedulableEnabled: 'Successfully enabled scheduling for {count} account(s)', - bulkSchedulableDisabled: 'Successfully disabled scheduling for {count} account(s)', - bulkSchedulablePartial: 'Scheduling updated partially: {success} succeeded, {failed} failed', - bulkSchedulableResultUnknown: 'Bulk scheduling result incomplete. Please retry or refresh.', + reAuthorize: 'Повторная авторизация', + refreshToken: 'Обновить токен', + noAccountsYet: 'Аккаунтов пока нет', + createFirstAccount: 'Создайте первый аккаунт, чтобы начать использовать AI-сервисы.', + tokenRefreshed: 'Токен обновлён', + accountDeleted: 'Аккаунт удалён', + rateLimitCleared: 'Лимит частоты очищен', + bulkSchedulableEnabled: 'Участие в маршрутизации включено для аккаунтов: {count}', + bulkSchedulableDisabled: 'Участие в маршрутизации отключено для аккаунтов: {count}', + bulkSchedulablePartial: 'Маршрутизация обновлена частично: успешно {success}, ошибок {failed}', + bulkSchedulableResultUnknown: 'Результат массового изменения маршрутизации неполный. Повторите или обновите страницу.', bulkActions: { - selected: '{count} account(s) selected', - selectCurrentPage: 'Select this page', - clear: 'Clear selection', - edit: 'Bulk Edit', - delete: 'Bulk Delete', - enableScheduling: 'Enable Scheduling', - disableScheduling: 'Disable Scheduling', - resetStatus: 'Reset Status', - refreshToken: 'Refresh Token', - resetStatusSuccess: 'Successfully reset {count} account(s) status', - refreshTokenSuccess: 'Successfully refreshed {count} account(s) token', - partialSuccess: 'Partially completed: {success} succeeded, {failed} failed' + selected: 'Выбрано аккаунтов: {count}', + selectCurrentPage: 'Выбрать эту страницу', + clear: 'Снять выбор', + edit: 'Массовое редактирование', + delete: 'Массовое удаление', + enableScheduling: 'Включить маршрутизацию', + disableScheduling: 'Отключить маршрутизацию', + resetStatus: 'Сбросить статус', + refreshToken: 'Обновить токен', + resetStatusSuccess: 'Статус сброшен для аккаунтов: {count}', + refreshTokenSuccess: 'Токен обновлён для аккаунтов: {count}', + partialSuccess: 'Выполнено частично: успешно {success}, ошибок {failed}' }, bulkEdit: { - title: 'Bulk Edit Accounts', + title: 'Массовое редактирование аккаунтов', selectionInfo: - '{count} account(s) selected. Only checked or filled fields will be updated; others stay unchanged.', + 'Выбрано аккаунтов: {count}. Будут обновлены только отмеченные или заполненные поля; остальные останутся без изменений.', baseUrlPlaceholder: 'https://api.anthropic.com or https://api.openai.com', - baseUrlNotice: 'Applies to API Key accounts only; leave empty to keep existing value', - submit: 'Update Accounts', + baseUrlNotice: 'Применяется только к аккаунтам API Key; оставьте пустым, чтобы сохранить текущее значение', + submit: 'Обновить аккаунты', updating: 'Обновление...', - success: 'Updated {count} account(s)', - partialSuccess: 'Partially updated: {success} succeeded, {failed} failed', - failed: 'Bulk update failed', - noSelection: 'Please select accounts to edit', - noFieldsSelected: 'Select at least one field to update', - mixedPlatformWarning: 'Selected accounts span multiple platforms ({platforms}). Model mapping presets shown are combined — ensure mappings are appropriate for each platform.' - }, - bulkDeleteTitle: 'Bulk Delete Accounts', - bulkDeleteConfirm: 'Delete the selected {count} account(s)? This action cannot be undone.', - bulkDeleteSuccess: 'Deleted {count} account(s)', - bulkDeletePartial: 'Partially deleted: {success} succeeded, {failed} failed', - bulkDeleteFailed: 'Bulk delete failed', - recoverState: 'Recover State', - recoverStateHint: 'Used to recover error, rate-limit, and temporary unschedulable runtime state.', - recoverStateSuccess: 'Account state recovered successfully', - recoverStateFailed: 'Failed to recover account state', - resetStatus: 'Reset Status', - statusReset: 'Account status reset successfully', - failedToResetStatus: 'Failed to reset account status', - failedToLoad: 'Failed to load accounts', - failedToRefresh: 'Failed to refresh token', - failedToDelete: 'Failed to delete account', - failedToClearRateLimit: 'Failed to clear rate limit', + success: 'Обновлено аккаунтов: {count}', + partialSuccess: 'Обновлено частично: успешно {success}, ошибок {failed}', + failed: 'Массовое обновление не удалось', + noSelection: 'Выберите аккаунты для редактирования', + noFieldsSelected: 'Выберите хотя бы одно поле для обновления', + mixedPlatformWarning: 'Выбранные аккаунты относятся к нескольким платформам ({platforms}). Показанные пресеты model mapping объединены — убедитесь, что маппинги подходят для каждой платформы.' + }, + bulkDeleteTitle: 'Массовое удаление аккаунтов', + bulkDeleteConfirm: 'Удалить выбранные аккаунты ({count})? Это действие нельзя отменить.', + bulkDeleteSuccess: 'Удалено аккаунтов: {count}', + bulkDeletePartial: 'Удалено частично: успешно {success}, ошибок {failed}', + bulkDeleteFailed: 'Массовое удаление не удалось', + recoverState: 'Восстановить состояние', + recoverStateHint: 'Используется для восстановления runtime-состояния ошибок, лимитов частоты и временного исключения из маршрутизации.', + recoverStateSuccess: 'Состояние аккаунта восстановлено', + recoverStateFailed: 'Не удалось восстановить состояние аккаунта', + resetStatus: 'Сбросить статус', + statusReset: 'Статус аккаунта сброшен', + failedToResetStatus: 'Не удалось сбросить статус аккаунта', + failedToLoad: 'Не удалось загрузить аккаунты', + failedToRefresh: 'Не удалось обновить токен', + failedToDelete: 'Не удалось удалить аккаунт', + failedToClearRateLimit: 'Не удалось очистить лимит частоты', deleteConfirm: "Удалить '{name}'? Это действие нельзя отменить.", // Create/Edit Account Modal platform: 'Платформа', - accountName: 'Account Name', - enterAccountName: 'Enter account name', - accountType: 'Account Type', + accountName: 'Имя аккаунта', + enterAccountName: 'Введите имя аккаунта', + accountType: 'Тип аккаунта', claudeCode: 'Claude Code', claudeConsole: 'Claude Console', bedrockLabel: 'AWS Bedrock', bedrockDesc: 'SigV4 / API Key', vertexLabel: 'Vertex', vertexDesc: 'Service Account', - vertexAnthropicHint: 'Use a Google Cloud Service Account JSON to call Anthropic Claude via Vertex AI. It is recommended to configure model mapping to map client Claude model names to Vertex model IDs.', - vertexGeminiHint: 'Use a Google Cloud Service Account JSON to access Vertex AI Gemini. It is recommended to place Vertex accounts in a separate group to avoid mixing with AI Studio/Gemini OAuth on the same models.', + vertexAnthropicHint: 'Используйте Google Cloud Service Account JSON для вызова Anthropic Claude через Vertex AI. Рекомендуется настроить model mapping, чтобы сопоставлять клиентские имена моделей Claude с Vertex model IDs.', + vertexGeminiHint: 'Используйте Google Cloud Service Account JSON для доступа к Vertex AI Gemini. Рекомендуется помещать аккаунты Vertex в отдельную группу, чтобы не смешивать их с AI Studio/Gemini OAuth на одних и тех же моделях.', vertexSaJsonLabel: 'Service Account JSON', - vertexSaJsonLoaded: 'Service Account JSON loaded', - vertexSaJsonDrop: 'Drop Service Account JSON here', - vertexSaJsonKeyHidden: 'Key content is not displayed in the form.', - vertexSaJsonDropHint: 'Drag a .json file here, or click the button to select one.', - vertexSaJsonSelectBtn: 'Select JSON', - vertexSaJsonUploadHint: 'After uploading or dropping a JSON file, the project_id will be auto-extracted. Key content is only used for account creation.', - vertexSaJsonEditHint: 'Service Account JSON is not shown on the edit page; to change the JSON, delete the account and recreate it.', - vertexProjectIdPlaceholder: 'Auto-extracted from JSON', - vertexLocationHint: 'Available locations vary by Vertex model. Select the default endpoint location for this account.', - vertexLocationRequired: 'Please enter a Vertex location', - vertexSaJsonMissingFields: 'Service Account JSON is missing project_id, client_email, or private_key', - vertexSaJsonMissingProjectId: 'Service Account JSON is missing project_id', - vertexSaJsonMissingClientEmail: 'Service Account JSON is missing client_email', - vertexSaJsonInvalid: 'Service Account JSON format is invalid', - vertexSaJsonRequired: 'Please upload a Service Account JSON', + vertexSaJsonLoaded: 'Service Account JSON загружен', + vertexSaJsonDrop: 'Перетащите Service Account JSON сюда', + vertexSaJsonKeyHidden: 'Содержимое ключа не отображается в форме.', + vertexSaJsonDropHint: 'Перетащите .json файл сюда или нажмите кнопку, чтобы выбрать файл.', + vertexSaJsonSelectBtn: 'Выбрать JSON', + vertexSaJsonUploadHint: 'После загрузки или перетаскивания JSON-файла project_id будет извлечён автоматически. Содержимое ключа используется только при создании аккаунта.', + vertexSaJsonEditHint: 'Service Account JSON не показывается на странице редактирования; чтобы изменить JSON, удалите аккаунт и создайте его заново.', + vertexProjectIdPlaceholder: 'Автоматически извлекается из JSON', + vertexLocationHint: 'Доступные локации зависят от модели Vertex. Выберите локацию endpoint по умолчанию для этого аккаунта.', + vertexLocationRequired: 'Введите локацию Vertex', + vertexSaJsonMissingFields: 'В Service Account JSON отсутствует project_id, client_email или private_key', + vertexSaJsonMissingProjectId: 'В Service Account JSON отсутствует project_id', + vertexSaJsonMissingClientEmail: 'В Service Account JSON отсутствует client_email', + vertexSaJsonInvalid: 'Неверный формат Service Account JSON', + vertexSaJsonRequired: 'Загрузите Service Account JSON', oauthSetupToken: 'OAuth / Setup Token', - addMethod: 'Add Method', - setupTokenLongLived: 'Setup Token (Long-lived)', + addMethod: 'Добавить способ', + setupTokenLongLived: 'Setup Token (долгоживущий)', baseUrl: 'Base URL', - baseUrlHint: 'Leave default for official Anthropic API', + baseUrlHint: 'Оставьте значение по умолчанию для официального Anthropic API', apiKeyRequired: 'API Key *', apiKeyPlaceholder: 'sk-ant-api03-...', - apiKeyHint: 'Your Claude Console API Key', + apiKeyHint: 'Ваш Claude Console API Key', // OpenAI specific hints openai: { - baseUrlHint: 'Leave default for official OpenAI API', - apiKeyHint: 'Your OpenAI API Key', - oauthPassthrough: 'Auto passthrough (auth only)', + baseUrlHint: 'Оставьте значение по умолчанию для официального OpenAI API', + apiKeyHint: 'Ваш OpenAI API Key', + oauthPassthrough: 'Автоматический passthrough (только auth)', oauthPassthroughDesc: - 'When enabled, this OpenAI account uses automatic passthrough: the gateway forwards request/response as-is and only swaps auth, while keeping billing/concurrency/audit and necessary safety filtering.', + 'Если включено, этот аккаунт OpenAI использует автоматический passthrough: шлюз пересылает request/response как есть и только заменяет auth, сохраняя тарификацию, параллелизм, аудит и необходимую safety-фильтрацию.', responsesWebsocketsV2: 'Responses WebSocket v2', responsesWebsocketsV2Desc: - 'Disabled by default. Enable to allow responses_websockets_v2 capability (still gated by global and account-type switches).', - wsMode: 'WS mode', - wsModeDesc: 'Only applies to the current OpenAI account type.', + 'По умолчанию отключено. Включите, чтобы разрешить capability responses_websockets_v2 (по-прежнему ограничивается глобальными переключателями и переключателями типа аккаунта).', + wsMode: 'Режим WS', + wsModeDesc: 'Применяется только к текущему типу аккаунта OpenAI.', wsModeOff: 'Off (off)', wsModeCtxPool: 'Context Pool (ctx_pool)', wsModePassthrough: 'Passthrough (passthrough)', wsModeShared: 'Shared (shared)', wsModeDedicated: 'Dedicated (dedicated)', wsModeConcurrencyHint: - 'When WS mode is enabled, account concurrency becomes the WS connection pool limit for this account.', - wsModePassthroughHint: 'Passthrough mode does not use the WS connection pool.', + 'Когда режим WS включён, параллелизм аккаунта становится лимитом пула WS-соединений для этого аккаунта.', + wsModePassthroughHint: 'Режим passthrough не использует пул WS-соединений.', oauthResponsesWebsocketsV2: 'OAuth WebSocket Mode', oauthResponsesWebsocketsV2Desc: - 'Only applies to OpenAI OAuth. This account can use OpenAI WebSocket Mode only when enabled.', + 'Применяется только к OpenAI OAuth. Этот аккаунт может использовать OpenAI WebSocket Mode только когда функция включена.', apiKeyResponsesWebsocketsV2: 'API Key WebSocket Mode', apiKeyResponsesWebsocketsV2Desc: - 'Only applies to OpenAI API Key. This account can use OpenAI WebSocket Mode only when enabled.', + 'Применяется только к OpenAI API Key. Этот аккаунт может использовать OpenAI WebSocket Mode только когда функция включена.', responsesWebsocketsV2PassthroughHint: - 'Automatic passthrough is currently enabled: it only affects HTTP passthrough and does not disable WS mode.', - responsesMode: 'Responses API support', + 'Автоматический passthrough сейчас включён: он влияет только на HTTP passthrough и не отключает режим WS.', + responsesMode: 'Поддержка Responses API', responsesModeDesc: - 'Only applies to OpenAI API Key accounts. Auto follows probe results; force modes override probing.', + 'Применяется только к аккаунтам OpenAI API Key. Auto следует результатам probe; принудительные режимы переопределяют probing.', responsesModeAuto: 'Auto', responsesModeForceResponses: 'Force Responses', responsesModeForceChatCompletions: 'Force Chat Completions', @@ -3335,459 +3335,459 @@ export default { responsesStatusAutoUnknown: 'Auto probe: unknown', responsesStatusForcedResponses: 'Forced Responses', responsesStatusForcedChatCompletions: 'Forced Chat Completions', - codexCLIOnly: 'Codex official clients only', + codexCLIOnly: 'Только официальные клиенты Codex', codexCLIOnlyDesc: - 'Only applies to OpenAI OAuth. When enabled, only Codex official client families are allowed; when disabled, the gateway bypasses this restriction and keeps existing behavior.', - codexImageGenerationBridge: 'Codex image-generation bridge', + 'Применяется только к OpenAI OAuth. Если включено, разрешены только семейства официальных клиентов Codex; если отключено, шлюз обходит это ограничение и сохраняет прежнее поведение.', + codexImageGenerationBridge: 'Мост генерации изображений Codex', codexImageGenerationBridgeDesc: - 'Account policy takes precedence over channel and global settings. Only controls whether Codex requests through the /responses text endpoint receive the image_generation tool; standalone image-generation endpoints are unaffected.', - codexImageGenerationBridgeInherit: 'Follow channel', - codexImageGenerationBridgeInheritDesc: 'Do not write an account override; use the channel or global policy.', - codexImageGenerationBridgeEnabled: 'Force on', - codexImageGenerationBridgeEnabledDesc: 'Allow image tool injection for Codex /responses requests.', - codexImageGenerationBridgeDisabled: 'Force off', - codexImageGenerationBridgeDisabledDesc: 'Block image tool injection for Codex /responses requests.', - codexImageGenerationBridgeBadgeInherit: 'Channel policy', - codexImageGenerationBridgeBadgeEnabled: 'Account on', - codexImageGenerationBridgeBadgeDisabled: 'Account off', + 'Политика аккаунта имеет приоритет над настройками канала и глобальными настройками. Управляет только тем, получают ли запросы Codex через текстовый endpoint /responses инструмент image_generation; отдельные endpoint-ы генерации изображений не затрагиваются.', + codexImageGenerationBridgeInherit: 'Следовать каналу', + codexImageGenerationBridgeInheritDesc: 'Не записывать переопределение аккаунта; использовать политику канала или глобальную политику.', + codexImageGenerationBridgeEnabled: 'Принудительно включить', + codexImageGenerationBridgeEnabledDesc: 'Разрешить внедрение image tool для запросов Codex /responses.', + codexImageGenerationBridgeDisabled: 'Принудительно отключить', + codexImageGenerationBridgeDisabledDesc: 'Блокировать внедрение image tool для запросов Codex /responses.', + codexImageGenerationBridgeBadgeInherit: 'Политика канала', + codexImageGenerationBridgeBadgeEnabled: 'Включено на аккаунте', + codexImageGenerationBridgeBadgeDisabled: 'Отключено на аккаунте', compactMode: 'Compact mode', compactModeDesc: - 'Controls how this account participates in /responses/compact routing. Auto follows probe results, Force On always allows, Force Off always excludes.', + 'Управляет участием этого аккаунта в маршрутизации /responses/compact. Auto следует результатам probe, Force On всегда разрешает, Force Off всегда исключает.', compactModeAuto: 'Auto', compactModeForceOn: 'Force On', compactModeForceOff: 'Force Off', - compactModelMapping: 'Compact-only model mapping', + compactModelMapping: 'Model mapping только для Compact', compactModelMappingDesc: - 'Only applies to /responses/compact. Use this when the upstream compact endpoint requires a special compact model.', - compactSupported: 'Compact supported', - compactUnsupported: 'Compact unsupported', + 'Применяется только к /responses/compact. Используйте, когда upstream compact endpoint требует специальную compact-модель.', + compactSupported: 'Compact поддерживается', + compactUnsupported: 'Compact не поддерживается', compactAuto: 'Compact Auto', compactUnknown: 'Compact Auto', - compactLastChecked: 'Last compact probe', - testMode: 'Test mode', - testModeDefault: 'Default request', + compactLastChecked: 'Последний compact probe', + testMode: 'Режим теста', + testModeDefault: 'Запрос по умолчанию', testModeCompact: 'Compact probe', - modelRestrictionDisabledByPassthrough: 'Automatic passthrough is enabled: model whitelist/mapping will not take effect.', + modelRestrictionDisabledByPassthrough: 'Автоматический passthrough включён: whitelist/mapping моделей не будет применяться.', }, anthropic: { - apiKeyPassthrough: 'Auto passthrough (auth only)', + apiKeyPassthrough: 'Автоматический passthrough (только auth)', apiKeyPassthroughDesc: - 'Only applies to Anthropic API Key accounts. When enabled, messages/count_tokens are forwarded in passthrough mode with auth replacement only, while billing/concurrency/audit and safety filtering are preserved. Disable to roll back immediately.', - webSearchEmulation: 'Web Search Emulation', + 'Применяется только к аккаунтам Anthropic API Key. Если включено, messages/count_tokens пересылаются в passthrough-режиме только с заменой auth, при этом тарификация, параллелизм, аудит и safety-фильтрация сохраняются. Отключите, чтобы сразу откатиться.', + webSearchEmulation: 'Эмуляция Web Search', webSearchEmulationDesc: - 'Enable web search emulation for this API Key account. When a pure web_search request is detected, the gateway calls a third-party search API and constructs the response locally. Default follows channel config.', + 'Включите эмуляцию Web Search для этого аккаунта API Key. Когда обнаружен чистый запрос web_search, шлюз вызывает сторонний search API и собирает ответ локально. По умолчанию следует настройке канала.', webSearchDefault: 'По умолчанию', webSearchEnabled: 'Включено', webSearchDisabled: 'Отключено', }, - modelRestriction: 'Model Restriction (Optional)', - modelWhitelist: 'Model Whitelist', + modelRestriction: 'Ограничение моделей (необязательно)', + modelWhitelist: 'Whitelist моделей', modelMapping: 'Model Mapping', - selectAllowedModels: 'Select allowed models. Leave empty to support all models.', + selectAllowedModels: 'Выберите разрешённые модели. Оставьте пустым для поддержки всех моделей.', mapRequestModels: - 'Map request models to actual models. Left is the requested model, right is the actual model sent to API.', - selectedModels: 'Selected {count} model(s)', - supportsAllModels: '(supports all models)', - requestModel: 'Request model', - actualModel: 'Actual model', - addMapping: 'Add Mapping', - mappingExists: 'Mapping for {model} already exists', - wildcardOnlyAtEnd: 'Wildcard * can only be at the end', - targetNoWildcard: 'Target model cannot contain wildcard *', - searchModels: 'Search models...', - noMatchingModels: 'No matching models', - fillRelatedModels: 'Sync latest supported models', - syncUpstreamModels: 'Sync upstream supported models', - syncUpstreamModelsLoading: 'Syncing upstream...', - syncUpstreamModelsSuccess: 'Synced {count} new model(s) from upstream ({total} upstream total)', - syncUpstreamModelsNoChanges: 'All {count} upstream model(s) are already in the whitelist', - syncUpstreamModelsEmpty: 'Upstream returned no models to sync', - syncUpstreamModelsFailed: 'Failed to sync upstream models', - syncUpstreamModelsError: 'Failed to sync upstream models: {message}', - clearAllModels: 'Clear all models', - customModelName: 'Custom model name', - enterCustomModelName: 'Enter custom model name', + 'Сопоставьте запрошенные модели с фактическими. Слева — запрошенная модель, справа — фактическая модель, отправляемая в API.', + selectedModels: 'Выбрано моделей: {count}', + supportsAllModels: '(поддерживает все модели)', + requestModel: 'Запрошенная модель', + actualModel: 'Фактическая модель', + addMapping: 'Добавить mapping', + mappingExists: 'Mapping для {model} уже существует', + wildcardOnlyAtEnd: 'Wildcard * может быть только в конце', + targetNoWildcard: 'Целевая модель не может содержать wildcard *', + searchModels: 'Поиск моделей...', + noMatchingModels: 'Нет подходящих моделей', + fillRelatedModels: 'Синхронизировать последние поддерживаемые модели', + syncUpstreamModels: 'Синхронизировать поддерживаемые upstream-модели', + syncUpstreamModelsLoading: 'Синхронизация upstream...', + syncUpstreamModelsSuccess: 'Синхронизировано новых моделей: {count} (всего upstream: {total})', + syncUpstreamModelsNoChanges: 'Все upstream-модели ({count}) уже есть в whitelist', + syncUpstreamModelsEmpty: 'Upstream не вернул моделей для синхронизации', + syncUpstreamModelsFailed: 'Не удалось синхронизировать upstream-модели', + syncUpstreamModelsError: 'Не удалось синхронизировать upstream-модели: {message}', + clearAllModels: 'Очистить все модели', + customModelName: 'Имя пользовательской модели', + enterCustomModelName: 'Введите имя пользовательской модели', addModel: 'Добавить', - modelExists: 'Model already exists', - modelCount: '{count} models', + modelExists: 'Модель уже существует', + modelCount: 'Моделей: {count}', poolMode: 'Pool Mode', - poolModeHint: 'Enable when upstream is an account pool; errors won\'t mark local account status', + poolModeHint: 'Включите, когда upstream является пулом аккаунтов; ошибки не будут помечать локальный статус аккаунта', poolModeInfo: - 'When enabled, upstream 429/403/401 errors will auto-retry without marking the account as rate-limited or errored. Suitable for upstream pointing to another sub2api instance.', - poolModeRetryCount: 'Same-Account Retries', + 'Если включено, upstream-ошибки 429/403/401 будут автоматически повторяться без пометки аккаунта как ограниченного по лимиту или ошибочного. Подходит для upstream, указывающего на другой экземпляр sub2api.', + poolModeRetryCount: 'Повторы на том же аккаунте', poolModeRetryCountHint: - 'Only applies in pool mode. Use 0 to disable in-place retry. Default {default}, maximum {max}.', - poolModeRetryStatusCodes: 'Retry Status Codes', + 'Применяется только в pool mode. Используйте 0, чтобы отключить повтор на месте. По умолчанию {default}, максимум {max}.', + poolModeRetryStatusCodes: 'HTTP-коды для повтора', poolModeRetryStatusCodesHint: - 'Comma-separated HTTP status codes (100-599) that trigger same-account retry in pool mode. Leave blank to use defaults ({default}).', - customErrorCodes: 'Custom Error Codes', - customErrorCodesHint: 'Only stop scheduling for selected error codes', + 'HTTP status codes (100-599) через запятую, которые запускают повтор на том же аккаунте в pool mode. Оставьте пустым, чтобы использовать значения по умолчанию ({default}).', + customErrorCodes: 'Пользовательские коды ошибок', + customErrorCodesHint: 'Останавливать маршрутизацию только для выбранных кодов ошибок', customErrorCodesWarning: - 'Only selected error codes will stop scheduling. Other errors will return 500.', + 'Только выбранные коды ошибок будут останавливать маршрутизацию. Остальные ошибки вернут 500.', customErrorCodes429Warning: - '429 already has built-in rate limit handling. Adding it to custom error codes will disable the account instead of temporary rate limiting. Are you sure?', + '429 уже имеет встроенную обработку лимита частоты. Добавление его в пользовательские коды ошибок отключит аккаунт вместо временного ограничения по лимиту. Продолжить?', customErrorCodes529Warning: - '529 already has built-in overload handling. Adding it to custom error codes will disable the account instead of temporary overload marking. Are you sure?', - selectedErrorCodes: 'Selected', - noneSelectedUsesDefault: 'None selected (uses default policy)', - enterErrorCode: 'Enter error code (100-599)', - invalidErrorCode: 'Please enter a valid HTTP error code (100-599)', - errorCodeExists: 'This error code is already selected', - interceptWarmupRequests: 'Intercept Warmup Requests', + '529 уже имеет встроенную обработку перегрузки. Добавление его в пользовательские коды ошибок отключит аккаунт вместо временной пометки перегрузки. Продолжить?', + selectedErrorCodes: 'Выбрано', + noneSelectedUsesDefault: 'Ничего не выбрано (используется политика по умолчанию)', + enterErrorCode: 'Введите код ошибки (100-599)', + invalidErrorCode: 'Введите корректный HTTP-код ошибки (100-599)', + errorCodeExists: 'Этот код ошибки уже выбран', + interceptWarmupRequests: 'Перехватывать warmup-запросы', interceptWarmupRequestsDesc: - 'When enabled, warmup requests like title generation will return mock responses without consuming upstream tokens', - autoPauseOnExpired: 'Auto Pause On Expired', - autoPauseOnExpiredDesc: 'When enabled, the account will auto pause scheduling after it expires', + 'Если включено, warmup-запросы вроде генерации заголовка будут возвращать mock-ответы без расхода upstream-токенов', + autoPauseOnExpired: 'Автопауза при истечении срока', + autoPauseOnExpiredDesc: 'Если включено, аккаунт автоматически приостановит маршрутизацию после истечения срока действия', // Quota control (Anthropic OAuth/SetupToken only) quotaControl: { - title: 'Quota Control', - hint: 'Configure cost window, session limits, client affinity and other scheduling controls.', + title: 'Управление квотой', + hint: 'Настройте окно расхода, лимиты сессий, client affinity и другие параметры маршрутизации.', windowCost: { - label: '5h Window Cost Limit', - hint: 'Limit account cost usage within the 5-hour window', - limit: 'Cost Threshold', + label: 'Лимит расхода окна 5h', + hint: 'Ограничьте расход аккаунта в пределах 5-часового окна', + limit: 'Порог расхода', limitPlaceholder: '50', - limitHint: 'Account will not participate in new scheduling after reaching threshold', + limitHint: 'Аккаунт не будет участвовать в новой маршрутизации после достижения порога', stickyReserve: 'Sticky Reserve', stickyReservePlaceholder: '10', - stickyReserveHint: 'Additional reserve for sticky sessions' + stickyReserveHint: 'Дополнительный резерв для sticky-сессий' }, sessionLimit: { - label: 'Session Count Limit', - hint: 'Limit the number of active concurrent sessions', - maxSessions: 'Max Sessions', + label: 'Лимит количества сессий', + hint: 'Ограничьте число активных параллельных сессий', + maxSessions: 'Макс. сессий', maxSessionsPlaceholder: '3', - maxSessionsHint: 'Maximum number of active concurrent sessions', + maxSessionsHint: 'Максимальное число активных параллельных сессий', idleTimeout: 'Idle Timeout', idleTimeoutPlaceholder: '5', - idleTimeoutHint: 'Sessions will be released after idle timeout' + idleTimeoutHint: 'Сессии будут освобождены после idle timeout' }, rpmLimit: { label: 'Лимит RPM', - hint: 'Limit requests per minute to protect upstream accounts', + hint: 'Ограничьте запросы в минуту для защиты upstream-аккаунтов', baseRpm: 'Base RPM', baseRpmPlaceholder: '15', - baseRpmHint: 'Max requests per minute, 0 or empty means no limit', - strategy: 'RPM Strategy', + baseRpmHint: 'Максимум запросов в минуту; 0 или пусто = без лимита', + strategy: 'Стратегия RPM', strategyTiered: 'Tiered Model', strategyStickyExempt: 'Sticky Exempt', - strategyTieredHint: 'Green → Yellow → Sticky only → Blocked, progressive throttling', - strategyStickyExemptHint: 'Only sticky sessions allowed when over limit', - strategyHint: 'Tiered: gradually restrict when exceeded; Sticky Exempt: existing sessions unrestricted', + strategyTieredHint: 'Green → Yellow → только sticky → Blocked, постепенное ограничение', + strategyStickyExemptHint: 'При превышении лимита разрешены только sticky-сессии', + strategyHint: 'Tiered: постепенно ограничивает при превышении; Sticky Exempt: существующие сессии не ограничиваются', stickyBuffer: 'Sticky Buffer', - stickyBufferPlaceholder: 'Default: 20% of base RPM', - stickyBufferHint: 'Extra requests allowed for sticky sessions after exceeding base RPM. Leave empty to use default (20% of base RPM, min 1)', - userMsgQueue: 'User Message Rate Control', - userMsgQueueHint: 'Rate-limit user messages to avoid triggering upstream RPM limits', + stickyBufferPlaceholder: 'По умолчанию: 20% от base RPM', + stickyBufferHint: 'Дополнительные запросы для sticky-сессий после превышения base RPM. Оставьте пустым, чтобы использовать значение по умолчанию (20% от base RPM, минимум 1)', + userMsgQueue: 'Контроль частоты сообщений пользователя', + userMsgQueueHint: 'Ограничивайте частоту сообщений пользователя, чтобы не срабатывать upstream-лимиты RPM', umqModeOff: 'Off', umqModeThrottle: 'Throttle', umqModeSerialize: 'Serialize', }, tlsFingerprint: { - label: 'TLS Fingerprint Simulation', - hint: 'Simulate Node.js/Claude Code client TLS fingerprint', - defaultProfile: 'Built-in Default', - randomProfile: 'Random' + label: 'Симуляция TLS fingerprint', + hint: 'Симулирует TLS fingerprint клиента Node.js/Claude Code', + defaultProfile: 'Встроенный профиль по умолчанию', + randomProfile: 'Случайный' }, sessionIdMasking: { - label: 'Session ID Masking', - hint: 'When enabled, fixes the session ID in metadata.user_id for 15 minutes, making upstream think requests come from the same session' + label: 'Маскирование Session ID', + hint: 'Если включено, фиксирует session ID в metadata.user_id на 15 минут, чтобы upstream считал запросы идущими из одной сессии' }, cacheTTLOverride: { - label: 'Cache TTL Override', - hint: 'Force all cache creation tokens to be billed as the selected TTL tier (5m or 1h)', - target: 'Target TTL', - targetHint: 'Select the TTL tier for billing' + label: 'Переопределение Cache TTL', + hint: 'Принудительно тарифицировать все токены создания cache по выбранному уровню TTL (5m или 1h)', + target: 'Целевой TTL', + targetHint: 'Выберите уровень TTL для тарификации' }, customBaseUrl: { - label: 'Custom Relay URL', - hint: 'Forward requests to a custom relay service. Proxy URL will be passed as a query parameter.', + label: 'Пользовательский Relay URL', + hint: 'Пересылать запросы в пользовательский relay-сервис. Proxy URL будет передан как query-параметр.', urlHint: 'Relay service URL (e.g., https://relay.example.com)', }, clientAffinity: { - label: 'Client Affinity Scheduling', - hint: 'When enabled, new sessions prefer accounts previously used by this client to reduce account switching' + label: 'Маршрутизация Client Affinity', + hint: 'Если включено, новые сессии предпочитают аккаунты, которые этот клиент использовал раньше, чтобы сократить переключения аккаунтов' } }, - affinityNoClients: 'No affinity clients', - affinityClients: '{count} affinity clients:', + affinityNoClients: 'Нет affinity-клиентов', + affinityClients: 'Affinity-клиентов: {count}', affinitySection: 'Client Affinity', - affinitySectionHint: 'Control how clients are distributed across accounts. Configure zone thresholds to balance load.', - affinityToggle: 'Enable Client Affinity', - affinityToggleHint: 'New sessions prefer accounts previously used by this client', - affinityBase: 'Base Limit (Green Zone)', - affinityBasePlaceholder: 'Empty = no limit', - affinityBaseHint: 'Max clients in green zone (full priority scheduling)', - affinityBaseOffHint: 'No green zone limit. All clients receive full priority scheduling.', + affinitySectionHint: 'Управляйте распределением клиентов по аккаунтам. Настройте пороги зон для балансировки нагрузки.', + affinityToggle: 'Включить Client Affinity', + affinityToggleHint: 'Новые сессии предпочитают аккаунты, которые этот клиент использовал раньше', + affinityBase: 'Базовый лимит (Green Zone)', + affinityBasePlaceholder: 'Пусто = без лимита', + affinityBaseHint: 'Максимум клиентов в green zone (полный приоритет маршрутизации)', + affinityBaseOffHint: 'Нет лимита green zone. Все клиенты получают полный приоритет маршрутизации.', affinityBuffer: 'Buffer (Yellow Zone)', affinityBufferPlaceholder: 'e.g. 3', - affinityBufferHint: 'Additional clients allowed in the yellow zone (degraded priority)', + affinityBufferHint: 'Дополнительные клиенты, разрешённые в yellow zone (пониженный приоритет)', affinityBufferInfinite: 'Безлимитно', expired: 'Истекла', proxy: 'Proxy', - noProxy: 'No Proxy', + noProxy: 'Без proxy', concurrency: 'Параллелизм', - loadFactor: 'Load Factor', - loadFactorHint: 'Higher load factor increases scheduling frequency', - priority: 'Priority', - priorityHint: 'Lower value accounts are used first', - billingRateMultiplier: 'Billing Rate Multiplier', - billingRateMultiplierHint: '0 = free, affects account billing only', + loadFactor: 'Коэффициент нагрузки', + loadFactorHint: 'Более высокий коэффициент нагрузки увеличивает частоту маршрутизации', + priority: 'Приоритет', + priorityHint: 'Аккаунты с меньшим значением используются первыми', + billingRateMultiplier: 'Тарифный коэффициент', + billingRateMultiplierHint: '0 = бесплатно, влияет только на тарификацию аккаунта', expiresAt: 'Истекает', - expiresAtHint: 'Leave empty for no expiration', - higherPriorityFirst: 'Lower value means higher priority', - mixedScheduling: 'Use in /v1/messages', - mixedSchedulingHint: 'Enable to participate in Anthropic/Gemini group scheduling', + expiresAtHint: 'Оставьте пустым, если срок действия не ограничен', + higherPriorityFirst: 'Меньшее значение означает более высокий приоритет', + mixedScheduling: 'Использовать в /v1/messages', + mixedSchedulingHint: 'Включите для участия в маршрутизации группы Anthropic/Gemini', mixedSchedulingTooltip: - '!! WARNING !! Antigravity Claude and Anthropic Claude cannot be used in the same context. If you have both Anthropic and Antigravity accounts, enabling this option will cause frequent 400 errors. When enabled, please use the group feature to isolate Antigravity accounts from Anthropic accounts. Make sure you understand this before enabling!!', + '!! ПРЕДУПРЕЖДЕНИЕ !! Antigravity Claude и Anthropic Claude нельзя использовать в одном контексте. Если у вас есть аккаунты Anthropic и Antigravity, включение этой опции приведёт к частым ошибкам 400. После включения используйте группы, чтобы изолировать аккаунты Antigravity от аккаунтов Anthropic. Убедитесь, что понимаете это перед включением!!', aiCreditsBalance: 'AI Credits', - allowOverages: 'Allow Overages (AI Credits)', + allowOverages: 'Разрешить перерасход (AI Credits)', allowOveragesTooltip: - 'Only use AI Credits after free quota is explicitly exhausted. Ordinary concurrent 429 rate limits will not switch to overages.', - creating: 'Creating...', + 'Использовать AI Credits только после явного исчерпания бесплатной квоты. Обычные параллельные лимиты 429 не будут переключаться на перерасход.', + creating: 'Создание...', updating: 'Обновление...', - accountCreated: 'Account created successfully', - accountUpdated: 'Account updated successfully', - failedToCreate: 'Failed to create account', - failedToUpdate: 'Failed to update account', - pleaseSelectStatus: 'Please select a valid account status', - mixedChannelWarningTitle: 'Mixed Channel Warning', - mixedChannelWarning: 'Warning: Group "{groupName}" contains both {currentPlatform} and {otherPlatform} accounts. Mixing different channels may cause thinking block signature validation issues, which will fallback to non-thinking mode. Are you sure you want to continue?', - pleaseEnterAccountName: 'Please enter account name', - pleaseEnterApiKey: 'Please enter API Key', + accountCreated: 'Аккаунт создан', + accountUpdated: 'Аккаунт обновлён', + failedToCreate: 'Не удалось создать аккаунт', + failedToUpdate: 'Не удалось обновить аккаунт', + pleaseSelectStatus: 'Выберите корректный статус аккаунта', + mixedChannelWarningTitle: 'Предупреждение о смешанных каналах', + mixedChannelWarning: 'Предупреждение: группа "{groupName}" содержит аккаунты {currentPlatform} и {otherPlatform}. Смешивание разных каналов может вызвать проблемы проверки подписи thinking block, что приведёт к fallback в non-thinking mode. Продолжить?', + pleaseEnterAccountName: 'Введите имя аккаунта', + pleaseEnterApiKey: 'Введите API Key', bedrockAccessKeyId: 'AWS Access Key ID', bedrockSecretAccessKey: 'AWS Secret Access Key', bedrockSessionToken: 'AWS Session Token', bedrockRegion: 'AWS Region', bedrockRegionHint: 'e.g. us-east-1, us-west-2, eu-west-1', - bedrockForceGlobal: 'Force Global cross-region inference', - bedrockForceGlobalHint: 'When enabled, model IDs use the global. prefix (e.g. global.anthropic.claude-...), routing requests to any supported region worldwide for higher availability', - bedrockAccessKeyIdRequired: 'Please enter AWS Access Key ID', - bedrockSecretAccessKeyRequired: 'Please enter AWS Secret Access Key', - bedrockRegionRequired: 'Please select AWS Region', - bedrockSessionTokenHint: 'Optional, for temporary credentials', - bedrockSecretKeyLeaveEmpty: 'Leave empty to keep current key', - bedrockAuthMode: 'Authentication Mode', + bedrockForceGlobal: 'Принудительный global cross-region inference', + bedrockForceGlobalHint: 'Если включено, model IDs используют префикс global. (например, global.anthropic.claude-...), направляя запросы в любой поддерживаемый регион по всему миру для более высокой доступности', + bedrockAccessKeyIdRequired: 'Введите AWS Access Key ID', + bedrockSecretAccessKeyRequired: 'Введите AWS Secret Access Key', + bedrockRegionRequired: 'Выберите AWS Region', + bedrockSessionTokenHint: 'Необязательно, для временных учётных данных', + bedrockSecretKeyLeaveEmpty: 'Оставьте пустым, чтобы сохранить текущий ключ', + bedrockAuthMode: 'Режим аутентификации', bedrockAuthModeSigv4: 'SigV4 Signing', bedrockAuthModeApikey: 'Bedrock API Key', bedrockApiKeyLabel: 'Bedrock API Key', bedrockApiKeyDesc: 'Bearer Token', bedrockApiKeyInput: 'API-ключ', - bedrockApiKeyRequired: 'Please enter Bedrock API Key', - bedrockApiKeyLeaveEmpty: 'Leave empty to keep current key', - apiKeyIsRequired: 'API Key is required', - leaveEmptyToKeep: 'Leave empty to keep current key', + bedrockApiKeyRequired: 'Введите Bedrock API Key', + bedrockApiKeyLeaveEmpty: 'Оставьте пустым, чтобы сохранить текущий ключ', + apiKeyIsRequired: 'API Key обязателен', + leaveEmptyToKeep: 'Оставьте пустым, чтобы сохранить текущий ключ', // Upstream type upstream: { baseUrl: 'Upstream Base URL', - baseUrlHint: 'The address of the upstream Antigravity service, e.g., https://cloudcode-pa.googleapis.com', + baseUrlHint: 'Адрес upstream-сервиса Antigravity, например https://cloudcode-pa.googleapis.com', apiKey: 'Upstream API Key', - apiKeyHint: 'API Key for the upstream service', - pleaseEnterBaseUrl: 'Please enter upstream Base URL', - pleaseEnterApiKey: 'Please enter upstream API Key' + apiKeyHint: 'API Key для upstream-сервиса', + pleaseEnterBaseUrl: 'Введите upstream Base URL', + pleaseEnterApiKey: 'Введите upstream API Key' }, // OAuth flow oauth: { - title: 'Claude Account Authorization', - authMethod: 'Authorization Method', - manualAuth: 'Manual Authorization', - cookieAutoAuth: 'Cookie Auto-Auth', + title: 'Авторизация аккаунта Claude', + authMethod: 'Способ авторизации', + manualAuth: 'Ручная авторизация', + cookieAutoAuth: 'Автоавторизация через cookie', cookieAutoAuthDesc: - 'Use claude.ai sessionKey to automatically complete OAuth authorization without manually opening browser.', + 'Используйте sessionKey claude.ai, чтобы автоматически завершить OAuth-авторизацию без ручного открытия браузера.', sessionKey: 'sessionKey', - keysCount: '{count} keys', - batchCreateAccounts: 'Will batch create {count} accounts', + keysCount: 'Ключей: {count}', + batchCreateAccounts: 'Будет массово создано аккаунтов: {count}', sessionKeyPlaceholder: - 'One sessionKey per line, e.g.:\nsk-ant-sid01-xxxxx...\nsk-ant-sid01-yyyyy...', + 'Один sessionKey на строку, например:\nsk-ant-sid01-xxxxx...\nsk-ant-sid01-yyyyy...', sessionKeyPlaceholderSingle: 'sk-ant-sid01-xxxxx...', - howToGetSessionKey: 'How to get sessionKey', - step1: 'Login to claude.ai in your browser', - step2: 'Press F12 to open Developer Tools', - step3: 'Go to Application tab', - step4: 'Find Cookies → https://claude.ai', - step5: 'Find the row with key sessionKey', - step6: 'Copy the Value', - sessionKeyFormat: 'sessionKey usually starts with sk-ant-sid01-', - startAutoAuth: 'Start Auto-Auth', - authorizing: 'Authorizing...', - followSteps: 'Follow these steps to authorize your Claude account:', - step1GenerateUrl: 'Click the button below to generate the authorization URL', - generateAuthUrl: 'Generate Auth URL', - generating: 'Generating...', - regenerate: 'Regenerate', - step2OpenUrl: 'Open the URL in your browser and complete authorization', + howToGetSessionKey: 'Как получить sessionKey', + step1: 'Войдите в claude.ai в браузере', + step2: 'Нажмите F12, чтобы открыть Developer Tools', + step3: 'Перейдите на вкладку Application', + step4: 'Найдите Cookies → https://claude.ai', + step5: 'Найдите строку с ключом sessionKey', + step6: 'Скопируйте Value', + sessionKeyFormat: 'sessionKey обычно начинается с sk-ant-sid01-', + startAutoAuth: 'Начать автоавторизацию', + authorizing: 'Авторизация...', + followSteps: 'Выполните шаги для авторизации аккаунта Claude:', + step1GenerateUrl: 'Нажмите кнопку ниже, чтобы сгенерировать URL авторизации', + generateAuthUrl: 'Сгенерировать Auth URL', + generating: 'Генерация...', + regenerate: 'Сгенерировать заново', + step2OpenUrl: 'Откройте URL в браузере и завершите авторизацию', openUrlDesc: - 'Open the authorization URL in a new tab, log in to your Claude account and authorize.', + 'Откройте URL авторизации в новой вкладке, войдите в аккаунт Claude и авторизуйте доступ.', proxyWarning: - 'Note: If you configured a proxy, make sure your browser uses the same proxy to access the authorization page.', - step3EnterCode: 'Enter the Authorization Code', + 'Примечание: если настроен proxy, убедитесь, что браузер использует тот же proxy для доступа к странице авторизации.', + step3EnterCode: 'Введите Authorization Code', authCodeDesc: - 'After authorization is complete, the page will display an Authorization Code. Copy and paste it below:', + 'После завершения авторизации страница покажет Authorization Code. Скопируйте и вставьте его ниже:', authCode: 'Authorization Code', - authCodePlaceholder: 'Paste the Authorization Code from Claude page...', - authCodeHint: 'Paste the Authorization Code copied from the Claude page', - completeAuth: 'Complete Authorization', + authCodePlaceholder: 'Вставьте Authorization Code со страницы Claude...', + authCodeHint: 'Вставьте Authorization Code, скопированный со страницы Claude', + completeAuth: 'Завершить авторизацию', verifying: 'Проверка...', - pleaseEnterSessionKey: 'Please enter at least one valid sessionKey', - authFailed: 'Authorization failed', - cookieAuthFailed: 'Cookie authorization failed', - keyAuthFailed: 'Key {index}: {error}', - successCreated: 'Successfully created {count} account(s)', - batchSuccess: 'Successfully created {count} account(s)', - batchPartialSuccess: 'Partial success: {success} succeeded, {failed} failed', - batchFailed: 'Batch creation failed', + pleaseEnterSessionKey: 'Введите хотя бы один корректный sessionKey', + authFailed: 'Авторизация не удалась', + cookieAuthFailed: 'Cookie-авторизация не удалась', + keyAuthFailed: 'Ключ {index}: {error}', + successCreated: 'Создано аккаунтов: {count}', + batchSuccess: 'Создано аккаунтов: {count}', + batchPartialSuccess: 'Частичный успех: успешно {success}, ошибок {failed}', + batchFailed: 'Массовое создание не удалось', // OpenAI specific openai: { - title: 'OpenAI Account Authorization', - followSteps: 'Follow these steps to complete OpenAI account authorization:', - step1GenerateUrl: 'Click the button below to generate the authorization URL', - generateAuthUrl: 'Generate Auth URL', - step2OpenUrl: 'Open the URL in your browser and complete authorization', + title: 'Авторизация аккаунта OpenAI', + followSteps: 'Выполните шаги для авторизации аккаунта OpenAI:', + step1GenerateUrl: 'Нажмите кнопку ниже, чтобы сгенерировать URL авторизации', + generateAuthUrl: 'Сгенерировать Auth URL', + step2OpenUrl: 'Откройте URL в браузере и завершите авторизацию', openUrlDesc: - 'Open the authorization URL in a new tab, log in to your OpenAI account and authorize.', + 'Откройте URL авторизации в новой вкладке, войдите в аккаунт OpenAI и авторизуйте доступ.', importantNotice: - 'Important: The page may take a while to load after authorization. Please wait patiently. When the browser address bar changes to http://localhost..., the authorization is complete.', - step3EnterCode: 'Enter Authorization URL or Code', + 'Важно: после авторизации страница может загружаться некоторое время. Подождите. Когда адресная строка браузера изменится на http://localhost..., авторизация завершена.', + step3EnterCode: 'Введите Authorization URL или код', authCodeDesc: - 'After authorization is complete, when the page URL becomes http://localhost:xxx/auth/callback?code=...:', - authCode: 'Authorization URL or Code', + 'После завершения авторизации, когда URL страницы станет http://localhost:xxx/auth/callback?code=...:', + authCode: 'Authorization URL или код', authCodePlaceholder: - 'Option 1: Copy the complete URL\n(http://localhost:xxx/auth/callback?code=...)\nOption 2: Copy only the code parameter value', + 'Вариант 1: скопируйте полный URL\n(http://localhost:xxx/auth/callback?code=...)\nВариант 2: скопируйте только значение параметра code', authCodeHint: - 'You can copy the entire URL or just the code parameter value, the system will auto-detect', - failedToGenerateUrl: 'Failed to generate OpenAI auth URL', - failedToExchangeCode: 'Failed to exchange OpenAI auth code', - failedToValidateRT: 'Failed to validate refresh token', + 'Можно скопировать весь URL или только значение параметра code — система определит это автоматически', + failedToGenerateUrl: 'Не удалось сгенерировать auth URL OpenAI', + failedToExchangeCode: 'Не удалось обменять auth code OpenAI', + failedToValidateRT: 'Не удалось проверить refresh token', errors: { OPENAI_OAUTH_PROXY_REQUIRED: - 'No proxy is configured and this server could not reach OpenAI directly, so the OpenAI OAuth request failed. Select a proxy that can access OpenAI and retry; if the authorization code has expired, regenerate the authorization URL.' + 'Proxy не настроен, и этот сервер не смог напрямую подключиться к OpenAI, поэтому запрос OpenAI OAuth не удался. Выберите proxy с доступом к OpenAI и повторите попытку; если код авторизации истёк, сгенерируйте URL авторизации заново.' }, // Refresh Token auth - refreshTokenAuth: 'Manual RT Input', - refreshTokenDesc: 'Enter your existing OpenAI Refresh Token(s). Supports batch input (one per line). The system will automatically validate and create accounts.', - refreshTokenPlaceholder: 'Paste your OpenAI Refresh Token...\nSupports multiple, one per line', - codexSessionAuth: 'Codex JSON / AT Batch Input', - codexSessionDesc: 'Paste Codex JSON or an accessToken. Accounts use the step 1 settings.', - codexSessionInputLabel: 'Codex JSON or accessToken', - codexSessionPlaceholder: 'Multiple lines supported, one token or JSON per line', - codexSessionHint: 'sessionToken will not be saved as refresh_token. Without refresh_token, the account expires with the accessToken expiry; import is rejected if the expiry cannot be parsed and step 1 has no expiration.', - codexSessionImportAndCreate: 'Import & Create Account', - codexSessionEmpty: 'Please enter Codex JSON or accessToken', - codexSessionImportFailed: 'Failed to import Codex account', - codexSessionImportSuccess: 'Import completed: created {created}, updated {updated}, skipped {skipped}', - codexSessionImportPartial: 'Partial success: created {created}, updated {updated}, skipped {skipped}, failed {failed}', - sessionTokenAuth: 'Manual ST Input', - sessionTokenDesc: 'Enter your existing Session Token(s). Supports batch input (one per line). The system will automatically validate and create accounts.', - sessionTokenPlaceholder: 'Paste your Session Token...\nSupports multiple, one per line', - sessionTokenRawLabel: 'Raw Input', - sessionTokenRawPlaceholder: 'Paste /api/auth/session raw payload or Session Token...', - sessionTokenRawHint: 'You can paste full JSON. The system will auto-parse ST and AT.', - openSessionUrl: 'Open Fetch URL', - copySessionUrl: 'Copy URL', - sessionUrlHint: 'This URL usually returns AT. If sessionToken is absent, copy __Secure-next-auth.session-token from browser cookies as ST.', - parsedSessionTokensLabel: 'Parsed ST', - parsedSessionTokensEmpty: 'No ST parsed. Please check your input.', - parsedAccessTokensLabel: 'Parsed AT', - validating: 'Validating...', - validateAndCreate: 'Validate & Create Account', - pleaseEnterRefreshToken: 'Please enter Refresh Token', - pleaseEnterSessionToken: 'Please enter Session Token' + refreshTokenAuth: 'Ручной ввод RT', + refreshTokenDesc: 'Введите существующие OpenAI Refresh Token. Поддерживается массовый ввод (по одному на строку). Система автоматически проверит их и создаст аккаунты.', + refreshTokenPlaceholder: 'Вставьте OpenAI Refresh Token...\nПоддерживается несколько, по одному на строку', + codexSessionAuth: 'Массовый ввод Codex JSON / AT', + codexSessionDesc: 'Вставьте Codex JSON или accessToken. Аккаунты используют настройки шага 1.', + codexSessionInputLabel: 'Codex JSON или accessToken', + codexSessionPlaceholder: 'Поддерживается несколько строк: один токен или JSON на строку', + codexSessionHint: 'sessionToken не будет сохранён как refresh_token. Без refresh_token срок действия аккаунта заканчивается вместе со сроком действия accessToken; импорт будет отклонён, если срок действия нельзя разобрать и на шаге 1 не задан срок истечения.', + codexSessionImportAndCreate: 'Импортировать и создать аккаунт', + codexSessionEmpty: 'Введите Codex JSON или accessToken', + codexSessionImportFailed: 'Не удалось импортировать аккаунт Codex', + codexSessionImportSuccess: 'Импорт завершён: создано {created}, обновлено {updated}, пропущено {skipped}', + codexSessionImportPartial: 'Частичный успех: создано {created}, обновлено {updated}, пропущено {skipped}, ошибок {failed}', + sessionTokenAuth: 'Ручной ввод ST', + sessionTokenDesc: 'Введите существующие Session Token. Поддерживается массовый ввод (по одному на строку). Система автоматически проверит их и создаст аккаунты.', + sessionTokenPlaceholder: 'Вставьте Session Token...\nПоддерживается несколько, по одному на строку', + sessionTokenRawLabel: 'Raw-ввод', + sessionTokenRawPlaceholder: 'Вставьте raw payload /api/auth/session или Session Token...', + sessionTokenRawHint: 'Можно вставить полный JSON. Система автоматически разберёт ST и AT.', + openSessionUrl: 'Открыть Fetch URL', + copySessionUrl: 'Скопировать URL', + sessionUrlHint: 'Этот URL обычно возвращает AT. Если sessionToken отсутствует, скопируйте __Secure-next-auth.session-token из cookies браузера как ST.', + parsedSessionTokensLabel: 'Разобранный ST', + parsedSessionTokensEmpty: 'ST не разобран. Проверьте ввод.', + parsedAccessTokensLabel: 'Разобранный AT', + validating: 'Проверка...', + validateAndCreate: 'Проверить и создать аккаунт', + pleaseEnterRefreshToken: 'Введите Refresh Token', + pleaseEnterSessionToken: 'Введите Session Token' }, // Gemini specific gemini: { - title: 'Gemini Account Authorization', - followSteps: 'Follow these steps to authorize your Gemini account:', - step1GenerateUrl: 'Generate the authorization URL', - generateAuthUrl: 'Generate Auth URL', - projectIdLabel: 'Project ID (optional)', + title: 'Авторизация аккаунта Gemini', + followSteps: 'Выполните шаги для авторизации аккаунта Gemini:', + step1GenerateUrl: 'Сгенерируйте URL авторизации', + generateAuthUrl: 'Сгенерировать Auth URL', + projectIdLabel: 'Project ID (необязательно)', projectIdPlaceholder: 'e.g. my-gcp-project or cloud-ai-companion-xxxxx', projectIdHint: - 'Leave empty to auto-detect after code exchange. If auto-detection fails, fill it in and re-generate the auth URL to try again.', - howToGetProjectId: 'How to get', - step2OpenUrl: 'Open the URL in your browser and complete authorization', + 'Оставьте пустым для автоопределения после обмена кода. Если автоопределение не сработает, заполните поле и заново сгенерируйте auth URL.', + howToGetProjectId: 'Как получить', + step2OpenUrl: 'Откройте URL в браузере и завершите авторизацию', openUrlDesc: - 'Open the authorization URL in a new tab, log in to your Google account and authorize.', - step3EnterCode: 'Enter Authorization URL or Code', + 'Откройте URL авторизации в новой вкладке, войдите в аккаунт Google и авторизуйте доступ.', + step3EnterCode: 'Введите Authorization URL или код', authCodeDesc: - 'After authorization, copy the callback URL (recommended) or just the code and paste it below.', - authCode: 'Callback URL or Code', + 'После авторизации скопируйте callback URL (рекомендуется) или только код и вставьте ниже.', + authCode: 'Callback URL или код', authCodePlaceholder: - 'Option 1 (recommended): Paste the callback URL\nOption 2: Paste only the code value', - authCodeHint: 'The system will auto-extract code/state from the URL.', + 'Вариант 1 (рекомендуется): вставьте callback URL\nВариант 2: вставьте только значение кода', + authCodeHint: 'Система автоматически извлечёт code/state из URL.', redirectUri: 'Redirect URI', redirectUriHint: - 'This must be configured in your Google OAuth client and must match exactly.', + 'Это должно быть настроено в вашем Google OAuth client и совпадать точно.', confirmRedirectUri: - 'I have configured this Redirect URI in the Google OAuth client (must match exactly)', - invalidRedirectUri: 'Redirect URI must be a valid http(s) URL', - redirectUriNotConfirmed: 'Please confirm the Redirect URI is configured correctly', - missingRedirectUri: 'Missing redirect URI', - failedToGenerateUrl: 'Failed to generate Gemini auth URL', - missingExchangeParams: 'Missing auth code, session ID, or state', - failedToExchangeCode: 'Failed to exchange Gemini auth code', - missingProjectId: 'GCP Project ID retrieval failed: Your Google account is not linked to an active GCP project. Please activate GCP and bind a credit card in Google Cloud Console, or manually enter the Project ID during authorization.', - modelPassthrough: 'Gemini Model Passthrough', + 'Я настроил этот Redirect URI в Google OAuth client (должен совпадать точно)', + invalidRedirectUri: 'Redirect URI должен быть корректным http(s) URL', + redirectUriNotConfirmed: 'Подтвердите, что Redirect URI настроен корректно', + missingRedirectUri: 'Отсутствует redirect URI', + failedToGenerateUrl: 'Не удалось сгенерировать auth URL Gemini', + missingExchangeParams: 'Отсутствует auth code, session ID или state', + failedToExchangeCode: 'Не удалось обменять auth code Gemini', + missingProjectId: 'Не удалось получить GCP Project ID: ваш аккаунт Google не связан с активным проектом GCP. Активируйте GCP и привяжите банковскую карту в Google Cloud Console или вручную введите Project ID при авторизации.', + modelPassthrough: 'Passthrough моделей Gemini', modelPassthroughDesc: - 'All model requests are forwarded directly to the Gemini API without model restrictions or mappings.', - stateWarningTitle: 'Note', - stateWarningDesc: 'Recommended: paste the full callback URL (includes code & state).', - oauthTypeLabel: 'OAuth Type', - needsProjectId: 'Built-in OAuth (Code Assist)', - needsProjectIdDesc: 'Requires GCP project and Project ID', - noProjectIdNeeded: 'Custom OAuth (AI Studio)', - noProjectIdNeededDesc: 'Requires admin-configured OAuth client', - aiStudioNotConfiguredShort: 'Not configured', + 'Все запросы моделей пересылаются напрямую в Gemini API без ограничений моделей и mappings.', + stateWarningTitle: 'Примечание', + stateWarningDesc: 'Рекомендуется вставить полный callback URL (содержит code и state).', + oauthTypeLabel: 'Тип OAuth', + needsProjectId: 'Встроенный OAuth (Code Assist)', + needsProjectIdDesc: 'Требуется проект GCP и Project ID', + noProjectIdNeeded: 'Пользовательский OAuth (AI Studio)', + noProjectIdNeededDesc: 'Требуется OAuth client, настроенный админом', + aiStudioNotConfiguredShort: 'Не настроено', aiStudioNotConfiguredTip: - 'AI Studio OAuth is not configured: set GEMINI_OAUTH_CLIENT_ID / GEMINI_OAUTH_CLIENT_SECRET and add Redirect URI: http://localhost:1455/auth/callback (Consent screen scopes must include https://www.googleapis.com/auth/generative-language.retriever)', + 'AI Studio OAuth не настроен: задайте GEMINI_OAUTH_CLIENT_ID / GEMINI_OAUTH_CLIENT_SECRET и добавьте Redirect URI: http://localhost:1455/auth/callback (scopes экрана согласия должны включать https://www.googleapis.com/auth/generative-language.retriever)', aiStudioNotConfigured: - 'AI Studio OAuth is not configured: set GEMINI_OAUTH_CLIENT_ID / GEMINI_OAUTH_CLIENT_SECRET and add Redirect URI: http://localhost:1455/auth/callback' + 'AI Studio OAuth не настроен: задайте GEMINI_OAUTH_CLIENT_ID / GEMINI_OAUTH_CLIENT_SECRET и добавьте Redirect URI: http://localhost:1455/auth/callback' }, // Antigravity specific antigravity: { - title: 'Antigravity Account Authorization', - followSteps: 'Follow these steps to authorize your Antigravity account:', - step1GenerateUrl: 'Generate the authorization URL', - generateAuthUrl: 'Generate Auth URL', - step2OpenUrl: 'Open the URL in your browser and complete authorization', - openUrlDesc: 'Open the authorization URL in a new tab, log in to your Google account and authorize.', + title: 'Авторизация аккаунта Antigravity', + followSteps: 'Выполните шаги для авторизации аккаунта Antigravity:', + step1GenerateUrl: 'Сгенерируйте URL авторизации', + generateAuthUrl: 'Сгенерировать Auth URL', + step2OpenUrl: 'Откройте URL в браузере и завершите авторизацию', + openUrlDesc: 'Откройте URL авторизации в новой вкладке, войдите в аккаунт Google и авторизуйте доступ.', importantNotice: - 'Important: The page may take a while to load after authorization. Please wait patiently. When the browser address bar shows http://localhost..., authorization is complete.', - step3EnterCode: 'Enter Authorization URL or Code', + 'Важно: после авторизации страница может загружаться некоторое время. Подождите. Когда адресная строка браузера покажет http://localhost..., авторизация завершена.', + step3EnterCode: 'Введите Authorization URL или код', authCodeDesc: - 'After authorization, when the page URL becomes http://localhost:xxx/auth/callback?code=...:', - authCode: 'Authorization URL or Code', + 'После авторизации, когда URL страницы станет http://localhost:xxx/auth/callback?code=...:', + authCode: 'Authorization URL или код', authCodePlaceholder: - 'Option 1: Copy the complete URL\n(http://localhost:xxx/auth/callback?code=...)\nOption 2: Copy only the code parameter value', - authCodeHint: 'You can copy the entire URL or just the code parameter value, the system will auto-detect', - failedToGenerateUrl: 'Failed to generate Antigravity auth URL', - missingExchangeParams: 'Missing code, session ID, or state', - failedToExchangeCode: 'Failed to exchange Antigravity auth code', + 'Вариант 1: скопируйте полный URL\n(http://localhost:xxx/auth/callback?code=...)\nВариант 2: скопируйте только значение параметра code', + authCodeHint: 'Можно скопировать весь URL или только значение параметра code — система определит это автоматически', + failedToGenerateUrl: 'Не удалось сгенерировать auth URL Antigravity', + missingExchangeParams: 'Отсутствует code, session ID или state', + failedToExchangeCode: 'Не удалось обменять auth code Antigravity', // Refresh Token auth - refreshTokenAuth: 'Manual RT', - refreshTokenDesc: 'Enter your existing Antigravity Refresh Token. Supports batch input (one per line). The system will automatically validate and create accounts.', - refreshTokenPlaceholder: 'Paste your Antigravity Refresh Token...\nSupports multiple tokens, one per line', - validating: 'Validating...', - validateAndCreate: 'Validate & Create', - pleaseEnterRefreshToken: 'Please enter Refresh Token', - failedToValidateRT: 'Failed to validate Refresh Token' + refreshTokenAuth: 'Ручной RT', + refreshTokenDesc: 'Введите существующий Antigravity Refresh Token. Поддерживается массовый ввод (по одному на строку). Система автоматически проверит их и создаст аккаунты.', + refreshTokenPlaceholder: 'Вставьте Antigravity Refresh Token...\nПоддерживается несколько токенов, по одному на строку', + validating: 'Проверка...', + validateAndCreate: 'Проверить и создать', + pleaseEnterRefreshToken: 'Введите Refresh Token', + failedToValidateRT: 'Не удалось проверить Refresh Token' } }, // Gemini specific (platform-wide) gemini: { - helpButton: 'Help', + helpButton: 'Справка', helpDialog: { - title: 'Gemini Usage Guide', - apiKeySection: 'API Key Links' + title: 'Руководство по использованию Gemini', + apiKeySection: 'Ссылки API Key' }, - modelPassthrough: 'Gemini Model Passthrough', + modelPassthrough: 'Passthrough моделей Gemini', modelPassthroughDesc: - 'All model requests are forwarded directly to the Gemini API without model restrictions or mappings.', - baseUrlHint: 'Leave default for official Gemini API', - apiKeyHint: 'Your Gemini API Key (starts with AIza)', + 'Все запросы моделей пересылаются напрямую в Gemini API без ограничений моделей и mappings.', + baseUrlHint: 'Оставьте значение по умолчанию для официального Gemini API', + apiKeyHint: 'Ваш Gemini API Key (начинается с AIza)', tier: { - label: 'Account Tier', - hint: 'Tip: The system will try to auto-detect the tier first; if auto-detection is unavailable or fails, your selected tier is used as a fallback (simulated quota).', + label: 'Уровень аккаунта', + hint: 'Подсказка: система сначала попробует автоматически определить уровень. Если автоопределение недоступно или не сработает, выбранный уровень используется как fallback (симулированная квота).', aiStudioHint: - 'AI Studio quotas are per-model (Pro/Flash are limited independently). If billing is enabled, choose Pay-as-you-go.', + 'Квоты AI Studio задаются по моделям (Pro/Flash ограничиваются независимо). Если billing включён, выберите Pay-as-you-go.', googleOne: { free: 'Google One Free', pro: 'Google One Pro', @@ -3804,63 +3804,63 @@ export default { }, accountType: { oauthTitle: 'OAuth (Gemini)', - oauthDesc: 'Authorize with your Google account and choose an OAuth type.', + oauthDesc: 'Авторизуйтесь через аккаунт Google и выберите тип OAuth.', apiKeyTitle: 'API Key (AI Studio)', - apiKeyDesc: 'Fastest setup. Use an AIza API key.', + apiKeyDesc: 'Самая быстрая настройка. Используйте AIza API key.', apiKeyNote: - 'Best for light testing. Free tier has strict rate limits and data may be used for training.', - apiKeyLink: 'Get API Key', - quotaLink: 'Quota guide' + 'Подходит для лёгкого тестирования. Free tier имеет строгие лимиты частоты, а данные могут использоваться для обучения.', + apiKeyLink: 'Получить API Key', + quotaLink: 'Руководство по квотам' }, oauthType: { - builtInTitle: 'Built-in OAuth (Gemini CLI / Code Assist)', - builtInDesc: 'Uses Google built-in client ID. No admin configuration required.', - builtInRequirement: 'Requires a GCP project and Project ID.', - gcpProjectLink: 'Create project', - customTitle: 'Custom OAuth (AI Studio OAuth)', - customDesc: 'Uses admin-configured OAuth client for org management.', - customRequirement: 'Admin must configure Client ID and add you as a test user.', + builtInTitle: 'Встроенный OAuth (Gemini CLI / Code Assist)', + builtInDesc: 'Использует встроенный client ID Google. Настройка админом не требуется.', + builtInRequirement: 'Требуется проект GCP и Project ID.', + gcpProjectLink: 'Создать проект', + customTitle: 'Пользовательский OAuth (AI Studio OAuth)', + customDesc: 'Использует настроенный админом OAuth client для управления организацией.', + customRequirement: 'Админ должен настроить Client ID и добавить вас как test user.', badges: { - recommended: 'Recommended', - highConcurrency: 'High concurrency', - noAdmin: 'No admin setup', - orgManaged: 'Org managed', - adminRequired: 'Admin required' + recommended: 'Рекомендуется', + highConcurrency: 'Высокий параллелизм', + noAdmin: 'Без настройки админом', + orgManaged: 'Управляется организацией', + adminRequired: 'Требуется админ' } }, setupGuide: { - title: 'Gemini Setup Checklist', - checklistTitle: 'Checklist', + title: 'Чек-лист настройки Gemini', + checklistTitle: 'Чек-лист', checklistItems: { - usIp: 'Use a US IP and ensure your account country is set to US.', - age: 'Account must be 18+.' + usIp: 'Используйте US IP и убедитесь, что страна аккаунта установлена как US.', + age: 'Аккаунт должен быть 18+.' }, - activationTitle: 'One-click Activation', + activationTitle: 'Активация в один клик', activationItems: { - geminiWeb: 'Activate Gemini Web to avoid User not initialized.', - gcpProject: 'Activate a GCP project and get the Project ID for Code Assist.' + geminiWeb: 'Активируйте Gemini Web, чтобы избежать ошибки User not initialized.', + gcpProject: 'Активируйте проект GCP и получите Project ID для Code Assist.' }, links: { - countryCheck: 'Check country association', - geminiWebActivation: 'Activate Gemini Web', - gcpProject: 'Open GCP Console' + countryCheck: 'Проверить привязку страны', + geminiWebActivation: 'Активировать Gemini Web', + gcpProject: 'Открыть GCP Console' } }, quotaPolicy: { - title: 'Gemini Quota & Limit Policy (Reference)', - note: 'Note: Gemini does not provide an official quota inquiry API. The "Daily Quota" shown here is an estimate simulated by the system based on account tiers for scheduling reference only. Please refer to official Google errors for actual limits.', + title: 'Политика квот и лимитов Gemini (справка)', + note: 'Примечание: Gemini не предоставляет официальный API для запроса квоты. Показанная здесь "Daily Quota" — оценка, симулированная системой на основе уровней аккаунта только для справки при маршрутизации. Фактические лимиты смотрите в официальных ошибках Google.', columns: { - channel: 'Auth Channel', - account: 'Account Status', - limits: 'Limit Policy', - docs: 'Official Docs' + channel: 'Канал авторизации', + account: 'Статус аккаунта', + limits: 'Политика лимитов', + docs: 'Официальная документация' }, docs: { codeAssist: 'Code Assist Quotas', aiStudio: 'AI Studio Pricing', vertex: 'Vertex AI Quotas' }, - simulatedNote: 'Simulated quota, for reference only', + simulatedNote: 'Симулированная квота, только для справки', rows: { googleOne: { channel: 'Google One OAuth (Individuals / Code Assist for Individuals)', @@ -3882,106 +3882,106 @@ export default { }, gcloud: { channel: 'GCP Code Assist (gcloud auth)', - account: 'No Code Assist subscription', + account: 'Нет подписки Code Assist', limits: 'RPD ~1000; RPM ~60 (preview)' }, aiStudio: { channel: 'AI Studio API Key / OAuth', - free: 'No billing (free tier)', - paid: 'Billing enabled (pay-as-you-go)', + free: 'Без billing (free tier)', + paid: 'Billing включён (pay-as-you-go)', limitsFree: 'RPD 50; RPM 2 (Pro) / 15 (Flash)', limitsPaid: 'RPD unlimited; RPM 1000 (Pro) / 2000 (Flash) (per model)' }, customOAuth: { channel: 'Custom OAuth Client (GCP)', - free: 'Project not billed', - paid: 'Project billed', + free: 'Проект без billing', + paid: 'Проект с billing', limitsFree: 'RPD 50; RPM 2 (project quota)', limitsPaid: 'RPD unlimited; RPM 1000+ (project quota)' } } }, rateLimit: { - ok: 'Not rate limited', + ok: 'Не ограничен по лимиту', unlimited: 'Безлимитно', - limited: 'Rate limited {time}', - now: 'now' + limited: 'Ограничен по лимиту {time}', + now: 'сейчас' } }, // Re-Auth Modal - reAuthorizeAccount: 'Re-Authorize Account', - claudeCodeAccount: 'Claude Code Account', - openaiAccount: 'OpenAI Account', - geminiAccount: 'Gemini Account', - antigravityAccount: 'Antigravity Account', - inputMethod: 'Input Method', - reAuthorizedSuccess: 'Account re-authorized successfully', + reAuthorizeAccount: 'Повторно авторизовать аккаунт', + claudeCodeAccount: 'Аккаунт Claude Code', + openaiAccount: 'Аккаунт OpenAI', + geminiAccount: 'Аккаунт Gemini', + antigravityAccount: 'Аккаунт Antigravity', + inputMethod: 'Способ ввода', + reAuthorizedSuccess: 'Аккаунт повторно авторизован', // Test Modal - testAccountConnection: 'Test Account Connection', - account: 'Account', - readyToTest: 'Ready to test. Click "Start Test" to begin...', - connectingToApi: 'Connecting to API...', - testCompleted: 'Test completed successfully!', - testFailed: 'Test failed', - connectedToApi: 'Connected to API', - usingModel: 'Using model: {model}', - sendingTestMessage: 'Sending test message: "hi"', - sendingImageRequest: 'Sending image generation test request...', - response: 'Response:', - startTest: 'Start Test', + testAccountConnection: 'Проверить подключение аккаунта', + account: 'Аккаунт', + readyToTest: 'Готово к проверке. Нажмите "Начать тест"...', + connectingToApi: 'Подключение к API...', + testCompleted: 'Тест успешно завершён!', + testFailed: 'Тест не удался', + connectedToApi: 'Подключено к API', + usingModel: 'Используется модель: {model}', + sendingTestMessage: 'Отправка тестового сообщения: "hi"', + sendingImageRequest: 'Отправка тестового запроса генерации изображения...', + response: 'Ответ:', + startTest: 'Начать тест', testing: 'Проверка...', retry: 'Повторить', - copyOutput: 'Copy output', - outputCopied: 'Output copied', - startingTestForAccount: 'Starting test for account: {name}', - testAccountTypeLabel: 'Account type: {type}', - selectTestModel: 'Select Test Model', - testModel: 'Test model', + copyOutput: 'Скопировать вывод', + outputCopied: 'Вывод скопирован', + startingTestForAccount: 'Запуск теста для аккаунта: {name}', + testAccountTypeLabel: 'Тип аккаунта: {type}', + selectTestModel: 'Выберите тестовую модель', + testModel: 'Тестовая модель', testPrompt: 'Prompt: "hi"', - imagePromptLabel: 'Image prompt', - imagePromptPlaceholder: 'Example: Generate an orange cat astronaut sticker in pixel-art style on a solid background.', + imagePromptLabel: 'Prompt изображения', + imagePromptPlaceholder: 'Пример: Generate an orange cat astronaut sticker in pixel-art style on a solid background.', imagePromptDefault: 'Generate a cute orange cat astronaut sticker on a clean pastel background.', - imageTestHint: 'When an image model is selected, this test sends a real image-generation request and previews the returned image below.', - imageTestMode: 'Mode: Image generation test', - imagePreview: 'Generated images:', - imageReceived: 'Received test image #{count}', + imageTestHint: 'Когда выбрана модель изображений, этот тест отправляет настоящий запрос генерации изображения и показывает предпросмотр ниже.', + imageTestMode: 'Режим: тест изображения', + imagePreview: 'Сгенерированные изображения:', + imageReceived: 'Получено тестовое изображение #{count}', // Stats Modal - viewStats: 'View Stats', - usageStatistics: 'Usage Statistics', - last30DaysUsage: 'Last 30 days usage statistics (based on actual usage days)', + viewStats: 'Посмотреть статистику', + usageStatistics: 'Статистика расхода', + last30DaysUsage: 'Статистика расхода за 30 дней (по фактическим дням использования)', stats: { - totalCost: '30-Day Total Cost', - accumulatedCost: 'Accumulated cost', + totalCost: 'Общая стоимость за 30 дней', + accumulatedCost: 'Накопленная стоимость', standardCost: 'Стандарт', - totalRequests: '30-Day Total Requests', - totalCalls: 'Total API calls', - avgDailyCost: 'Daily Avg Cost', - basedOnActualDays: 'Based on {days} actual usage days', - avgDailyRequests: 'Daily Avg Requests', - avgDailyUsage: 'Average daily usage', - todayOverview: 'Today Overview', + totalRequests: 'Всего запросов за 30 дней', + totalCalls: 'Всего API-вызовов', + avgDailyCost: 'Средняя дневная стоимость', + basedOnActualDays: 'На основе фактических дней использования: {days}', + avgDailyRequests: 'Среднее число запросов в день', + avgDailyUsage: 'Средний дневной расход', + todayOverview: 'Обзор за сегодня', cost: 'Стоимость', requests: 'Запросы', tokens: 'Токены', - highestCostDay: 'Highest Cost Day', - highestRequestDay: 'Highest Request Day', + highestCostDay: 'День с максимальной стоимостью', + highestRequestDay: 'День с максимумом запросов', date: 'Дата', - accumulatedTokens: 'Accumulated Tokens', - totalTokens: '30-Day Total', - dailyAvgTokens: 'Daily Average', + accumulatedTokens: 'Накоплено токенов', + totalTokens: 'Всего за 30 дней', + dailyAvgTokens: 'Среднее за день', performance: 'Производительность', avgResponseTime: 'Средний ответ', - daysActive: 'Days Active', + daysActive: 'Активных дней', recentActivity: 'Последняя активность', todayRequests: 'Запросов сегодня', todayTokens: 'Токенов сегодня', todayCost: 'Расход сегодня', - usageTrend: '30-Day Cost & Request Trend', - noData: 'No usage data available for this account' + usageTrend: 'Тренд стоимости и запросов за 30 дней', + noData: 'Нет данных расхода для этого аккаунта' }, usageWindow: { - statsTitle: '5-Hour Window Usage Statistics', - statsTitleDaily: 'Daily Usage Statistics', + statsTitle: 'Статистика расхода за окно 5h', + statsTitleDaily: 'Дневная статистика расхода', geminiProDaily: 'Pro', geminiFlashDaily: 'Flash', gemini3Pro: 'G3P', @@ -4002,95 +4002,95 @@ export default { unlimited: 'Безлимитно' }, ineligibleWarning: - 'This account is not eligible for Antigravity, but API forwarding still works. Use at your own risk.', + 'Этот аккаунт не подходит для Antigravity, но пересылка API всё равно работает. Используйте на свой риск.', forbidden: 'Доступ запрещён', - forbiddenValidation: 'Verification Required', - forbiddenViolation: 'Violation Ban', - openVerification: 'Open Verification Link', + forbiddenValidation: 'Требуется проверка', + forbiddenViolation: 'Блокировка за нарушение', + openVerification: 'Открыть ссылку проверки', copyLink: 'Скопировать ссылку', - linkCopied: 'Link Copied', - needsReauth: 'Re-auth Required', - rateLimited: 'Rate Limited', - usageError: 'Fetch Error' + linkCopied: 'Ссылка скопирована', + needsReauth: 'Требуется повторная авторизация', + rateLimited: 'Ограничен по лимиту', + usageError: 'Ошибка получения данных' }, // Scheduled Tests scheduledTests: { - title: 'Scheduled Tests', - addPlan: 'Add Plan', - editPlan: 'Edit Plan', - deletePlan: 'Delete Plan', + title: 'Плановые проверки', + addPlan: 'Добавить план', + editPlan: 'Редактировать план', + deletePlan: 'Удалить план', model: 'Модель', - cronExpression: 'Cron Expression', + cronExpression: 'Cron expression', enabled: 'Включено', - lastRun: 'Last Run', - nextRun: 'Next Run', - maxResults: 'Max Results', - noPlans: 'No scheduled test plans', - confirmDelete: 'Are you sure you want to delete this plan?', - createSuccess: 'Plan created successfully', - updateSuccess: 'Plan updated successfully', - deleteSuccess: 'Plan deleted successfully', - results: 'Test Results', - noResults: 'No test results yet', - responseText: 'Response', + lastRun: 'Последний запуск', + nextRun: 'Следующий запуск', + maxResults: 'Максимум результатов', + noPlans: 'Нет планов плановых проверок', + confirmDelete: 'Удалить этот план?', + createSuccess: 'План успешно создан', + updateSuccess: 'План успешно обновлён', + deleteSuccess: 'План успешно удалён', + results: 'Результаты проверки', + noResults: 'Результатов проверок пока нет', + responseText: 'Ответ', errorMessage: 'Ошибка', success: 'Успешно', failed: 'Ошибка', - running: 'Running', - schedule: 'Schedule', - cronHelp: 'Standard 5-field cron expression (e.g., */30 * * * *)', - cronTooltipTitle: 'Cron expression examples:', - cronTooltipMeaning: 'Defines when the test runs automatically. The 5 fields are: minute, hour, day, month, and weekday.', - cronTooltipExampleEvery30Min: '*/30 * * * *: run every 30 minutes', - cronTooltipExampleHourly: '0 * * * *: run at the start of every hour', - cronTooltipExampleDaily: '0 9 * * *: run every day at 09:00', - cronTooltipExampleWeekly: '0 9 * * 1: run every Monday at 09:00', - cronTooltipRange: 'Recommended range: use standard 5-field cron. For health checks, start with a moderate frequency such as every 30 minutes, every hour, or once a day instead of running too often.', - maxResultsTooltipTitle: 'What Max Results means:', - maxResultsTooltipMeaning: 'Sets how many historical test results are kept for a single plan so the result list does not grow without limit.', - maxResultsTooltipBody: 'Only the newest test results are kept. Once the number of saved results exceeds this value, older records are pruned automatically so the history list and storage stay under control.', - maxResultsTooltipExample: 'For example, 100 means keeping at most the latest 100 test results. When the 101st result is saved, the oldest one is removed.', - maxResultsTooltipRange: 'Recommended range: usually 20 to 200. Use 20-50 when you only care about recent health status, or 100-200 if you want a longer trend history.', - autoRecover: 'Auto Recover', - autoRecoverHelp: 'Automatically recover account from error/rate-limited state on successful test' + running: 'Выполняется', + schedule: 'Расписание', + cronHelp: 'Стандартное 5-польное cron expression (например, */30 * * * *)', + cronTooltipTitle: 'Примеры cron expression:', + cronTooltipMeaning: 'Определяет, когда проверка запускается автоматически. 5 полей: минута, час, день, месяц и день недели.', + cronTooltipExampleEvery30Min: '*/30 * * * *: запуск каждые 30 минут', + cronTooltipExampleHourly: '0 * * * *: запуск в начале каждого часа', + cronTooltipExampleDaily: '0 9 * * *: запуск каждый день в 09:00', + cronTooltipExampleWeekly: '0 9 * * 1: запуск каждый понедельник в 09:00', + cronTooltipRange: 'Рекомендация: используйте стандартный 5-польный cron. Для health checks начните с умеренной частоты — каждые 30 минут, каждый час или раз в день, а не слишком часто.', + maxResultsTooltipTitle: 'Что означает максимум результатов:', + maxResultsTooltipMeaning: 'Задаёт, сколько исторических результатов проверки хранится для одного плана, чтобы список не рос без ограничений.', + maxResultsTooltipBody: 'Хранятся только новые результаты проверок. Когда число сохранённых результатов превышает это значение, старые записи удаляются автоматически, чтобы история и хранилище оставались под контролем.', + maxResultsTooltipExample: 'Например, 100 означает хранение максимум последних 100 результатов проверки. При сохранении 101-го результата самый старый удаляется.', + maxResultsTooltipRange: 'Рекомендуемый диапазон обычно 20–200. Используйте 20–50, если важен только недавний health status, или 100–200 для более длинной истории тренда.', + autoRecover: 'Автовосстановление', + autoRecoverHelp: 'Автоматически восстанавливать аккаунт из состояния ошибки/rate-limited после успешной проверки' }, // Proxies proxies: { - title: 'Proxy Management', - description: 'Manage proxy servers for accounts', - createProxy: 'Create Proxy', - editProxy: 'Edit Proxy', - deleteProxy: 'Delete Proxy', + title: 'Управление proxy', + description: 'Управляйте proxy-серверами для аккаунтов', + createProxy: 'Создать proxy', + editProxy: 'Изменить proxy', + deleteProxy: 'Удалить proxy', ad: { - inline: 'Need proxy IP?' + inline: 'Нужен proxy IP?' }, dataImport: 'Импорт', - dataExportSelected: 'Export Selected', - dataImportTitle: 'Import Proxies', - dataImportHint: 'Upload the exported proxy JSON file to import proxies in bulk.', - dataImportWarning: 'Import will create or reuse proxies, keep their status, and trigger latency checks after completion.', - dataImportFile: 'Data File', - dataImportButton: 'Start Import', - dataImporting: 'Importing...', - dataImportSelectFile: 'Please select a data file', - dataImportParseFailed: 'Failed to parse data', - dataImportFailed: 'Failed to import data', - dataImportResult: 'Import Result', - dataImportResultSummary: 'Created {proxy_created}, reused {proxy_reused}, failed {proxy_failed}', - dataImportErrors: 'Failure Details', - dataImportSuccess: 'Import completed: created {proxy_created}, reused {proxy_reused}', - dataImportCompletedWithErrors: 'Import completed with errors: failed {proxy_failed}', + dataExportSelected: 'Экспортировать выбранное', + dataImportTitle: 'Импорт proxy', + dataImportHint: 'Загрузите экспортированный proxy JSON-файл для массового импорта proxy.', + dataImportWarning: 'Импорт создаст или переиспользует proxy, сохранит их статус и запустит проверки задержки после завершения.', + dataImportFile: 'Файл данных', + dataImportButton: 'Начать импорт', + dataImporting: 'Импорт...', + dataImportSelectFile: 'Выберите файл данных', + dataImportParseFailed: 'Не удалось разобрать данные', + dataImportFailed: 'Не удалось импортировать данные', + dataImportResult: 'Результат импорта', + dataImportResultSummary: 'Создано {proxy_created}, переиспользовано {proxy_reused}, ошибок {proxy_failed}', + dataImportErrors: 'Детали ошибок', + dataImportSuccess: 'Импорт завершён: создано {proxy_created}, переиспользовано {proxy_reused}', + dataImportCompletedWithErrors: 'Импорт завершён с ошибками: ошибок {proxy_failed}', dataExport: 'Экспорт', - dataExportConfirmMessage: 'The exported data contains sensitive proxy information. Store it securely.', - dataExportConfirm: 'Confirm Export', - dataExported: 'Data exported successfully', - dataExportFailed: 'Failed to export data', - copyProxyUrl: 'Copy Proxy URL', - urlCopied: 'Proxy URL copied', - searchProxies: 'Search proxies...', - allProtocols: 'All Protocols', + dataExportConfirmMessage: 'Экспортированные данные содержат чувствительную информацию proxy. Храните их в безопасном месте.', + dataExportConfirm: 'Подтвердить экспорт', + dataExported: 'Данные экспортированы', + dataExportFailed: 'Не удалось экспортировать данные', + copyProxyUrl: 'Скопировать proxy URL', + urlCopied: 'proxy URL скопирован', + searchProxies: 'Поиск proxy...', + allProtocols: 'Все протоколы', allStatus: 'Все статусы', protocols: { http: 'HTTP', @@ -4100,207 +4100,207 @@ export default { }, columns: { name: 'Имя', - protocol: 'Protocol', - address: 'Address', + protocol: 'Протокол', + address: 'Адрес', auth: 'Auth', - location: 'Location', + location: 'Локация', status: 'Статус', accounts: 'Аккаунты', - latency: 'Latency', + latency: 'Задержка', actions: 'Действия' }, testConnection: 'Проверить подключение', - qualityCheck: 'Quality Check', - batchQualityCheck: 'Batch Quality Check', - batchTest: 'Test All Proxies', + qualityCheck: 'Проверка качества', + batchQualityCheck: 'Массовая проверка качества', + batchTest: 'Проверить все proxy', testFailed: 'Ошибка', - latencyFailed: 'Connection failed', - batchTestEmpty: 'No proxies available for testing', - batchTestDone: 'Batch test completed for {count} proxies', - batchTestFailed: 'Batch test failed', + latencyFailed: 'Подключение не удалось', + batchTestEmpty: 'Нет proxy для проверки', + batchTestDone: 'Массовая проверка завершена для proxy: {count}', + batchTestFailed: 'Массовая проверка не удалась', batchDeleteAction: 'Удалить', - batchDelete: 'Batch delete', - batchDeleteConfirm: 'Delete {count} selected proxies? In-use ones will be skipped.', - batchDeleteDone: 'Deleted {deleted} proxies, skipped {skipped}', - batchDeleteSkipped: 'Skipped {skipped} proxies', - batchDeleteFailed: 'Batch delete failed', - deleteBlockedInUse: 'This proxy is in use and cannot be deleted', - accountsTitle: 'Accounts using this IP', - accountsEmpty: 'No accounts are using this proxy', - accountsFailed: 'Failed to load accounts list', - accountName: 'Account', + batchDelete: 'Массовое удаление', + batchDeleteConfirm: 'Удалить выбранные proxy ({count})? Используемые будут пропущены.', + batchDeleteDone: 'Удалено proxy: {deleted}, пропущено {skipped}', + batchDeleteSkipped: 'Пропущено proxy: {skipped}', + batchDeleteFailed: 'Массовое удаление не удалось', + deleteBlockedInUse: 'Этот proxy используется и не может быть удалён', + accountsTitle: 'Аккаунты, использующие этот IP', + accountsEmpty: 'Ни один аккаунт не использует этот proxy', + accountsFailed: 'Не удалось загрузить список аккаунтов', + accountName: 'Аккаунт', accountPlatform: 'Платформа', - accountNotes: 'Notes', + accountNotes: 'Заметки', name: 'Имя', - protocol: 'Protocol', + protocol: 'Протокол', host: 'Хост', port: 'Порт', - username: 'Username (Optional)', - password: 'Password (Optional)', + username: 'Имя пользователя (необязательно)', + password: 'Пароль (необязательно)', status: 'Статус', - enterProxyName: 'Enter proxy name', - leaveEmptyToKeep: 'Leave empty to keep current', - optionalAuth: 'Optional authentication', + enterProxyName: 'Введите имя proxy', + leaveEmptyToKeep: 'Оставьте пустым, чтобы сохранить текущее значение', + optionalAuth: 'Необязательная аутентификация', form: { hostPlaceholder: 'proxy.example.com', portPlaceholder: '8080' }, - noProxiesYet: 'No proxies yet', - createFirstProxy: 'Create your first proxy to route traffic through it.', + noProxiesYet: 'proxy пока нет', + createFirstProxy: 'Создайте первый proxy, чтобы маршрутизировать через него трафик.', // Batch import - standardAdd: 'Standard Add', - batchAdd: 'Quick Add', - batchInput: 'Proxy List', + standardAdd: 'Стандартное добавление', + batchAdd: 'Быстрое добавление', + batchInput: 'Список proxy', batchInputPlaceholder: - "Enter one proxy per line in the following formats:\nsocks5://user:pass{'@'}192.168.1.1:1080\nhttp://192.168.1.1:8080\nhttps://user:pass{'@'}proxy.example.com:443", + "Введите по одному proxy на строку в следующих форматах:\nsocks5://user:pass{'@'}192.168.1.1:1080\nhttp://192.168.1.1:8080\nhttps://user:pass{'@'}proxy.example.com:443", batchInputHint: - "Supports http, https, socks5 protocols. Format: protocol://[user:pass{'@'}]host:port", - parsedCount: '{count} valid', - invalidCount: '{count} invalid', - duplicateCount: '{count} duplicate', - importing: 'Importing...', - importProxies: 'Import {count} proxies', - batchImportSuccess: 'Successfully imported {created} proxies, skipped {skipped} duplicates', - batchImportAllSkipped: 'All {skipped} proxies already exist, skipped import', - failedToImport: 'Failed to batch import', + "Поддерживаются протоколы http, https, socks5. Формат: protocol://[user:pass{'@'}]host:port", + parsedCount: 'Корректных: {count}', + invalidCount: 'Некорректных: {count}', + duplicateCount: 'Дубликатов: {count}', + importing: 'Импорт...', + importProxies: 'Импортировать proxy: {count}', + batchImportSuccess: 'Успешно импортировано proxy: {created}, пропущено дубликатов: {skipped}', + batchImportAllSkipped: 'Все proxy уже существуют ({skipped}), импорт пропущен', + failedToImport: 'Массовый импорт не удался', // Other messages - creating: 'Creating...', + creating: 'Создание...', updating: 'Обновление...', - proxyCreated: 'Proxy created successfully', - proxyUpdated: 'Proxy updated successfully', - proxyDeleted: 'Proxy deleted successfully', - proxyWorking: 'Proxy is working!', - proxyWorkingWithLatency: 'Proxy is working! Latency: {latency}ms', - proxyTestFailed: 'Proxy test failed', - qualityCheckDone: 'Quality check completed: score {score} ({grade})', - qualityCheckFailed: 'Failed to run proxy quality check', + proxyCreated: 'proxy создан', + proxyUpdated: 'proxy обновлён', + proxyDeleted: 'proxy удалён', + proxyWorking: 'proxy работает!', + proxyWorkingWithLatency: 'proxy работает! Задержка: {latency}ms', + proxyTestFailed: 'Проверка proxy не удалась', + qualityCheckDone: 'Проверка качества завершена: оценка {score} ({grade})', + qualityCheckFailed: 'Не удалось запустить проверку качества proxy', batchQualityDone: - 'Batch quality check completed for {count} proxies: healthy {healthy}, warn {warn}, challenge {challenge}, abnormal {failed}', - batchQualityFailed: 'Batch quality check failed', - batchQualityEmpty: 'No proxies available for quality check', - qualityReportTitle: 'Proxy Quality Report', - qualityGrade: 'Grade {grade}', - qualityExitIP: 'Exit IP', - qualityCountry: 'Exit Region', - qualityBaseLatency: 'Base Latency', - qualityCheckedAt: 'Checked At', - qualityTableTarget: 'Target', + 'Массовая проверка качества завершена для proxy: {count}; healthy {healthy}, warn {warn}, challenge {challenge}, abnormal {failed}', + batchQualityFailed: 'Массовая проверка качества не удалась', + batchQualityEmpty: 'Нет proxy для проверки качества', + qualityReportTitle: 'Отчёт качества proxy', + qualityGrade: 'Оценка {grade}', + qualityExitIP: 'Выходной IP', + qualityCountry: 'Регион выхода', + qualityBaseLatency: 'Базовая задержка', + qualityCheckedAt: 'Проверено', + qualityTableTarget: 'Цель', qualityTableStatus: 'Статус', - qualityTableLatency: 'Latency', - qualityTableMessage: 'Message', - qualityInline: 'Quality {grade}/{score}', - qualityStatusHealthy: 'Healthy', - qualityStatusPass: 'Pass', - qualityStatusWarn: 'Warn', - qualityStatusFail: 'Fail', + qualityTableLatency: 'Задержка', + qualityTableMessage: 'Сообщение', + qualityInline: 'Качество {grade}/{score}', + qualityStatusHealthy: 'Здоровый', + qualityStatusPass: 'Пройдено', + qualityStatusWarn: 'Предупреждение', + qualityStatusFail: 'Ошибка', qualityStatusChallenge: 'Challenge', - qualityTargetBase: 'Base Connectivity', - failedToLoad: 'Failed to load proxies', - failedToCreate: 'Failed to create proxy', - failedToUpdate: 'Failed to update proxy', - failedToDelete: 'Failed to delete proxy', - failedToTest: 'Failed to test proxy', - nameRequired: 'Please enter proxy name', - hostRequired: 'Please enter host address', - portInvalid: 'Port must be between 1-65535', + qualityTargetBase: 'Базовое подключение', + failedToLoad: 'Не удалось загрузить proxy', + failedToCreate: 'Не удалось создать proxy', + failedToUpdate: 'Не удалось обновить proxy', + failedToDelete: 'Не удалось удалить proxy', + failedToTest: 'Не удалось проверить proxy', + nameRequired: 'Введите имя proxy', + hostRequired: 'Введите адрес хоста', + portInvalid: 'Порт должен быть в диапазоне 1-65535', deleteConfirm: - "Are you sure you want to delete '{name}'? Accounts using this proxy will have their proxy removed." + "Удалить '{name}'? У аккаунтов, использующих этот proxy, proxy будет удалён." }, // Redeem Codes redeem: { - title: 'Redeem Code Management', - description: 'Generate and manage redeem codes', - generateCodes: 'Generate Codes', - searchCodes: 'Search codes or email...', - allTypes: 'All Types', + title: 'Управление кодами активации', + description: 'Создавайте и управляйте кодами активации', + generateCodes: 'Создать коды', + searchCodes: 'Поиск по кодам или email...', + allTypes: 'Все типы', allStatus: 'Все статусы', balance: 'Баланс', concurrency: 'Параллелизм', - subscription: 'Subscription', - invitation: 'Invitation', - invitationHint: 'Invitation codes are used to restrict user registration. They are automatically marked as used after use.', - unused: 'Unused', + subscription: 'Подписка', + invitation: 'Приглашение', + invitationHint: 'Коды приглашения используются для ограничения регистрации пользователей. После использования они автоматически помечаются как использованные.', + unused: 'Не использовано', used: 'Использовано', columns: { code: 'Код', type: 'Тип', - value: 'Value', + value: 'Значение', status: 'Статус', - usedBy: 'Used By', - usedAt: 'Used At', + usedBy: 'Использовано', + usedAt: 'Дата использования', expiresAt: 'Истекает', actions: 'Действия' }, - userPrefix: 'User #{id}', + userPrefix: 'Пользователь #{id}', exportCsv: 'Экспорт CSV', - batchUpdate: 'Batch Update', - batchUpdateTitle: 'Batch Update Redeem Codes', - selectedCount: '{count} redeem code(s) selected', - clearSelection: 'Clear selection', - selectCodesFirst: 'Select redeem codes first', - noBatchFieldsSelected: 'Select at least one field to update', - batchUpdateSuccess: 'Updated {count} redeem code(s)', - failedToBatchUpdate: 'Failed to batch update redeem codes', + batchUpdate: 'Массовое обновление', + batchUpdateTitle: 'Массовое обновление кодов активации', + selectedCount: 'Выбрано кодов активации: {count}', + clearSelection: 'Снять выбор', + selectCodesFirst: 'Сначала выберите коды активации', + noBatchFieldsSelected: 'Выберите хотя бы одно поле для обновления', + batchUpdateSuccess: 'Обновлено кодов активации: {count}', + failedToBatchUpdate: 'Не удалось массово обновить коды активации', batchFields: { status: 'Статус', expiresAt: 'Истекает', - notes: 'Notes', + notes: 'Заметки', group: 'Группа' }, - batchNotesPlaceholder: 'Enter the new note, or leave blank to clear it', - clearGroup: 'Clear group', - deleteAllUnused: 'Delete All Unused Codes', - deleteCode: 'Delete Redeem Code', + batchNotesPlaceholder: 'Введите новую заметку или оставьте пустым, чтобы очистить её', + clearGroup: 'Очистить группу', + deleteAllUnused: 'Удалить все неиспользованные коды', + deleteCode: 'Удалить код активации', deleteCodeConfirm: - 'Are you sure you want to delete this redeem code? This action cannot be undone.', + 'Удалить этот код активации? Это действие нельзя отменить.', deleteAllUnusedConfirm: - 'Are you sure you want to delete all unused (active) redeem codes? This action cannot be undone.', - deleteAll: 'Delete All', - generateCodesTitle: 'Generate Redeem Codes', - generatedSuccessfully: 'Generated Successfully', - codesCreated: '{count} redeem code(s) created', - codeType: 'Code Type', - amount: 'Amount ($)', - value: 'Value', - count: 'Count', - generating: 'Generating...', - generate: 'Generate', - copyAll: 'Copy All', + 'Удалить все неиспользованные (активные) коды активации? Это действие нельзя отменить.', + deleteAll: 'Удалить все', + generateCodesTitle: 'Создать коды активации', + generatedSuccessfully: 'Успешно создано', + codesCreated: 'Создано кодов активации: {count}', + codeType: 'Тип кода', + amount: 'Сумма ($)', + value: 'Значение', + count: 'Количество', + generating: 'Создание...', + generate: 'Создать', + copyAll: 'Скопировать все', copied: 'Скопировано!', - download: 'Download', - codesExported: 'Codes exported successfully', - codeDeleted: 'Redeem code deleted successfully', - codesDeleted: 'Successfully deleted {count} unused code(s)', - noUnusedCodes: 'No unused codes to delete', - failedToLoad: 'Failed to load redeem codes', - failedToGenerate: 'Failed to generate codes', - failedToExport: 'Failed to export codes', - failedToDelete: 'Failed to delete code', - failedToDeleteUnused: 'Failed to delete unused codes', - failedToCopy: 'Failed to copy codes', + download: 'Скачать', + codesExported: 'Коды экспортированы', + codeDeleted: 'Код активации удалён', + codesDeleted: 'Удалено неиспользованных кодов: {count}', + noUnusedCodes: 'Нет неиспользованных кодов для удаления', + failedToLoad: 'Не удалось загрузить коды активации', + failedToGenerate: 'Не удалось создать коды', + failedToExport: 'Не удалось экспортировать коды', + failedToDelete: 'Не удалось удалить код', + failedToDeleteUnused: 'Не удалось удалить неиспользованные коды', + failedToCopy: 'Не удалось скопировать коды', types: { balance: 'Баланс', concurrency: 'Параллелизм', - subscription: 'Subscription', - invitation: 'Invitation', + subscription: 'Подписка', + invitation: 'Приглашение', // Admin adjustment types (created when admin modifies user balance/concurrency) - admin_balance: 'Balance (Admin)', - admin_concurrency: 'Concurrency (Admin)' - }, - selectGroup: 'Select Group', - selectGroupPlaceholder: 'Choose a subscription group', - validityDays: 'Validity Days', - codeExpiry: 'Code Expiry', - neverExpires: 'Never expires', + admin_balance: 'Баланс (админ)', + admin_concurrency: 'Параллелизм (админ)' + }, + selectGroup: 'Выбрать группу', + selectGroupPlaceholder: 'Выберите группу подписки', + validityDays: 'Срок действия в днях', + codeExpiry: 'Срок действия кода', + neverExpires: 'Без срока действия', expiryPresetDays: '{days} дн.', customExpiry: 'Свой', - customExpiryDays: 'Custom days', - expiryDaysRequired: 'Please enter a valid expiry day count', - groupRequired: 'Please select a subscription group', - days: ' days', + customExpiryDays: 'Свои дни', + expiryDaysRequired: 'Введите корректное количество дней срока действия', + groupRequired: 'Выберите группу подписки', + days: ' дн.', status: { - unused: 'Unused', + unused: 'Не использовано', used: 'Использовано', expired: 'Истекла', disabled: 'Отключено' @@ -4310,52 +4310,52 @@ export default { // Announcements announcements: { title: 'Объявления', - description: 'Create announcements and target by conditions', - createAnnouncement: 'Create Announcement', - editAnnouncement: 'Edit Announcement', - deleteAnnouncement: 'Delete Announcement', - searchAnnouncements: 'Search announcements...', + description: 'Создавайте объявления и настраивайте таргетинг по условиям', + createAnnouncement: 'Создать объявление', + editAnnouncement: 'Изменить объявление', + deleteAnnouncement: 'Удалить объявление', + searchAnnouncements: 'Поиск объявлений...', status: 'Статус', allStatus: 'Все статусы', columns: { - title: 'Title', + title: 'Заголовок', status: 'Статус', - notifyMode: 'Notify Mode', - targeting: 'Targeting', - timeRange: 'Schedule', - createdAt: 'Created At', + notifyMode: 'Режим уведомления', + targeting: 'Таргетинг', + timeRange: 'Расписание', + createdAt: 'Создано', actions: 'Действия' }, statusLabels: { - draft: 'Draft', + draft: 'Черновик', active: 'Активен', - archived: 'Archived' + archived: 'В архиве' }, notifyModeLabels: { - silent: 'Silent', + silent: 'Тихий', popup: 'Popup' }, form: { - title: 'Title', - content: 'Content (Markdown supported)', + title: 'Заголовок', + content: 'Содержимое (поддерживается Markdown)', status: 'Статус', - notifyMode: 'Notify Mode', - notifyModeHint: 'Popup mode will show a popup notification to users', - startsAt: 'Starts At', - endsAt: 'Ends At', - startsAtHint: 'Leave empty to start immediately', - endsAtHint: 'Leave empty to never expire', - targetingMode: 'Targeting', - targetingAll: 'All users', - targetingCustom: 'Custom rules', - addOrGroup: 'Add OR group', - addAndCondition: 'Add AND condition', - conditionType: 'Condition type', - conditionSubscription: 'Subscription', + notifyMode: 'Режим уведомления', + notifyModeHint: 'Режим Popup покажет пользователям всплывающее уведомление', + startsAt: 'Начало', + endsAt: 'Окончание', + startsAtHint: 'Оставьте пустым, чтобы начать сразу', + endsAtHint: 'Оставьте пустым, чтобы не ограничивать срок', + targetingMode: 'Таргетинг', + targetingAll: 'Все пользователи', + targetingCustom: 'Пользовательские правила', + addOrGroup: 'Добавить OR-группу', + addAndCondition: 'Добавить AND-условие', + conditionType: 'Тип условия', + conditionSubscription: 'Подписка', conditionBalance: 'Баланс', - operator: 'Operator', - balanceValue: 'Balance threshold', - selectPackages: 'Select packages' + operator: 'Оператор', + balanceValue: 'Порог баланса', + selectPackages: 'Выберите пакеты' }, operators: { gt: '>', @@ -4364,208 +4364,208 @@ export default { lte: '≤', eq: '=' }, - targetingSummaryAll: 'All users', - targetingSummaryCustom: 'Custom ({groups} groups)', - timeImmediate: 'Immediate', + targetingSummaryAll: 'Все пользователи', + targetingSummaryCustom: 'Пользовательские правила (групп: {groups})', + timeImmediate: 'Сразу', timeNever: 'Никогда', - readStatus: 'Read Status', - eligible: 'Eligible', + readStatus: 'Статус прочтения', + eligible: 'Подходит', readAt: 'Прочитано', unread: 'Непрочитано', - searchUsers: 'Search users...', - failedToLoad: 'Failed to load announcements', - failedToCreate: 'Failed to create announcement', - failedToUpdate: 'Failed to update announcement', - failedToDelete: 'Failed to delete announcement', - failedToLoadReadStatus: 'Failed to load read status', - deleteConfirm: 'Are you sure you want to delete this announcement? This action cannot be undone.' + searchUsers: 'Поиск пользователей...', + failedToLoad: 'Не удалось загрузить объявления', + failedToCreate: 'Не удалось создать объявление', + failedToUpdate: 'Не удалось обновить объявление', + failedToDelete: 'Не удалось удалить объявление', + failedToLoadReadStatus: 'Не удалось загрузить статус прочтения', + deleteConfirm: 'Удалить это объявление? Это действие нельзя отменить.' }, // Promo Codes promo: { - title: 'Promo Code Management', - description: 'Create and manage registration promo codes', - createCode: 'Create Promo Code', - editCode: 'Edit Promo Code', - deleteCode: 'Delete Promo Code', - searchCodes: 'Search codes...', + title: 'Управление промокодами', + description: 'Создавайте и управляйте промокодами для регистрации', + createCode: 'Создать промокод', + editCode: 'Изменить промокод', + deleteCode: 'Удалить промокод', + searchCodes: 'Поиск промокодов...', allStatus: 'Все статусы', columns: { code: 'Код', - bonusAmount: 'Bonus Amount', - maxUses: 'Max Uses', + bonusAmount: 'Бонусная сумма', + maxUses: 'Макс. использований', usedCount: 'Использовано', usage: 'Расход', status: 'Статус', expiresAt: 'Истекает', - createdAt: 'Created At', + createdAt: 'Создано', actions: 'Действия' }, // Form labels (flat structure for template usage) code: 'Промокод', - autoGenerate: 'auto-generate if empty', - codePlaceholder: 'Enter promo code or leave empty', - bonusAmount: 'Bonus Amount ($)', - maxUses: 'Max Uses', - zeroUnlimited: '0 = unlimited', + autoGenerate: 'автогенерация, если пусто', + codePlaceholder: 'Введите промокод или оставьте пустым', + bonusAmount: 'Бонусная сумма ($)', + maxUses: 'Макс. использований', + zeroUnlimited: '0 = безлимитно', expiresAt: 'Истекает', - notes: 'Notes', - notesPlaceholder: 'Optional notes for this code', + notes: 'Заметки', + notesPlaceholder: 'Необязательные заметки для этого кода', status: 'Статус', - neverExpires: 'Never expires', + neverExpires: 'Без срока действия', // Status labels statusActive: 'Активен', statusDisabled: 'Отключено', statusExpired: 'Истекла', - statusMaxUsed: 'Used Up', + statusMaxUsed: 'Использован полностью', // Usage records usageRecords: 'История расхода', - viewUsages: 'View Usages', - noUsages: 'No usage records yet', - userPrefix: 'User #{id}', + viewUsages: 'Посмотреть использования', + noUsages: 'Записей расхода пока нет', + userPrefix: 'Пользователь #{id}', copied: 'Скопировано!', // Messages - noCodesYet: 'No promo codes yet', - createFirstCode: 'Create your first promo code to offer registration bonuses.', - codeCreated: 'Promo code created successfully', - codeUpdated: 'Promo code updated successfully', - codeDeleted: 'Promo code deleted successfully', - deleteCodeConfirm: 'Are you sure you want to delete this promo code? This action cannot be undone.', - copyRegisterLink: 'Copy register link', - registerLinkCopied: 'Register link copied to clipboard', - failedToLoad: 'Failed to load promo codes', - failedToCreate: 'Failed to create promo code', - failedToUpdate: 'Failed to update promo code', - failedToDelete: 'Failed to delete promo code', - failedToLoadUsages: 'Failed to load usage records' + noCodesYet: 'Промокодов пока нет', + createFirstCode: 'Создайте первый промокод, чтобы выдавать бонусы за регистрацию.', + codeCreated: 'Промокод создан', + codeUpdated: 'Промокод обновлён', + codeDeleted: 'Промокод удалён', + deleteCodeConfirm: 'Удалить этот промокод? Это действие нельзя отменить.', + copyRegisterLink: 'Скопировать ссылку регистрации', + registerLinkCopied: 'Ссылка регистрации скопирована', + failedToLoad: 'Не удалось загрузить промокоды', + failedToCreate: 'Не удалось создать промокод', + failedToUpdate: 'Не удалось обновить промокод', + failedToDelete: 'Не удалось удалить промокод', + failedToLoadUsages: 'Не удалось загрузить записи расхода' }, // Usage Records usage: { title: 'История расхода', - description: 'View and manage all user usage records', + description: 'Просматривайте и управляйте всеми пользовательскими записями расхода', userFilter: 'Пользователь', - searchUserPlaceholder: 'Search user by email...', - searchApiKeyPlaceholder: 'Search API key by name...', - searchAccountPlaceholder: 'Search account by name...', - selectedUser: 'Selected', + searchUserPlaceholder: 'Поиск пользователя по email...', + searchApiKeyPlaceholder: 'Поиск API-ключа по имени...', + searchAccountPlaceholder: 'Поиск аккаунта по имени...', + selectedUser: 'Выбрано', user: 'Пользователь', - account: 'Account', + account: 'Аккаунт', group: 'Группа', requestId: 'Request ID', - requestIdCopied: 'Request ID copied', - allModels: 'All Models', - allAccounts: 'All Accounts', + requestIdCopied: 'Request ID скопирован', + allModels: 'Все модели', + allAccounts: 'Все аккаунты', allGroups: 'Все группы', - allTypes: 'All Types', - inputCost: 'Input Cost', - outputCost: 'Output Cost', - cacheCreationCost: 'Cache Creation Cost', - cacheReadCost: 'Cache Read Cost', + allTypes: 'Все типы', + inputCost: 'Стоимость входа', + outputCost: 'Стоимость выхода', + cacheCreationCost: 'Стоимость создания cache', + cacheReadCost: 'Стоимость чтения cache', inputTokens: 'Входные токены', outputTokens: 'Выходные токены', - cacheCreationTokens: 'Cache Creation Tokens', + cacheCreationTokens: 'Токены создания cache', cacheCreation5mTokens: 'Запись кэша', cacheCreation1hTokens: 'Запись кэша', - cacheReadTokens: 'Cache Read Tokens', - failedToLoad: 'Failed to load usage records', - billingType: 'Billing Type', - allBillingTypes: 'All Billing Types', + cacheReadTokens: 'Токены чтения cache', + failedToLoad: 'Не удалось загрузить записи расхода', + billingType: 'Тип списания', + allBillingTypes: 'Все типы списания', billingTypeBalance: 'Баланс', - billingTypeSubscription: 'Subscription', - billingMode: 'Режим биллинга', + billingTypeSubscription: 'Подписка', + billingMode: 'Режим списания', billingModeToken: 'Token', billingModePerRequest: 'За запрос', - billingModeImage: 'Image', - allBillingModes: 'All Billing Modes', + billingModeImage: 'Изображение', + allBillingModes: 'Все режимы списания', ipAddress: 'IP', - clickToViewBalance: 'Click to view balance history', - failedToLoadUser: 'Failed to load user info', + clickToViewBalance: 'Нажмите, чтобы посмотреть историю баланса', + failedToLoadUser: 'Не удалось загрузить информацию пользователя', cleanup: { - button: 'Cleanup', - title: 'Cleanup Usage Records', - warning: 'Cleanup is irreversible and will affect historical stats.', - submit: 'Submit Cleanup', + button: 'Очистка', + title: 'Очистка записей расхода', + warning: 'Очистка необратима и повлияет на историческую статистику.', + submit: 'Запустить очистку', submitting: 'Отправка...', - confirmTitle: 'Confirm Cleanup', - confirmMessage: 'Are you sure you want to submit this cleanup task? This action cannot be undone.', - confirmSubmit: 'Confirm Cleanup', + confirmTitle: 'Подтвердите очистку', + confirmMessage: 'Отправить эту задачу очистки? Это действие нельзя отменить.', + confirmSubmit: 'Подтвердить очистку', cancel: 'Отмена', - cancelConfirmTitle: 'Confirm Cancel', - cancelConfirmMessage: 'Are you sure you want to cancel this cleanup task?', - cancelConfirm: 'Confirm Cancel', - cancelSuccess: 'Cleanup task canceled', - cancelFailed: 'Failed to cancel cleanup task', - recentTasks: 'Recent Cleanup Tasks', - loadingTasks: 'Loading tasks...', - noTasks: 'No cleanup tasks yet', - range: 'Range', - deletedRows: 'Deleted', - missingRange: 'Please select a date range', - submitSuccess: 'Cleanup task created', - submitFailed: 'Failed to create cleanup task', - loadFailed: 'Failed to load cleanup tasks', + cancelConfirmTitle: 'Подтвердите отмену', + cancelConfirmMessage: 'Отменить эту задачу очистки?', + cancelConfirm: 'Подтвердить отмену', + cancelSuccess: 'Задача очистки отменена', + cancelFailed: 'Не удалось отменить задачу очистки', + recentTasks: 'Последние задачи очистки', + loadingTasks: 'Загрузка задач...', + noTasks: 'Задач очистки пока нет', + range: 'Диапазон', + deletedRows: 'Удалено', + missingRange: 'Выберите диапазон дат', + submitSuccess: 'Задача очистки создана', + submitFailed: 'Не удалось создать задачу очистки', + loadFailed: 'Не удалось загрузить задачи очистки', status: { pending: 'Ожидает', - running: 'Running', - succeeded: 'Succeeded', + running: 'Выполняется', + succeeded: 'Успешно', failed: 'Ошибка', - canceled: 'Canceled' + canceled: 'Отменено' } } }, // Ops Monitoring ops: { - title: 'Ops Monitoring', - description: 'Operational monitoring and troubleshooting', + title: 'Операционный мониторинг', + description: 'Операционный мониторинг и диагностика', // Dashboard - systemHealth: 'System Health', - overview: 'Overview', - noSystemMetrics: 'No system metrics collected yet.', - collectedAt: 'Collected at:', - window: 'window', - memory: 'Memory', + systemHealth: 'Состояние системы', + overview: 'Обзор', + noSystemMetrics: 'Системные метрики пока не собраны.', + collectedAt: 'Собрано:', + window: 'окно', + memory: 'Память', db: 'DB', goroutines: 'Goroutines', jobs: 'Jobs', - jobsHelp: 'Click “Details” to view job heartbeats and recent errors', - active: 'active', + jobsHelp: 'Нажмите «Details», чтобы посмотреть heartbeat задач и последние ошибки', + active: 'активно', idle: 'idle', waiting: 'waiting', conns: 'conns', queue: 'queue', - accountSwitches: 'Account switches', + accountSwitches: 'Переключения аккаунтов', ok: 'ok', lastRun: 'last_run:', lastSuccess: 'last_success:', lastError: 'last_error:', - noData: 'No data.', - loadingText: 'loading', - ready: 'ready', - requestsTotal: 'Requests (total)', - slaScope: 'SLA scope:', + noData: 'Нет данных.', + loadingText: 'загрузка', + ready: 'готово', + requestsTotal: 'Запросы (всего)', + slaScope: 'Область SLA:', tokens: 'Токены', tps: 'TPS:', - current: 'current', - peak: 'peak', - average: 'average', + current: 'текущее', + peak: 'пик', + average: 'среднее', totalRequests: 'Всего запросов', - avgQps: 'Avg QPS', - avgTps: 'Avg TPS', - avgLatency: 'Avg Request Duration', - avgTtft: 'Avg TTFT', - exceptions: 'Exceptions', - requestErrors: 'Request Errors', - errorCount: 'Error Count', - upstreamErrors: 'Upstream Errors', - errorCountExcl429529: 'Error Count (excl 429/529)', - sla: 'SLA (excl business limits)', + avgQps: 'Средний QPS', + avgTps: 'Средний TPS', + avgLatency: 'Средняя длительность запроса', + avgTtft: 'Средний TTFT', + exceptions: 'Исключения', + requestErrors: 'Ошибки запросов', + errorCount: 'Количество ошибок', + upstreamErrors: 'Ошибки upstream', + errorCountExcl429529: 'Количество ошибок (без 429/529)', + sla: 'SLA (без бизнес-лимитов)', businessLimited: 'business_limited:', - errors: 'Errors', + errors: 'Ошибки', errorRate: 'error_rate:', upstreamRate: 'upstream_rate:', - latencyDuration: 'Request Duration', + latencyDuration: 'Длительность запроса', ttftLabel: 'TTFT (first_token_ms)', p50: 'p50:', p90: 'p90:', @@ -4575,130 +4575,130 @@ export default { max: 'max:', requests: 'Запросы', requestsTitle: 'Запросы', - upstream: 'Апстрим', - client: 'Client', - system: 'System', + upstream: 'upstream', + client: 'Клиент', + system: 'Система', other: 'Другое', - errorsSla: 'Errors (SLA scope)', - upstreamExcl429529: 'Upstream (excl 429/529)', - failedToLoadData: 'Failed to load ops data.', - failedToLoadOverview: 'Failed to load overview', - failedToLoadThroughputTrend: 'Failed to load throughput trend', - failedToLoadSwitchTrend: 'Failed to load avg account switches trend', - failedToLoadLatencyHistogram: 'Failed to load request duration histogram', - failedToLoadErrorTrend: 'Failed to load error trend', - failedToLoadErrorDistribution: 'Failed to load error distribution', - failedToLoadErrorDetail: 'Failed to load error detail', - retryFailed: 'Retry failed', + errorsSla: 'Ошибки (область SLA)', + upstreamExcl429529: 'upstream (без 429/529)', + failedToLoadData: 'Не удалось загрузить данные ops.', + failedToLoadOverview: 'Не удалось загрузить обзор', + failedToLoadThroughputTrend: 'Не удалось загрузить динамику пропускной способности', + failedToLoadSwitchTrend: 'Не удалось загрузить динамику средних переключений аккаунтов', + failedToLoadLatencyHistogram: 'Не удалось загрузить гистограмму длительности запросов', + failedToLoadErrorTrend: 'Не удалось загрузить динамику ошибок', + failedToLoadErrorDistribution: 'Не удалось загрузить распределение ошибок', + failedToLoadErrorDetail: 'Не удалось загрузить детали ошибки', + retryFailed: 'Повтор не удался', tpsK: 'TPS (K)', top: 'Top:', - throughputTrend: 'Throughput Trend', - switchRateTrend: 'Avg Account Switches', - latencyHistogram: 'Request Duration Histogram', - errorTrend: 'Error Trend', - errorDistribution: 'Error Distribution', - switchRate: 'Avg switches', + throughputTrend: 'Динамика пропускной способности', + switchRateTrend: 'Средние переключения аккаунтов', + latencyHistogram: 'Гистограмма длительности запросов', + errorTrend: 'Динамика ошибок', + errorDistribution: 'Распределение ошибок', + switchRate: 'Средние переключения', // Health Score & Diagnosis - health: 'Health', - healthCondition: 'Health Condition', - healthHelp: 'Overall system health score based on SLA, error rate, and resource usage', - healthyStatus: 'Healthy', - riskyStatus: 'At Risk', - idleStatus: 'Idle', + health: 'Состояние', + healthCondition: 'Состояние здоровья', + healthHelp: 'Общая оценка состояния системы на основе SLA, доли ошибок и использования ресурсов', + healthyStatus: 'Здорово', + riskyStatus: 'В зоне риска', + idleStatus: 'Простой', timeRange: { - '5m': 'Last 5 minutes', - '30m': 'Last 30 minutes', - '1h': 'Last 1 hour', - '1d': 'Last 1 day', - '15d': 'Last 15 days', - '6h': 'Last 6 hours', - '24h': 'Last 24 hours', + '5m': 'Последние 5 минут', + '30m': 'Последние 30 минут', + '1h': 'Последний 1 час', + '1d': 'Последний 1 день', + '15d': 'Последние 15 дней', + '6h': 'Последние 6 часов', + '24h': 'Последние 24 часа', '7d': 'Последние 7 дней', - '30d': 'Last 30 days' + '30d': 'Последние 30 дней' }, openaiTokenStats: { - title: 'OpenAI Token Request Stats', + title: 'Статистика запросов Token OpenAI', viewModeTopN: 'TopN', - viewModePagination: 'Pagination', + viewModePagination: 'Пагинация', prevPage: 'Назад', nextPage: 'Далее', - pageInfo: 'Page {page}/{total}', - totalModels: 'Total models: {total}', - failedToLoad: 'Failed to load OpenAI token stats', - empty: 'No OpenAI token stats for the current filters', + pageInfo: 'Страница {page}/{total}', + totalModels: 'Всего моделей: {total}', + failedToLoad: 'Не удалось загрузить статистику Token OpenAI', + empty: 'Нет статистики Token OpenAI для текущих фильтров', table: { model: 'Модель', requestCount: 'Запросы', - avgTokensPerSec: 'Avg Tokens/sec', - avgFirstTokenMs: 'Avg First Token Latency (ms)', - totalOutputTokens: 'Total Output Tokens', - avgDurationMs: 'Avg Duration (ms)', - requestsWithFirstToken: 'Requests With First Token' + avgTokensPerSec: 'Среднее Tokens/sec', + avgFirstTokenMs: 'Средняя задержка первого Token (ms)', + totalOutputTokens: 'Всего выходных токенов', + avgDurationMs: 'Средняя длительность (ms)', + requestsWithFirstToken: 'Запросы с первым Token' } }, fullscreen: { - enter: 'Enter Fullscreen' + enter: 'Во весь экран' }, diagnosis: { - title: 'Smart Diagnosis', - footer: 'Automated diagnostic suggestions based on current metrics', - idle: 'System is currently idle', - idleImpact: 'No active traffic', + title: 'Умная диагностика', + footer: 'Автоматические диагностические рекомендации на основе текущих метрик', + idle: 'Система сейчас простаивает', + idleImpact: 'Нет активного трафика', // Resource diagnostics - dbDown: 'Database connection failed', - dbDownImpact: 'All database operations will fail', - dbDownAction: 'Check database service status, network connectivity, and connection configuration', - redisDown: 'Redis connection failed', - redisDownImpact: 'Cache functionality degraded, performance may decline', - redisDownAction: 'Check Redis service status and network connectivity', - cpuCritical: 'CPU usage critically high ({usage}%)', - cpuCriticalImpact: 'System response slowing, may affect all requests', - cpuCriticalAction: 'Check CPU-intensive tasks, consider scaling or code optimization', - cpuHigh: 'CPU usage elevated ({usage}%)', - cpuHighImpact: 'System load is high, needs attention', - cpuHighAction: 'Monitor CPU trends, prepare scaling plan', - memoryCritical: 'Memory usage critically high ({usage}%)', - memoryCriticalImpact: 'May trigger OOM, system stability threatened', - memoryCriticalAction: 'Check for memory leaks, consider increasing memory or optimizing usage', - memoryHigh: 'Memory usage elevated ({usage}%)', - memoryHighImpact: 'Memory pressure is high, needs attention', - memoryHighAction: 'Monitor memory trends, check for memory leaks', - ttftHigh: 'Time to first token elevated ({ttft}ms)', - ttftHighImpact: 'User perceived latency increased', - ttftHighAction: 'Optimize request processing flow, reduce pre-processing time', + dbDown: 'Ошибка подключения к базе данных', + dbDownImpact: 'Все операции с базой данных будут завершаться ошибкой', + dbDownAction: 'Проверьте статус сервиса базы данных, сетевое подключение и конфигурацию соединения', + redisDown: 'Ошибка подключения к Redis', + redisDownImpact: 'Работа cache ухудшена, производительность может снизиться', + redisDownAction: 'Проверьте статус сервиса Redis и сетевое подключение', + cpuCritical: 'Использование CPU критически высокое ({usage}%)', + cpuCriticalImpact: 'Ответы системы замедляются, это может влиять на все запросы', + cpuCriticalAction: 'Проверьте CPU-интенсивные задачи, рассмотрите масштабирование или оптимизацию кода', + cpuHigh: 'Использование CPU повышено ({usage}%)', + cpuHighImpact: 'Нагрузка на систему высокая, требуется внимание', + cpuHighAction: 'Отслеживайте динамику CPU, подготовьте план масштабирования', + memoryCritical: 'Использование памяти критически высокое ({usage}%)', + memoryCriticalImpact: 'Может вызвать OOM, стабильность системы под угрозой', + memoryCriticalAction: 'Проверьте утечки памяти, рассмотрите увеличение памяти или оптимизацию использования', + memoryHigh: 'Использование памяти повышено ({usage}%)', + memoryHighImpact: 'Давление на память высокое, требуется внимание', + memoryHighAction: 'Отслеживайте динамику памяти, проверьте утечки памяти', + ttftHigh: 'Time to first token повышен ({ttft}ms)', + ttftHighImpact: 'Воспринимаемая пользователем задержка увеличилась', + ttftHighAction: 'Оптимизируйте обработку запросов, сократите время предобработки', // Error rate diagnostics - upstreamCritical: 'Upstream error rate critically high ({rate}%)', - upstreamCriticalImpact: 'May affect many user requests', - upstreamCriticalAction: 'Check upstream service health, enable fallback strategies', - upstreamHigh: 'Upstream error rate elevated ({rate}%)', - upstreamHighImpact: 'Recommend checking upstream service status', - upstreamHighAction: 'Contact upstream service team, prepare fallback plan', - errorHigh: 'Error rate too high ({rate}%)', - errorHighImpact: 'Many requests failing', - errorHighAction: 'Check error logs, identify root cause, urgent fix required', - errorElevated: 'Error rate elevated ({rate}%)', - errorElevatedImpact: 'Recommend checking error logs', - errorElevatedAction: 'Analyze error types and distribution, create fix plan', + upstreamCritical: 'Доля ошибок upstream критически высокая ({rate}%)', + upstreamCriticalImpact: 'Может повлиять на множество пользовательских запросов', + upstreamCriticalAction: 'Проверьте состояние upstream-сервиса, включите fallback-стратегии', + upstreamHigh: 'Доля ошибок upstream повышена ({rate}%)', + upstreamHighImpact: 'Рекомендуется проверить статус upstream-сервиса', + upstreamHighAction: 'Свяжитесь с командой upstream-сервиса, подготовьте fallback-план', + errorHigh: 'Доля ошибок слишком высокая ({rate}%)', + errorHighImpact: 'Многие запросы завершаются ошибкой', + errorHighAction: 'Проверьте логи ошибок, найдите причину, требуется срочное исправление', + errorElevated: 'Доля ошибок повышена ({rate}%)', + errorElevatedImpact: 'Рекомендуется проверить логи ошибок', + errorElevatedAction: 'Проанализируйте типы и распределение ошибок, подготовьте план исправления', // SLA diagnostics - slaCritical: 'SLA critically below target ({sla}%)', - slaCriticalImpact: 'User experience severely degraded', - slaCriticalAction: 'Urgently investigate errors and latency, consider rate limiting', - slaLow: 'SLA below target ({sla}%)', - slaLowImpact: 'Service quality needs attention', - slaLowAction: 'Analyze SLA decline causes, optimize system performance', + slaCritical: 'SLA критически ниже целевого значения ({sla}%)', + slaCriticalImpact: 'Пользовательский опыт сильно ухудшен', + slaCriticalAction: 'Срочно проверьте ошибки и задержки, рассмотрите ограничение частоты', + slaLow: 'SLA ниже целевого значения ({sla}%)', + slaLowImpact: 'Качество сервиса требует внимания', + slaLowAction: 'Проанализируйте причины снижения SLA, оптимизируйте производительность системы', // Health score diagnostics - healthCritical: 'Overall health score critically low ({score})', - healthCriticalImpact: 'Multiple metrics may be degraded; prioritize error rate and latency investigation', - healthCriticalAction: 'Comprehensive system check, prioritize critical-level issues', - healthLow: 'Overall health score low ({score})', - healthLowImpact: 'May indicate minor instability; monitor SLA and error rates', - healthLowAction: 'Monitor metric trends, prevent issue escalation', - healthy: 'All system metrics normal', - healthyImpact: 'Service running stable' + healthCritical: 'Общая оценка состояния критически низкая ({score})', + healthCriticalImpact: 'Несколько метрик могут быть ухудшены; сначала проверьте долю ошибок и задержки', + healthCriticalAction: 'Проведите комплексную проверку системы, приоритизируйте критические проблемы', + healthLow: 'Общая оценка состояния низкая ({score})', + healthLowImpact: 'Может указывать на небольшую нестабильность; отслеживайте SLA и долю ошибок', + healthLowAction: 'Отслеживайте динамику метрик, не допускайте эскалации проблемы', + healthy: 'Все системные метрики в норме', + healthyImpact: 'Сервис работает стабильно' }, // Error Log errorLog: { - timeId: 'Time / ID', + timeId: 'Время / ID', commonErrors: { contextDeadlineExceeded: 'context deadline exceeded', connectionRefused: 'connection refused', @@ -4706,29 +4706,29 @@ export default { }, time: 'Время', type: 'Тип', - context: 'Context', + context: 'Контекст', platform: 'Платформа', model: 'Модель', group: 'Группа', user: 'Пользователь', userId: 'ID пользователя', - account: 'Account', + account: 'Аккаунт', accountId: 'Account ID', status: 'Статус', - message: 'Message', - latency: 'Request Duration', - action: 'Action', - noErrors: 'No errors in this window.', + message: 'Сообщение', + latency: 'Длительность запроса', + action: 'Действие', + noErrors: 'В этом окне ошибок нет.', grp: 'GRP:', acc: 'ACC:', - details: 'Details', - phase: 'Phase', + details: 'Детали', + phase: 'Фаза', id: 'ID:', typeUpstream: 'Апстрим', - typeRequest: 'Request', + typeRequest: 'Запрос', typeAuth: 'Auth', - typeRouting: 'Routing', - typeInternal: 'Internal', + typeRouting: 'Маршрутизация', + typeInternal: 'Внутренняя', endpoint: 'Endpoint', requestType: 'Тип', requestTypeSync: 'Sync', @@ -4737,129 +4737,129 @@ export default { }, // Error Details Modal errorDetails: { - upstreamErrors: 'Upstream Errors', - requestErrors: 'Request Errors', - unresolved: 'Unresolved', - resolved: 'Resolved', - viewErrors: 'Errors', - viewExcluded: 'Excluded', + upstreamErrors: 'Ошибки upstream', + requestErrors: 'Ошибки запросов', + unresolved: 'Не решено', + resolved: 'Решено', + viewErrors: 'Ошибки', + viewExcluded: 'Исключено', statusCodeOther: 'Другое', owner: { provider: 'Провайдер', - client: 'Client', + client: 'Клиент', platform: 'Платформа' }, phase: { - request: 'Request', + request: 'Запрос', auth: 'Auth', - routing: 'Routing', - upstream: 'Апстрим', - network: 'Network', - internal: 'Internal' + routing: 'Маршрутизация', + upstream: 'upstream', + network: 'Сеть', + internal: 'Внутренняя' }, - total: 'Total:', - searchPlaceholder: 'Search request_id / client_request_id / message', + total: 'Всего:', + searchPlaceholder: 'Поиск request_id / client_request_id / message', }, // Error Detail Modal errorDetail: { - title: 'Error Detail', - titleWithId: 'Error #{id}', - noErrorSelected: 'No error selected.', - resolution: 'Resolved:', - failedToUpdateResolvedStatus: 'Failed to update resolved status', + title: 'Детали ошибки', + titleWithId: 'Ошибка #{id}', + noErrorSelected: 'Ошибка не выбрана.', + resolution: 'Решено:', + failedToUpdateResolvedStatus: 'Не удалось обновить статус решения', classificationKeys: { - phase: 'Phase', - owner: 'Owner', - source: 'Source', - resolvedAt: 'Resolved At', - resolvedBy: 'Resolved By' + phase: 'Фаза', + owner: 'Владелец', + source: 'Источник', + resolvedAt: 'Решено в', + resolvedBy: 'Решил' }, source: { upstream_http: 'Upstream HTTP' }, upstreamKeys: { status: 'Статус', - message: 'Message', - detail: 'Detail', - upstreamErrors: 'Upstream Errors' + message: 'Сообщение', + detail: 'Детали', + upstreamErrors: 'Ошибки upstream' }, upstreamEvent: { - account: 'Account', + account: 'Аккаунт', status: 'Статус', requestId: 'Request ID' }, responsePreview: { - expand: 'Response (click to expand)', - collapse: 'Response (click to collapse)' + expand: 'Response (нажмите, чтобы раскрыть)', + collapse: 'Response (нажмите, чтобы свернуть)' }, - loading: 'Loading…', + loading: 'Загрузка…', requestId: 'Request ID', time: 'Время', - phase: 'Phase', + phase: 'Фаза', status: 'Статус', - message: 'Message', - basicInfo: 'Basic Info', + message: 'Сообщение', + basicInfo: 'Основная информация', platform: 'Платформа', model: 'Модель', group: 'Группа', user: 'Пользователь', - account: 'Account', - latency: 'Request Duration', - businessLimited: 'Business Limited', - requestPath: 'Request Path', + account: 'Аккаунт', + latency: 'Длительность запроса', + businessLimited: 'Бизнес-лимит', + requestPath: 'Путь запроса', inboundEndpoint: 'Входящий endpoint', upstreamEndpoint: 'Апстрим endpoint', - requestedModel: 'Requested Model', - upstreamModel: 'Upstream Model', - requestType: 'Request Type', + requestedModel: 'Запрошенная модель', + upstreamModel: 'Модель upstream', + requestType: 'Тип запроса', requestTypeUnknown: 'Неизвестно', requestTypeSync: 'Sync', requestTypeStream: 'Stream', requestTypeWs: 'WebSocket', modelMapping: 'Model Mapping', - timings: 'Timings', + timings: 'Тайминги', auth: 'Auth', - routing: 'Routing', - upstream: 'Апстрим', + routing: 'Маршрутизация', + upstream: 'upstream', response: 'Response', - classification: 'Classification', - errorBody: 'Error Body', - trimmed: 'trimmed', - markResolved: 'Mark resolved', - markUnresolved: 'Mark unresolved', - tabOverview: 'Overview', + classification: 'Классификация', + errorBody: 'Тело ошибки', + trimmed: 'обрезано', + markResolved: 'Пометить решённой', + markUnresolved: 'Пометить нерешённой', + tabOverview: 'Обзор', tabRequest: 'Request', tabResponse: 'Response', responseBody: 'Response', compareA: 'Compare A', compareB: 'Compare B', - suggestion: 'Suggestion', - suggestUpstream: 'Upstream instability: check account status or consider switching accounts', - suggestRequest: 'Client request error: ask customer to fix request parameters', - suggestAuth: 'Auth failed: verify API key/credentials', - suggestPlatform: 'Platform error: prioritize investigation and fix', - suggestGeneric: 'See details for more context' + suggestion: 'Рекомендация', + suggestUpstream: 'Нестабильность upstream: проверьте статус аккаунта или рассмотрите переключение аккаунтов', + suggestRequest: 'Ошибка клиентского запроса: попросите клиента исправить параметры запроса', + suggestAuth: 'Ошибка Auth: проверьте API key/credentials', + suggestPlatform: 'Ошибка платформы: приоритизируйте расследование и исправление', + suggestGeneric: 'Смотрите детали для дополнительного контекста' }, requestDetails: { - title: 'Request Details', - details: 'Details', - rangeLabel: 'Window: {range}', - rangeMinutes: '{n} minutes', - rangeHours: '{n} hours', - empty: 'No requests in this window.', - emptyHint: 'Try a different time range or remove filters.', - failedToLoad: 'Failed to load request details', - requestIdCopied: 'Request ID copied', - copyFailed: 'Copy failed', + title: 'Детали запроса', + details: 'Детали', + rangeLabel: 'Окно: {range}', + rangeMinutes: '{n} минут', + rangeHours: '{n} часов', + empty: 'В этом окне запросов нет.', + emptyHint: 'Попробуйте другой диапазон времени или уберите фильтры.', + failedToLoad: 'Не удалось загрузить детали запроса', + requestIdCopied: 'Request ID скопирован', + copyFailed: 'Не удалось скопировать', copy: 'Копировать', - viewError: 'View Error', + viewError: 'Посмотреть ошибку', kind: { success: 'SUCCESS', error: 'ERROR' }, table: { time: 'Время', - kind: 'Kind', + kind: 'Тип', platform: 'Платформа', model: 'Модель', duration: 'Длительность', @@ -4869,348 +4869,348 @@ export default { } }, alertEvents: { - title: 'Alert Events', - description: 'Recent alert firing/resolution records (email-only)', + title: 'События оповещений', + description: 'Последние записи срабатывания/решения оповещений (только email)', loading: 'Загрузка...', - empty: 'No alert events', - loadFailed: 'Failed to load alert events', + empty: 'Событий оповещений нет', + loadFailed: 'Не удалось загрузить события оповещений', status: { firing: 'FIRING', resolved: 'RESOLVED', manualResolved: 'MANUAL RESOLVED' }, detail: { - title: 'Alert Detail', - loading: 'Loading detail...', - empty: 'No detail', - loadFailed: 'Failed to load alert detail', - manualResolve: 'Mark as Resolved', - manualResolvedSuccess: 'Marked as manually resolved', - manualResolvedFailed: 'Failed to mark as manually resolved', - silence: 'Ignore Alert', - silenceSuccess: 'Alert silenced', - silenceFailed: 'Failed to silence alert', - viewRule: 'View Rule', - viewLogs: 'View Logs', - firedAt: 'Fired At', - resolvedAt: 'Resolved At', + title: 'Детали оповещения', + loading: 'Загрузка деталей...', + empty: 'Деталей нет', + loadFailed: 'Не удалось загрузить детали оповещения', + manualResolve: 'Пометить как решённое', + manualResolvedSuccess: 'Помечено как решённое вручную', + manualResolvedFailed: 'Не удалось пометить как решённое вручную', + silence: 'Заглушить оповещение', + silenceSuccess: 'Оповещение заглушено', + silenceFailed: 'Не удалось заглушить оповещение', + viewRule: 'Посмотреть правило', + viewLogs: 'Посмотреть логи', + firedAt: 'Сработало в', + resolvedAt: 'Решено в', ruleId: 'Rule ID', - dimensions: 'Dimensions', - historyTitle: 'History', - historyHint: 'Recent events with same rule + dimensions', - historyLoading: 'Loading history...', - historyEmpty: 'No history' + dimensions: 'Измерения', + historyTitle: 'История', + historyHint: 'Последние события с тем же правилом + измерениями', + historyLoading: 'Загрузка истории...', + historyEmpty: 'Истории нет' }, table: { time: 'Время', status: 'Статус', - severity: 'Severity', + severity: 'Важность', platform: 'Платформа', ruleId: 'Rule ID', - title: 'Title', + title: 'Заголовок', duration: 'Длительность', - metric: 'Metric / Threshold', - dimensions: 'Dimensions', - email: 'Email Sent', - emailSent: 'Sent', - emailIgnored: 'Ignored' + metric: 'Метрика / порог', + dimensions: 'Измерения', + email: 'Email отправлен', + emailSent: 'Отправлено', + emailIgnored: 'Игнорировано' } }, alertRules: { - title: 'Alert Rules', - description: 'Create and manage threshold-based system alerts (email-only)', + title: 'Правила оповещений', + description: 'Создавайте и управляйте системными оповещениями по порогам (только email)', loading: 'Загрузка...', - empty: 'No alert rules', - loadFailed: 'Failed to load alert rules', - saveFailed: 'Failed to save alert rule', - saveSuccess: 'Alert rule saved successfully', - deleteFailed: 'Failed to delete alert rule', - deleteSuccess: 'Alert rule deleted successfully', - manage: 'Manage Alert Rules', - create: 'Create Rule', - createTitle: 'Create Alert Rule', - editTitle: 'Edit Alert Rule', - deleteConfirmTitle: 'Delete this rule?', - deleteConfirmMessage: 'This will remove the rule and its related events. Continue?', + empty: 'Правил оповещений нет', + loadFailed: 'Не удалось загрузить правила оповещений', + saveFailed: 'Не удалось сохранить правило оповещения', + saveSuccess: 'Правило оповещения сохранено', + deleteFailed: 'Не удалось удалить правило оповещения', + deleteSuccess: 'Правило оповещения удалено', + manage: 'Управлять правилами оповещений', + create: 'Создать правило', + createTitle: 'Создать правило оповещения', + editTitle: 'Изменить правило оповещения', + deleteConfirmTitle: 'Удалить это правило?', + deleteConfirmMessage: 'Это удалит правило и связанные события. Продолжить?', metricGroups: { - system: 'System Metrics', - group: 'Group-level Metrics (requires group_id)', - account: 'Account-level Metrics' + system: 'Системные метрики', + group: 'Метрики уровня группы (требуется group_id)', + account: 'Метрики уровня аккаунта' }, metrics: { - successRate: 'Success Rate (%)', - errorRate: 'Error Rate (%)', - upstreamErrorRate: 'Upstream Error Rate (%)', - p95: 'P95 Latency (ms)', - p99: 'P99 Latency (ms)', - cpu: 'CPU Usage (%)', - memory: 'Memory Usage (%)', - queueDepth: 'Concurrency Queue Depth', - groupAvailableAccounts: 'Group Available Accounts', - groupAvailableRatio: 'Group Available Ratio (%)', - groupRateLimitRatio: 'Group Rate Limit Ratio (%)', - accountRateLimitedCount: 'Rate-limited Accounts', - accountErrorCount: 'Error Accounts (excluding temporarily unschedulable)', - accountErrorRatio: 'Error Account Ratio (%)', - overloadAccountCount: 'Overloaded Accounts' + successRate: 'Доля успешных запросов (%)', + errorRate: 'Доля ошибок (%)', + upstreamErrorRate: 'Доля ошибок upstream (%)', + p95: 'P95 задержка (ms)', + p99: 'P99 задержка (ms)', + cpu: 'Использование CPU (%)', + memory: 'Использование памяти (%)', + queueDepth: 'Глубина очереди параллелизма', + groupAvailableAccounts: 'Доступные аккаунты группы', + groupAvailableRatio: 'Доля доступности группы (%)', + groupRateLimitRatio: 'Доля rate limit в группе (%)', + accountRateLimitedCount: 'Аккаунты с rate limit', + accountErrorCount: 'Аккаунты с ошибками (без временно исключённых из маршрутизации)', + accountErrorRatio: 'Доля аккаунтов с ошибками (%)', + overloadAccountCount: 'Перегруженные аккаунты' }, metricDescriptions: { - successRate: 'Percentage of successful requests in the window (0-100).', - errorRate: 'Percentage of failed requests in the window (0-100).', - upstreamErrorRate: 'Percentage of upstream failures in the window (0-100).', - p95: 'P95 request latency within the window (ms).', - p99: 'P99 request latency within the window (ms).', - cpu: 'Current instance CPU usage (0-100).', - memory: 'Current instance memory usage (0-100).', - queueDepth: 'Concurrency queue depth within the window (queued requests).', - groupAvailableAccounts: 'Number of available accounts in the selected group (requires group_id).', - groupAvailableRatio: 'Available account ratio in the selected group (0-100, requires group_id).', - groupRateLimitRatio: 'Rate-limited account ratio in the selected group (0-100, requires group_id).', - accountRateLimitedCount: 'Number of rate-limited accounts within the window.', - accountErrorCount: 'Number of error accounts within the window (excluding temporarily unschedulable).', - accountErrorRatio: 'Error account ratio within the window (0-100).', - overloadAccountCount: 'Number of overloaded accounts within the window.' + successRate: 'Доля успешных запросов в окне (0-100).', + errorRate: 'Доля неуспешных запросов в окне (0-100).', + upstreamErrorRate: 'Доля ошибок upstream в окне (0-100).', + p95: 'P95 задержка запросов в окне (ms).', + p99: 'P99 задержка запросов в окне (ms).', + cpu: 'Текущее использование CPU инстансом (0-100).', + memory: 'Текущее использование памяти инстансом (0-100).', + queueDepth: 'Глубина очереди параллелизма в окне (запросы в очереди).', + groupAvailableAccounts: 'Количество доступных аккаунтов в выбранной группе (требуется group_id).', + groupAvailableRatio: 'Доля доступных аккаунтов в выбранной группе (0-100, требуется group_id).', + groupRateLimitRatio: 'Доля аккаунтов с rate limit в выбранной группе (0-100, требуется group_id).', + accountRateLimitedCount: 'Количество аккаунтов с rate limit в окне.', + accountErrorCount: 'Количество аккаунтов с ошибками в окне (без временно исключённых из маршрутизации).', + accountErrorRatio: 'Доля аккаунтов с ошибками в окне (0-100).', + overloadAccountCount: 'Количество перегруженных аккаунтов в окне.' }, hints: { - recommended: 'Recommended: operator {operator}, threshold {threshold}{unit}', - groupRequired: 'This is a group-level metric; selecting a group (group_id) is required.', - groupOptional: 'Optional: limit the rule to a specific group via group_id.' + recommended: 'Рекомендуется: оператор {operator}, порог {threshold}{unit}', + groupRequired: 'Это метрика уровня группы; выбор группы (group_id) обязателен.', + groupOptional: 'Необязательно: ограничить правило конкретной группой через group_id.' }, table: { name: 'Имя', - metric: 'Metric', - severity: 'Severity', + metric: 'Метрика', + severity: 'Важность', enabled: 'Включено', actions: 'Действия' }, form: { name: 'Имя', description: 'Описание', - metric: 'Metric', - operator: 'Operator', + metric: 'Метрика', + operator: 'Оператор', groupId: 'Group (group_id)', groupPlaceholder: 'Выберите группу', - allGroups: 'All groups', - threshold: 'Threshold', - severity: 'Severity', - window: 'Window (minutes)', - sustained: 'Sustained (samples)', - cooldown: 'Cooldown (minutes)', + allGroups: 'Все группы', + threshold: 'Порог', + severity: 'Важность', + window: 'Окно (минуты)', + sustained: 'Устойчиво (samples)', + cooldown: 'Cooldown (минуты)', enabled: 'Включено', - notifyEmail: 'Send email notifications' + notifyEmail: 'Отправлять email-уведомления' }, validation: { - title: 'Please fix the following issues', - invalid: 'Invalid rule', - nameRequired: 'Name is required', - metricRequired: 'Metric is required', - groupIdRequired: 'group_id is required for group-level metrics', - operatorRequired: 'Operator is required', - thresholdRequired: 'Threshold must be a number', - windowRange: 'Window must be one of: 1, 5, 60 minutes', - sustainedRange: 'Sustained must be between 1 and 1440 samples', - cooldownRange: 'Cooldown must be between 0 and 1440 minutes' + title: 'Исправьте следующие проблемы', + invalid: 'Некорректное правило', + nameRequired: 'Имя обязательно', + metricRequired: 'Метрика обязательна', + groupIdRequired: 'group_id обязателен для метрик уровня группы', + operatorRequired: 'Оператор обязателен', + thresholdRequired: 'Порог должен быть числом', + windowRange: 'Окно должно быть одним из значений: 1, 5, 60 минут', + sustainedRange: 'Sustained должен быть между 1 и 1440 samples', + cooldownRange: 'Cooldown должен быть между 0 и 1440 минут' } }, runtime: { - title: 'Ops Runtime Settings', - description: 'Stored in database; changes take effect without editing config files.', + title: 'ops runtime-настройки', + description: 'Хранится в базе данных; изменения вступают в силу без редактирования config-файлов.', loading: 'Загрузка...', - noData: 'No runtime settings available', - loadFailed: 'Failed to load runtime settings', - saveSuccess: 'Runtime settings saved', - saveFailed: 'Failed to save runtime settings', - alertTitle: 'Alert Evaluator', - groupAvailabilityTitle: 'Group Availability Monitor', - evalIntervalSeconds: 'Evaluation Interval (seconds)', + noData: 'runtime-настройки недоступны', + loadFailed: 'Не удалось загрузить runtime-настройки', + saveSuccess: 'runtime-настройки сохранены', + saveFailed: 'Не удалось сохранить runtime-настройки', + alertTitle: 'Оценщик оповещений', + groupAvailabilityTitle: 'Монитор доступности групп', + evalIntervalSeconds: 'Интервал оценки (секунды)', silencing: { - title: 'Alert Silencing (Maintenance Mode)', - enabled: 'Enable silencing', - globalUntil: 'Silence until (RFC3339)', - untilHint: 'Leave empty to only toggle silencing without an expiry (not recommended).', - reason: 'Reason', - reasonPlaceholder: 'e.g., planned maintenance', + title: 'Заглушение оповещений (режим обслуживания)', + enabled: 'Включить заглушение', + globalUntil: 'Заглушить до (RFC3339)', + untilHint: 'Оставьте пустым, чтобы только переключить заглушение без срока действия (не рекомендуется).', + reason: 'Причина', + reasonPlaceholder: 'например, плановое обслуживание', entries: { - title: 'Advanced: targeted silencing', - hint: 'Optional: silence only certain rules or severities. Leave fields empty to match all.', - add: 'Add Entry', - empty: 'No targeted entries', - entryTitle: 'Entry #{n}', - ruleId: 'Rule ID (optional)', + title: 'Дополнительно: точечное заглушение', + hint: 'Необязательно: заглушить только отдельные правила или уровни важности. Оставьте поля пустыми, чтобы применить ко всем.', + add: 'Добавить запись', + empty: 'Точечных записей нет', + entryTitle: 'Запись #{n}', + ruleId: 'Rule ID (необязательно)', ruleIdPlaceholder: 'e.g., 1', - severities: 'Severities (optional)', - severitiesPlaceholder: 'e.g., P0,P1 (empty = all)', - until: 'Until (RFC3339)', - reason: 'Reason', + severities: 'Уровни важности (необязательно)', + severitiesPlaceholder: 'например, P0,P1 (пусто = все)', + until: 'До (RFC3339)', + reason: 'Причина', validation: { - untilRequired: 'Entry until time is required', - untilFormat: 'Entry until time must be a valid RFC3339 timestamp', - ruleIdPositive: 'Entry rule_id must be a positive integer', - severitiesFormat: 'Entry severities must be a comma-separated list of P0..P3' + untilRequired: 'Время окончания записи обязательно', + untilFormat: 'Время окончания записи должно быть корректным timestamp RFC3339', + ruleIdPositive: 'rule_id записи должен быть положительным целым числом', + severitiesFormat: 'Уровни важности записи должны быть списком P0..P3 через запятую' } }, validation: { - timeFormat: 'Silence time must be a valid RFC3339 timestamp' + timeFormat: 'Время заглушения должно быть корректным timestamp RFC3339' } }, - lockEnabled: 'Distributed Lock Enabled', - lockKey: 'Distributed Lock Key', - lockTTLSeconds: 'Distributed Lock TTL (seconds)', - showAdvancedDeveloperSettings: 'Show advanced developer settings (Distributed Lock)', - advancedSettingsSummary: 'Advanced settings (Distributed Lock)', - evalIntervalHint: 'How often the evaluator runs. Keeping the default is recommended.', + lockEnabled: 'Распределённая блокировка включена', + lockKey: 'Ключ распределённой блокировки', + lockTTLSeconds: 'TTL распределённой блокировки (секунды)', + showAdvancedDeveloperSettings: 'Показать расширенные настройки разработчика (распределённая блокировка)', + advancedSettingsSummary: 'Расширенные настройки (распределённая блокировка)', + evalIntervalHint: 'Как часто запускается оценщик. Рекомендуется оставить значение по умолчанию.', validation: { - title: 'Please fix the following issues', - invalid: 'Invalid settings', - evalIntervalRange: 'Evaluation interval must be between 1 and 86400 seconds', - lockKeyRequired: 'Distributed lock key is required when lock is enabled', - lockKeyPrefix: 'Distributed lock key must start with "{prefix}"', - lockKeyHint: 'Recommended: start with "{prefix}" to avoid conflicts', - lockTtlRange: 'Distributed lock TTL must be between 1 and 86400 seconds', - slaMinPercentRange: 'SLA minimum percentage must be between 0 and 100', - ttftP99MaxRange: 'TTFT P99 maximum must be a number ≥ 0', - requestErrorRateMaxRange: 'Request error rate maximum must be between 0 and 100', - upstreamErrorRateMaxRange: 'Upstream error rate maximum must be between 0 and 100' + title: 'Исправьте следующие проблемы', + invalid: 'Некорректные настройки', + evalIntervalRange: 'Интервал оценки должен быть от 1 до 86400 секунд', + lockKeyRequired: 'Ключ распределённой блокировки обязателен, когда блокировка включена', + lockKeyPrefix: 'Ключ распределённой блокировки должен начинаться с "{prefix}"', + lockKeyHint: 'Рекомендуется начинать с "{prefix}", чтобы избежать конфликтов', + lockTtlRange: 'TTL распределённой блокировки должен быть от 1 до 86400 секунд', + slaMinPercentRange: 'Минимальный процент SLA должен быть от 0 до 100', + ttftP99MaxRange: 'Максимум TTFT P99 должен быть числом ≥ 0', + requestErrorRateMaxRange: 'Максимальная доля ошибок запросов должна быть от 0 до 100', + upstreamErrorRateMaxRange: 'Максимальная доля ошибок upstream должна быть от 0 до 100' } }, email: { - title: 'Email Notification', - description: 'Configure alert/report email notifications (stored in database).', + title: 'Email-уведомления', + description: 'Настройте email-оповещения и email-отчёты (хранятся в базе данных).', loading: 'Загрузка...', - noData: 'No email notification config', - loadFailed: 'Failed to load email notification config', - saveSuccess: 'Email notification config saved', - saveFailed: 'Failed to save email notification config', - alertTitle: 'Alert Emails', - reportTitle: 'Report Emails', - recipients: 'Recipients', - recipientsHint: 'If empty, the system may fallback to the first admin email.', - minSeverity: 'Min Severity', - minSeverityAll: 'All severities', - rateLimitPerHour: 'Rate limit per hour', - batchWindowSeconds: 'Batch window (seconds)', - includeResolved: 'Include resolved alerts', - dailySummary: 'Daily summary', - weeklySummary: 'Weekly summary', - errorDigest: 'Error digest', - errorDigestMinCount: 'Min errors for digest', - accountHealth: 'Account health', - accountHealthThreshold: 'Error rate threshold (%)', - cronPlaceholder: 'Cron expression', - reportHint: 'Schedules use cron syntax; leave empty to use defaults.', + noData: 'Настройки email-уведомлений отсутствуют', + loadFailed: 'Не удалось загрузить настройки email-уведомлений', + saveSuccess: 'Настройки email-уведомлений сохранены', + saveFailed: 'Не удалось сохранить настройки email-уведомлений', + alertTitle: 'Email-оповещения', + reportTitle: 'Email-отчёты', + recipients: 'Получатели', + recipientsHint: 'Если пусто, система может использовать email первого админа.', + minSeverity: 'Мин. важность', + minSeverityAll: 'Все уровни важности', + rateLimitPerHour: 'Лимит частоты в час', + batchWindowSeconds: 'Batch-окно (секунды)', + includeResolved: 'Включать решённые оповещения', + dailySummary: 'Ежедневная сводка', + weeklySummary: 'Еженедельная сводка', + errorDigest: 'Дайджест ошибок', + errorDigestMinCount: 'Мин. ошибок для дайджеста', + accountHealth: 'Состояние аккаунтов', + accountHealthThreshold: 'Порог доли ошибок (%)', + cronPlaceholder: 'Cron-выражение', + reportHint: 'Расписания используют синтаксис cron; оставьте пустым, чтобы использовать значения по умолчанию.', validation: { - title: 'Please fix the following issues', - invalid: 'Invalid email notification config', - alertRecipientsRequired: 'Alert emails are enabled but no recipients are configured', - reportRecipientsRequired: 'Report emails are enabled but no recipients are configured', - invalidRecipients: 'One or more recipient emails are invalid', - rateLimitRange: 'Rate limit per hour must be a number ≥ 0', - batchWindowRange: 'Batch window must be between 0 and 86400 seconds', - cronRequired: 'A cron expression is required when schedule is enabled', - cronFormat: 'Cron expression format looks invalid (expected at least 5 parts)', - digestMinCountRange: 'Min errors for digest must be a number ≥ 0', - accountHealthThresholdRange: 'Account health threshold must be between 0 and 100' + title: 'Исправьте следующие проблемы', + invalid: 'Некорректные настройки email-уведомлений', + alertRecipientsRequired: 'Email-оповещения включены, но получатели не настроены', + reportRecipientsRequired: 'Email-отчёты включены, но получатели не настроены', + invalidRecipients: 'Один или несколько email получателей некорректны', + rateLimitRange: 'Лимит частоты в час должен быть числом ≥ 0', + batchWindowRange: 'Batch-окно должно быть от 0 до 86400 секунд', + cronRequired: 'Cron-выражение обязательно, когда расписание включено', + cronFormat: 'Формат Cron-выражения выглядит некорректно (ожидается минимум 5 частей)', + digestMinCountRange: 'Мин. ошибок для дайджеста должно быть числом ≥ 0', + accountHealthThresholdRange: 'Порог состояния аккаунтов должен быть от 0 до 100' } }, settings: { - title: 'Ops Monitoring Settings', - loadFailed: 'Failed to load settings', - saveSuccess: 'Ops monitoring settings saved successfully', - saveFailed: 'Failed to save settings', - dataCollection: 'Data Collection', - evaluationInterval: 'Evaluation Interval (seconds)', - evaluationIntervalHint: 'Frequency of detection tasks, recommended to keep default', - alertConfig: 'Alert Configuration', - enableAlert: 'Enable Alerts', - alertRecipients: 'Alert Recipient Emails', + title: 'Настройки ops-мониторинга', + loadFailed: 'Не удалось загрузить настройки', + saveSuccess: 'Настройки ops-мониторинга сохранены', + saveFailed: 'Не удалось сохранить настройки', + dataCollection: 'Сбор данных', + evaluationInterval: 'Интервал оценки (секунды)', + evaluationIntervalHint: 'Частота задач обнаружения; рекомендуется оставить значение по умолчанию', + alertConfig: 'Настройка оповещений', + enableAlert: 'Включить оповещения', + alertRecipients: 'Email получателей оповещений', emailPlaceholder: 'Введите email', - recipientsHint: 'If empty, the system will use the first admin email as default recipient', - minSeverity: 'Minimum Severity', - reportConfig: 'Report Configuration', - enableReport: 'Enable Reports', - reportRecipients: 'Report Recipient Emails', - dailySummary: 'Daily Summary', - weeklySummary: 'Weekly Summary', - metricThresholds: 'Metric Thresholds', - metricThresholdsHint: 'Configure alert thresholds for metrics, values exceeding thresholds will be displayed in red', - slaMinPercent: 'SLA Minimum Percentage', - slaMinPercentHint: 'SLA below this value will be displayed in red (default: 99.5%)', - ttftP99MaxMs: 'TTFT P99 Maximum (ms)', - ttftP99MaxMsHint: 'TTFT P99 above this value will be displayed in red (default: 500ms)', - requestErrorRateMaxPercent: 'Request Error Rate Maximum (%)', - requestErrorRateMaxPercentHint: 'Request error rate above this value will be displayed in red (default: 5%)', - upstreamErrorRateMaxPercent: 'Upstream Error Rate Maximum (%)', - upstreamErrorRateMaxPercentHint: 'Upstream error rate above this value will be displayed in red (default: 5%)', - advancedSettings: 'Advanced Settings', - dataRetention: 'Data Retention Policy', - enableCleanup: 'Enable Data Cleanup', - cleanupSchedule: 'Cleanup Schedule (Cron)', - cleanupScheduleHint: 'Example: 0 2 * * * means 2 AM daily', - errorLogRetentionDays: 'Error Log Retention Days', - minuteMetricsRetentionDays: 'Minute Metrics Retention Days', - hourlyMetricsRetentionDays: 'Hourly Metrics Retention Days', - retentionDaysHint: 'Recommended 7-90 days; longer periods consume more storage. Set to 0 to wipe all history on every scheduled cleanup', - aggregation: 'Pre-aggregation Tasks', - enableAggregation: 'Enable Pre-aggregation', - aggregationHint: 'Pre-aggregation improves query performance for long time windows', - errorFiltering: 'Error Filtering', - ignoreCountTokensErrors: 'Ignore count_tokens errors', - ignoreCountTokensErrorsHint: 'When enabled, errors from count_tokens requests will not be written to the error log.', - ignoreContextCanceled: 'Ignore client disconnect errors', - ignoreContextCanceledHint: 'When enabled, client disconnect (context canceled) errors will not be written to the error log.', - ignoreNoAvailableAccounts: 'Ignore no available accounts errors', - ignoreNoAvailableAccountsHint: 'When enabled, "No available accounts" errors will not be written to the error log (not recommended; usually a config issue).', - ignoreInvalidApiKeyErrors: 'Ignore invalid API key errors', - ignoreInvalidApiKeyErrorsHint: 'When enabled, invalid or missing API key errors (INVALID_API_KEY, API_KEY_REQUIRED) will not be written to the error log.', - ignoreInsufficientBalanceErrors: 'Ignore Insufficient Balance Errors', - ignoreInsufficientBalanceErrorsHint: 'When enabled, insufficient account balance errors will not be written to the error log.', + recipientsHint: 'Если пусто, система будет использовать email первого админа как получателя по умолчанию', + minSeverity: 'Минимальная важность', + reportConfig: 'Настройка отчётов', + enableReport: 'Включить отчёты', + reportRecipients: 'Email получателей отчётов', + dailySummary: 'Ежедневная сводка', + weeklySummary: 'Еженедельная сводка', + metricThresholds: 'Пороги метрик', + metricThresholdsHint: 'Настройте пороги оповещений для метрик; значения выше порогов будут показаны красным', + slaMinPercent: 'Минимальный процент SLA', + slaMinPercentHint: 'SLA ниже этого значения будет показан красным (по умолчанию: 99.5%)', + ttftP99MaxMs: 'Максимум TTFT P99 (ms)', + ttftP99MaxMsHint: 'TTFT P99 выше этого значения будет показан красным (по умолчанию: 500ms)', + requestErrorRateMaxPercent: 'Максимальная доля ошибок запросов (%)', + requestErrorRateMaxPercentHint: 'Доля ошибок запросов выше этого значения будет показана красным (по умолчанию: 5%)', + upstreamErrorRateMaxPercent: 'Максимальная доля ошибок upstream (%)', + upstreamErrorRateMaxPercentHint: 'Доля ошибок upstream выше этого значения будет показана красным (по умолчанию: 5%)', + advancedSettings: 'Расширенные настройки', + dataRetention: 'Политика хранения данных', + enableCleanup: 'Включить очистку данных', + cleanupSchedule: 'Расписание очистки (Cron)', + cleanupScheduleHint: 'Пример: 0 2 * * * означает ежедневно в 2 AM', + errorLogRetentionDays: 'Дней хранения лога ошибок', + minuteMetricsRetentionDays: 'Дней хранения минутных метрик', + hourlyMetricsRetentionDays: 'Дней хранения часовых метрик', + retentionDaysHint: 'Рекомендуется 7–90 дней; более длительные периоды расходуют больше хранилища. Установите 0, чтобы удалять всю историю при каждой запланированной очистке', + aggregation: 'Задачи предагрегации', + enableAggregation: 'Включить предагрегацию', + aggregationHint: 'Предагрегация повышает производительность запросов для длинных временных окон', + errorFiltering: 'Фильтрация ошибок', + ignoreCountTokensErrors: 'Игнорировать ошибки count_tokens', + ignoreCountTokensErrorsHint: 'Если включено, ошибки запросов count_tokens не будут записываться в лог ошибок.', + ignoreContextCanceled: 'Игнорировать ошибки отключения клиента', + ignoreContextCanceledHint: 'Если включено, ошибки отключения клиента (context canceled) не будут записываться в лог ошибок.', + ignoreNoAvailableAccounts: 'Игнорировать ошибки отсутствия доступных аккаунтов', + ignoreNoAvailableAccountsHint: 'Если включено, ошибки "No available accounts" не будут записываться в лог ошибок (не рекомендуется; обычно это проблема конфигурации).', + ignoreInvalidApiKeyErrors: 'Игнорировать ошибки некорректного API key', + ignoreInvalidApiKeyErrorsHint: 'Если включено, ошибки некорректного или отсутствующего API key (INVALID_API_KEY, API_KEY_REQUIRED) не будут записываться в лог ошибок.', + ignoreInsufficientBalanceErrors: 'Игнорировать ошибки недостаточного баланса', + ignoreInsufficientBalanceErrorsHint: 'Если включено, ошибки недостаточного баланса аккаунта не будут записываться в лог ошибок.', autoRefresh: 'Автообновление', enableAutoRefresh: 'Включить автообновление', - enableAutoRefreshHint: 'Automatically refresh dashboard data at a fixed interval.', - refreshInterval: 'Refresh Interval', - refreshInterval15s: '15 seconds', - refreshInterval30s: '30 seconds', - refreshInterval60s: '60 seconds', - dashboardCards: 'Dashboard Cards', - displayAlertEvents: 'Display alert events', - displayAlertEventsHint: 'Show or hide the recent alert events card on the ops dashboard. Enabled by default.', - displayOpenAITokenStats: 'Display OpenAI token request stats', - displayOpenAITokenStatsHint: 'Show or hide the OpenAI token request stats card on the ops dashboard. Hidden by default.', + enableAutoRefreshHint: 'Автоматически обновлять данные dashboard с фиксированным интервалом.', + refreshInterval: 'Интервал обновления', + refreshInterval15s: '15 секунд', + refreshInterval30s: '30 секунд', + refreshInterval60s: '60 секунд', + dashboardCards: 'Карточки dashboard', + displayAlertEvents: 'Показывать события оповещений', + displayAlertEventsHint: 'Показать или скрыть карточку последних событий оповещений на ops dashboard. По умолчанию включено.', + displayOpenAITokenStats: 'Показывать статистику запросов Token OpenAI', + displayOpenAITokenStatsHint: 'Показать или скрыть карточку статистики запросов Token OpenAI на ops dashboard. По умолчанию скрыто.', autoRefreshCountdown: 'Автообновление: {seconds}с', validation: { - title: 'Please fix the following issues', - retentionDaysRange: 'Retention days must be between 0 and 365 (0 = wipe all on every cleanup)', - slaMinPercentRange: 'SLA minimum percentage must be between 0 and 100', - ttftP99MaxRange: 'TTFT P99 maximum must be a number ≥ 0', - requestErrorRateMaxRange: 'Request error rate maximum must be between 0 and 100', - upstreamErrorRateMaxRange: 'Upstream error rate maximum must be between 0 and 100' + title: 'Исправьте следующие проблемы', + retentionDaysRange: 'Дни хранения должны быть от 0 до 365 (0 = удалять всё при каждой очистке)', + slaMinPercentRange: 'Минимальный процент SLA должен быть от 0 до 100', + ttftP99MaxRange: 'Максимум TTFT P99 должен быть числом ≥ 0', + requestErrorRateMaxRange: 'Максимальная доля ошибок запросов должна быть от 0 до 100', + upstreamErrorRateMaxRange: 'Максимальная доля ошибок upstream должна быть от 0 до 100' } }, concurrency: { - title: 'Concurrency / Queue', - byPlatform: 'By Platform', - byGroup: 'By Group', - byAccount: 'By Account', - byUser: 'By User', - showByUserTooltip: 'Switch to user view to see concurrency usage per user', - switchToUser: 'Switch to user view', - switchToPlatform: 'Switch to platform view', - totalRows: '{count} rows', - disabledHint: 'Realtime monitoring is disabled in settings.', + title: 'Параллелизм / очередь', + byPlatform: 'По платформе', + byGroup: 'По группе', + byAccount: 'По аккаунту', + byUser: 'По пользователю', + showByUserTooltip: 'Переключитесь на вид по пользователям, чтобы увидеть использование параллелизма по каждому пользователю', + switchToUser: 'Переключиться на вид по пользователям', + switchToPlatform: 'Переключиться на вид по платформам', + totalRows: 'Строк: {count}', + disabledHint: 'Realtime-мониторинг отключён в настройках.', empty: 'Нет данных', - queued: 'Queue {count}', - rateLimited: 'Rate-limited {count}', - errorAccounts: 'Errors {count}', - loadFailed: 'Failed to load concurrency data' + queued: 'Очередь {count}', + rateLimited: 'Ограничено по лимиту {count}', + errorAccounts: 'Ошибки {count}', + loadFailed: 'Не удалось загрузить данные параллелизма' }, realtime: { title: 'Realtime', - connected: 'Realtime connected', - connecting: 'Realtime connecting', - reconnecting: 'Realtime reconnecting', + connected: 'Realtime подключён', + connecting: 'Realtime подключается', + reconnecting: 'Realtime переподключается', offline: 'Realtime offline', - closed: 'Realtime closed', - reconnectIn: 'retry in {seconds}s' + closed: 'Realtime закрыт', + reconnectIn: 'повтор через {seconds}с' }, queryMode: { auto: 'Auto', @@ -5219,289 +5219,289 @@ export default { }, accountAvailability: { available: 'Доступно', - unavailable: 'Unavailable', + unavailable: 'Недоступно', accountError: 'Ошибка' }, tooltips: { - totalRequests: 'Total number of requests (including both successful and failed requests) in the selected time window.', - throughputTrend: 'Requests/QPS + Tokens/TPS in the selected window.', - switchRateTrend: 'Trend of account switches / total requests over the last 5 hours (avg switches).', - latencyHistogram: 'Request duration distribution (ms) for successful requests.', - errorTrend: 'Error counts over time (SLA scope excludes business limits; upstream excludes 429/529).', - errorDistribution: 'Error distribution by status code (SLA scope, excluding business limits).', + totalRequests: 'Общее количество запросов (успешных и неуспешных) в выбранном временном окне.', + throughputTrend: 'Requests/QPS + Tokens/TPS в выбранном окне.', + switchRateTrend: 'Динамика переключений аккаунтов / всего запросов за последние 5 часов (средние переключения).', + latencyHistogram: 'Распределение длительности запросов (ms) для успешных запросов.', + errorTrend: 'Количество ошибок во времени (область SLA исключает бизнес-лимиты; upstream исключает 429/529).', + errorDistribution: 'Распределение ошибок по status code (область SLA, без бизнес-лимитов).', goroutines: - 'Number of Go runtime goroutines (lightweight threads). There is no absolute "safe" number—use your historical baseline. Heuristic: <2k is common; 2k–8k watch; >8k plus rising queue/latency often suggests blocking/leaks.', - cpu: 'CPU usage percentage, showing system processor load.', - memory: 'Memory usage, including used and total available memory.', - db: 'Database connection pool status, including active, idle, and waiting connections.', - redis: 'Redis connection pool status, showing active and idle connections.', - jobs: 'Background job execution status, including last run time, success time, and error information.', - qps: 'Queries Per Second (QPS) and Tokens Per Second (TPS), real-time system throughput.', - tokens: 'Total number of tokens processed in the current time window.', - sla: 'Service Level Agreement success rate, excluding business limits (e.g., insufficient balance, quota exceeded).', - errors: 'Error statistics, including total errors, error rate, and upstream error rate.', - upstreamErrors: 'Upstream error statistics, excluding rate limit errors (429/529).', - latency: 'Request duration statistics, including p50, p90, p95, p99 percentiles.', - ttft: 'Time To First Token, measuring the speed of first token return in streaming responses.', - health: 'System health score (0-100), considering SLA, error rate, and resource usage.' + 'Количество goroutines Go runtime (легковесные потоки). Абсолютно "безопасного" числа нет — используйте исторический baseline. Эвристика: <2k обычно нормально; 2k–8k требует наблюдения; >8k вместе с растущей очередью/задержкой часто указывает на блокировки/утечки.', + cpu: 'Процент использования CPU, показывает нагрузку на процессор системы.', + memory: 'Использование памяти, включая занятую и общий доступный объём.', + db: 'Статус пула подключений к базе данных, включая active, idle и waiting соединения.', + redis: 'Статус пула подключений Redis, показывает active и idle соединения.', + jobs: 'Статус выполнения фоновых задач, включая время последнего запуска, успешного выполнения и информацию об ошибках.', + qps: 'Queries Per Second (QPS) и Tokens Per Second (TPS), пропускная способность системы в realtime.', + tokens: 'Общее количество токенов, обработанных в текущем временном окне.', + sla: 'Доля успешности SLA, без учёта бизнес-лимитов (например, недостаточный баланс, превышена квота).', + errors: 'Статистика ошибок, включая общее число ошибок, долю ошибок и долю ошибок upstream.', + upstreamErrors: 'Статистика ошибок upstream, без ошибок rate limit (429/529).', + latency: 'Статистика длительности запросов, включая процентили p50, p90, p95, p99.', + ttft: 'Time To First Token, измеряет скорость возврата первого токена в streaming-ответах.', + health: 'Оценка состояния системы (0-100), учитывает SLA, долю ошибок и использование ресурсов.' }, charts: { - emptyRequest: 'No requests in this window.', - emptyError: 'No errors in this window.', + emptyRequest: 'В этом окне запросов нет.', + emptyError: 'В этом окне ошибок нет.', resetZoom: 'Сбросить', - resetZoomHint: 'Reset zoom (if enabled)', - downloadChart: 'Download', - downloadChartHint: 'Download chart as image' + resetZoomHint: 'Сбросить масштаб (если включён)', + downloadChart: 'Скачать', + downloadChartHint: 'Скачать график как изображение' } }, // Settings settings: { - title: 'System Settings', - description: 'Manage registration, email verification, default values, and SMTP settings', + title: 'Настройки системы', + description: 'Управляйте регистрацией, подтверждением email, значениями по умолчанию и настройками SMTP', tabs: { - general: 'General', - agreement: 'Agreement', - features: 'Feature Switches', - security: 'Security', + general: 'Общие', + agreement: 'Соглашение', + features: 'Переключатели функций', + security: 'Безопасность', users: 'Пользователи', - gateway: 'Gateway', + gateway: 'Шлюз', email: 'Email', - backup: 'Backup', - payment: 'Payment', + backup: 'Бэкап', + payment: 'Оплата', }, features: { channelMonitor: { title: 'Монитор каналов', - description: 'Periodically probe configured channels and surface availability / latency to users. Turning it off stops the scheduler and returns an empty list on the user page.', - configureLink: 'Configure monitors in Channel Management > Channel Monitor', - enabled: 'Enable Channel Monitor', - enabledHint: 'Disabling stops background checks; existing history is preserved.', - defaultInterval: 'Default check interval (seconds)', - defaultIntervalHint: 'Pre-fills the interval when creating a new monitor; each monitor can override it. Range 15 – 3600.', + description: 'Периодически проверяет настроенные каналы и показывает пользователям доступность / задержку. Отключение остановит планировщик и вернёт пустой список на пользовательской странице.', + configureLink: 'Настройте мониторы в Управление каналами > Channel Monitor', + enabled: 'Включить монитор каналов', + enabledHint: 'Отключение остановит фоновые проверки; существующая история сохранится.', + defaultInterval: 'Интервал проверки по умолчанию (секунды)', + defaultIntervalHint: 'Подставляется при создании нового монитора; каждый монитор может переопределить значение. Диапазон 15–3600.', }, availableChannels: { title: 'Доступные каналы', - description: 'Show logged-in users an aggregate view of the channels, models and pricing they can access. Disabled by default.', - configureLink: 'Configure model pricing in Channel Management > Channel Pricing', - enabled: 'Enable Available Channels', - enabledHint: 'When off, the sidebar entry is hidden and the endpoint returns an empty list.', + description: 'Показывает вошедшим пользователям агрегированный вид доступных им каналов, моделей и цен. По умолчанию отключено.', + configureLink: 'Настройте цены моделей в Управление каналами > Channel Pricing', + enabled: 'Включить Доступные каналы', + enabledHint: 'Когда отключено, пункт бокового меню скрыт, а endpoint возвращает пустой список.', }, riskControl: { title: 'Риск-контроль', - description: 'Enable the content moderation menu and gateway audit entry point. Disabled by default.', - configureLink: 'Configure content moderation in Risk Control', - enabled: 'Enable Risk Control', - enabledHint: 'When off, the admin sidebar entry is hidden and gateway moderation is skipped.', + description: 'Включает меню модерации контента и точку входа аудита шлюза. По умолчанию отключено.', + configureLink: 'Настройте модерацию контента в Контроль рисков', + enabled: 'Включить Контроль рисков', + enabledHint: 'Когда отключено, пункт в админском боковом меню скрыт, а модерация шлюза пропускается.', }, affiliate: { - title: 'Affiliate (Invite Rebate)', - description: 'Existing users invite new ones; the inviter earns a percentage rebate on the invitee’s recharges. Disabled by default.', - enabled: 'Enable Affiliate', - enabledHint: 'When off, the affiliate menu is hidden, the aff parameter is ignored at signup, and new recharges generate no rebate. Existing rebate balances can still be transferred.', - rebateRate: 'Global Rebate Rate', - rebateRateHint: 'Default percentage given back to the inviter on recharges (0-100, e.g. 10 = 10%).', - freezeHours: 'Rebate Freeze Period (hours)', - freezeHoursDesc: 'New rebates will be frozen for this period before becoming available for withdrawal. 0 = no freeze.', - durationDays: 'Rebate Duration (days)', - durationDaysDesc: 'Rebate relationship expires after this many days since invitee registration. 0 = permanent.', - perInviteeCap: 'Per-Invitee Rebate Cap', - perInviteeCapDesc: 'Maximum total rebate from a single invitee. 0 = no limit.', + title: 'Партнёрская программа (бонус за приглашение)', + description: 'Существующие пользователи приглашают новых; пригласивший получает процентное вознаграждение с пополнений приглашённого. По умолчанию отключено.', + enabled: 'Включить партнёрскую программу', + enabledHint: 'Когда отключено, меню партнёрской программы скрыто, параметр aff при регистрации игнорируется, а новые пополнения не создают вознаграждение. Уже накопленные вознаграждения всё ещё можно переводить.', + rebateRate: 'Глобальная ставка вознаграждения', + rebateRateHint: 'Процент по умолчанию, возвращаемый пригласившему с пополнений (0-100, например 10 = 10%).', + freezeHours: 'Период заморозки вознаграждения (часы)', + freezeHoursDesc: 'Новые вознаграждения будут заморожены на этот период перед доступом к выводу. 0 = без заморозки.', + durationDays: 'Срок действия вознаграждения (дни)', + durationDaysDesc: 'Связь вознаграждения истекает через это число дней после регистрации приглашённого. 0 = навсегда.', + perInviteeCap: 'Лимит вознаграждения на приглашённого', + perInviteeCapDesc: 'Максимальное суммарное вознаграждение от одного приглашённого. 0 = без лимита.', customUsers: { - title: 'Per-User Overrides', - description: 'Set a custom invite code or exclusive rebate rate for specific users. Lists only users that have an override applied.', - addButton: 'Add Custom User', - searchPlaceholder: 'Search by email or username', - batchButton: 'Batch Set Rate ({count} selected)', - empty: 'No users with custom affiliate settings yet', - customBadge: 'custom', - useGlobal: 'use global', - resetTitle: 'Reset Custom Settings', - resetMessage: 'Reset all custom settings for {email}?\n• The exclusive rebate rate will be cleared (fall back to the global rate)\n• The invite code will be regenerated as a new system code (previously shared links will stop working)', - totalLabel: '{total} total', + title: 'Индивидуальные настройки пользователей', + description: 'Задайте пользовательский код приглашения или эксклюзивную ставку вознаграждения для отдельных пользователей. В списке только пользователи с индивидуальными настройками.', + addButton: 'Добавить пользователя', + searchPlaceholder: 'Поиск по email или имени пользователя', + batchButton: 'Массово задать ставку (выбрано {count})', + empty: 'Пользователей с индивидуальными настройками партнёрской программы пока нет', + customBadge: 'индивидуально', + useGlobal: 'использовать глобальную', + resetTitle: 'Сбросить индивидуальные настройки', + resetMessage: 'Сбросить все индивидуальные настройки для {email}?\n• Эксклюзивная ставка вознаграждения будет очищена (возврат к глобальной ставке)\n• Код приглашения будет заново создан как новый системный код (ранее отправленные ссылки перестанут работать)', + totalLabel: 'Всего: {total}', col: { email: 'Email', username: 'Имя пользователя', - code: 'Invite Code', - rate: 'Custom Rate', + code: 'Код приглашения', + rate: 'Индивидуальная ставка', actions: 'Действия', }, }, modal: { - addTitle: 'Add Custom User', - editTitle: 'Edit Custom Settings', + addTitle: 'Добавить пользователя', + editTitle: 'Изменить индивидуальные настройки', userLabel: 'Пользователь', userPlaceholder: 'Search by email or username', - changeUser: 'Change user', - codeLabel: 'Custom Invite Code (optional)', + changeUser: 'Сменить пользователя', + codeLabel: 'Пользовательский код приглашения (необязательно)', codePlaceholder: 'e.g. VIP2026', - codeHint: '4-32 characters; A-Z, 0-9, underscore, dash. Leave empty to keep current. Input is upper-cased.', - rateLabel: 'Exclusive Rebate Rate (optional)', + codeHint: '4–32 символа; A-Z, 0-9, подчёркивание, дефис. Оставьте пустым, чтобы сохранить текущее значение. Ввод приводится к верхнему регистру.', + rateLabel: 'Эксклюзивная ставка вознаграждения (необязательно)', ratePlaceholder: 'e.g. 30', - rateHint: '0-100. Leave empty (in edit mode) to clear and fall back to the global rate.', - errorBadRate: 'Please enter a number between 0 and 100', - errorEmpty: 'Fill at least one: custom invite code or exclusive rebate rate', + rateHint: '0-100. Оставьте пустым (в режиме редактирования), чтобы очистить и вернуться к глобальной ставке.', + errorBadRate: 'Введите число от 0 до 100', + errorEmpty: 'Заполните хотя бы одно поле: пользовательский код приглашения или эксклюзивную ставку вознаграждения', }, batchModal: { - title: 'Batch Set Rate ({count} users selected)', - hint: 'Apply the same exclusive rebate rate to all selected users.', + title: 'Массово задать ставку (выбрано пользователей: {count})', + hint: 'Применить одинаковую эксклюзивную ставку вознаграждения ко всем выбранным пользователям.', placeholder: 'e.g. 30', - clearHint: 'Submitting empty will clear the exclusive rate for selected users.', + clearHint: 'Отправка пустого значения очистит эксклюзивную ставку для выбранных пользователей.', }, }, }, - emailTabDisabledTitle: 'Email Verification Not Enabled', - emailTabDisabledHint: 'Enable email verification in the Security tab to configure SMTP settings.', + emailTabDisabledTitle: 'Подтверждение email не включено', + emailTabDisabledHint: 'Включите подтверждение email на вкладке Безопасность, чтобы настроить SMTP.', registration: { - title: 'Registration Settings', - description: 'Control user registration and verification', - enableRegistration: 'Enable Registration', - enableRegistrationHint: 'Allow new users to register', - emailVerification: 'Email Verification', - emailVerificationHint: 'Require email verification for new registrations', - emailSuffixWhitelist: 'Email Domain Whitelist', + title: 'Настройки регистрации', + description: 'Управляйте регистрацией и проверкой пользователей', + enableRegistration: 'Включить регистрацию', + enableRegistrationHint: 'Разрешить новым пользователям регистрироваться', + emailVerification: 'Подтверждение email', + emailVerificationHint: 'Требовать подтверждение email для новых регистраций', + emailSuffixWhitelist: 'Whitelist доменов email', emailSuffixWhitelistHint: - "Only email addresses from the specified domains can register (for example, {'@'}qq.com, {'@'}gmail.com, *.edu.cn)", + "Регистрироваться могут только email-адреса из указанных доменов (например, {'@'}qq.com, {'@'}gmail.com, *.edu.cn)", emailSuffixWhitelistPlaceholder: "{'@'}example.com, *.edu.cn", - emailSuffixWhitelistInputHint: 'Leave empty for no restriction. Use *.edu.cn to match edu.cn and its subdomains.', + emailSuffixWhitelistInputHint: 'Оставьте пустым без ограничений. Используйте *.edu.cn, чтобы сопоставлять edu.cn и его поддомены.', promoCode: 'Промокод', - promoCodeHint: 'Allow users to use promo codes during registration', - invitationCode: 'Invitation Code Registration', - invitationCodeHint: 'When enabled, users must enter a valid invitation code to register', - passwordReset: 'Password Reset', - passwordResetHint: 'Allow users to reset their password via email', + promoCodeHint: 'Разрешить пользователям применять промокоды при регистрации', + invitationCode: 'Регистрация по коду приглашения', + invitationCodeHint: 'Когда включено, пользователи должны ввести действительный код приглашения для регистрации', + passwordReset: 'Сброс пароля', + passwordResetHint: 'Разрешить пользователям сбрасывать пароль через email', frontendUrl: 'Frontend URL', frontendUrlPlaceholder: 'https://example.com', - frontendUrlHint: 'Used to generate password reset links in emails. Example: https://example.com', + frontendUrlHint: 'Используется для генерации ссылок сброса пароля в email. Пример: https://example.com', totp: 'Двухфакторная аутентификация (2FA)', - totpHint: 'Allow users to use authenticator apps like Google Authenticator', + totpHint: 'Разрешить пользователям использовать приложения-аутентификаторы, например Google Authenticator', totpKeyNotConfigured: - 'Please configure TOTP_ENCRYPTION_KEY in environment variables first. Generate a key with: openssl rand -hex 32' + 'Сначала настройте TOTP_ENCRYPTION_KEY в переменных окружения. Сгенерируйте ключ командой: openssl rand -hex 32' }, turnstile: { title: 'Cloudflare Turnstile', - description: 'Bot protection for login and registration', - enableTurnstile: 'Enable Turnstile', - enableTurnstileHint: 'Require Cloudflare Turnstile verification', + description: 'Защита от ботов для входа и регистрации', + enableTurnstile: 'Включить Turnstile', + enableTurnstileHint: 'Требовать проверку Cloudflare Turnstile', siteKey: 'Site Key', secretKey: 'Secret Key', - siteKeyHint: 'Get this from your Cloudflare Dashboard', + siteKeyHint: 'Получите это в Cloudflare Dashboard', cloudflareDashboard: 'Cloudflare Dashboard', - secretKeyHint: 'Server-side verification key (keep this secret)', - secretKeyConfiguredHint: 'Secret key configured. Leave empty to keep the current value.' + secretKeyHint: 'Ключ server-side проверки (храните в секрете)', + secretKeyConfiguredHint: 'Secret key настроен. Оставьте пустым, чтобы сохранить текущее значение.' }, apiKeyAcl: { - title: 'API Key IP Access Control', - description: 'Choose which client IP is used by API Key allowlists and denylists', - trustForwardedIp: 'Trust forwarded client IP', + title: 'Контроль доступа API-ключей по IP', + description: 'Выберите, какой IP клиента используется в allowlist/denylist API Key', + trustForwardedIp: 'Доверять forwarded client IP', trustForwardedIpHint: - 'Disabled by default. Enable only when the origin is reachable only through Cloudflare or Nginx reverse proxy. When enabled, API Key IP allowlists and denylists use CF-Connecting-IP, X-Real-IP, or X-Forwarded-For, matching the request IP shown in usage records.' + 'По умолчанию отключено. Включайте только когда origin доступен только через Cloudflare или reverse proxy Nginx. Когда включено, allowlist и denylist API Key используют CF-Connecting-IP, X-Real-IP или X-Forwarded-For, совпадая с IP запроса в записях расхода.' }, linuxdo: { - title: 'LinuxDo Connect Login', - description: 'Configure LinuxDo Connect OAuth for Sub2API end-user login', - enable: 'Enable LinuxDo Login', - enableHint: 'Show LinuxDo login on the login/register pages', + title: 'Вход через LinuxDo Connect', + description: 'Настройте LinuxDo Connect OAuth для входа конечных пользователей Sub2API', + enable: 'Включить вход через LinuxDo', + enableHint: 'Показывать вход через LinuxDo на страницах входа/регистрации', clientId: 'Client ID', clientIdPlaceholder: 'e.g., hprJ5pC3...', - clientIdHint: 'Get this from Connect.Linux.Do', + clientIdHint: 'Получите это в Connect.Linux.Do', clientSecret: 'Client Secret', clientSecretPlaceholder: '********', - clientSecretHint: 'Used by backend to exchange tokens (keep it secret)', + clientSecretHint: 'Используется backend для обмена токенов (храните в секрете)', clientSecretConfiguredPlaceholder: '********', - clientSecretConfiguredHint: 'Secret configured. Leave empty to keep the current value.', + clientSecretConfiguredHint: 'Secret настроен. Оставьте пустым, чтобы сохранить текущее значение.', redirectUrl: 'Redirect URL', redirectUrlPlaceholder: 'https://your-domain.com/api/v1/auth/oauth/linuxdo/callback', redirectUrlHint: - 'Must match the redirect URL configured in Connect.Linux.Do (must be an absolute http(s) URL)', - quickSetCopy: 'Generate & Copy (current site)', - redirectUrlSetAndCopied: 'Redirect URL generated and copied to clipboard' + 'Должен совпадать с redirect URL, настроенным в Connect.Linux.Do (должен быть абсолютным http(s) URL)', + quickSetCopy: 'Сгенерировать и скопировать (текущий сайт)', + redirectUrlSetAndCopied: 'Redirect URL сгенерирован и скопирован в буфер обмена' }, dingtalk: { - title: 'DingTalk Login', - description: 'Configure DingTalk OAuth for Sub2API end-user login', - enable: 'Enable DingTalk Login (Internal Corporate App)', - enableHint: 'Show DingTalk login on the login/register pages', + title: 'Вход через DingTalk', + description: 'Настройте DingTalk OAuth для входа конечных пользователей Sub2API', + enable: 'Включить вход через DingTalk (внутреннее корпоративное приложение)', + enableHint: 'Показывать вход через DingTalk на страницах входа/регистрации', clientId: 'Client ID (AppKey)', clientIdPlaceholder: 'e.g., dingxxxxxxxxxxxxxxxx', - clientIdHint: 'Get this from the DingTalk Open Platform app details', + clientIdHint: 'Получите это в деталях приложения DingTalk Open Platform', clientSecret: 'Client Secret (AppSecret)', clientSecretPlaceholder: '********', - clientSecretHint: 'Used by backend to exchange tokens (keep it secret)', + clientSecretHint: 'Используется backend для обмена токенов (храните в секрете)', clientSecretConfiguredPlaceholder: '********', - clientSecretConfiguredHint: 'Secret configured. Leave empty to keep the current value.', + clientSecretConfiguredHint: 'Secret настроен. Оставьте пустым, чтобы сохранить текущее значение.', redirectUrl: 'Redirect URL', redirectUrlPlaceholder: 'https://your-domain.com/api/v1/auth/oauth/dingtalk/callback', redirectUrlHint: - 'Must match the redirect URL configured in DingTalk Open Platform (must be an absolute http(s) URL)', + 'Должен совпадать с redirect URL, настроенным в DingTalk Open Platform (должен быть абсолютным http(s) URL)', corpPolicy: { - label: 'Corp Restriction Policy', - hint: 'Control which DingTalk accounts (orgs) are allowed to sign in', - none: 'No restriction (all DingTalk accounts allowed)', - internalOnly: 'Internal only (single corp)' + label: 'Политика ограничения организаций', + hint: 'Управляет тем, какие аккаунты DingTalk (организации) могут входить', + none: 'Без ограничений (разрешены все аккаунты DingTalk)', + internalOnly: 'Только внутренняя организация (одна corp)' }, - bypassRegistration: 'Enable DingTalk signup', - bypassRegistrationHint: 'Allow new users to register via DingTalk even when public registration is disabled.', - syncDisplayName: 'Sync DingTalk display name', - syncDisplayNameHint: 'Overwrite username with the DingTalk staff name on each login (also stored in the dingtalk_name attribute).', - syncCorpEmail: 'Sync corporate email', - syncCorpEmailHint: 'Write the DingTalk corporate email to the dingtalk_email attribute on each login (does not change the login email).', - syncCorpEmailPermissionHint: 'Requires the OAPI permission "Personal info incl. email (fieldEmail)" to be granted to the app on the DingTalk open platform, otherwise OAPI will not return the email field.', - syncDept: 'Sync department', - syncDeptHint: 'Write the full DingTalk department path to the dingtalk_department attribute on each login (fetched live each time).', - syncDeptPermissionHint: 'Requires the OAPI "Department info read (qyapi_get_department_list)" permission to be granted to the app on the DingTalk open platform, otherwise the department path cannot be resolved.', - syncDisplayNameTarget: 'Attribute key', - syncDisplayNameTargetHint: 'Defaults to dingtalk_name / DingTalk Name. Saving settings auto-creates the user attribute by the key and display name above (existing definition only has its display name synced).', - syncCorpEmailTarget: 'Attribute key', - syncCorpEmailTargetHint: 'Defaults to dingtalk_email / DingTalk Corporate Email. Saving settings auto-creates the user attribute by the key and display name above (existing definition only has its display name synced).', - syncDeptTarget: 'Attribute key', - syncDeptTargetHint: 'Defaults to dingtalk_department / DingTalk Department. Saving settings auto-creates the user attribute by the key and display name above (existing definition only has its display name synced).', - syncAttrDisplayName: 'Display name' + bypassRegistration: 'Включить регистрацию через DingTalk', + bypassRegistrationHint: 'Разрешить новым пользователям регистрироваться через DingTalk, даже когда публичная регистрация отключена.', + syncDisplayName: 'Синхронизировать отображаемое имя DingTalk', + syncDisplayNameHint: 'Перезаписывать имя пользователя именем сотрудника DingTalk при каждом входе (также сохраняется в атрибут dingtalk_name).', + syncCorpEmail: 'Синхронизировать корпоративный email', + syncCorpEmailHint: 'Записывать корпоративный email DingTalk в атрибут dingtalk_email при каждом входе (не меняет email для входа).', + syncCorpEmailPermissionHint: 'Требуется выдать приложению в DingTalk open platform разрешение OAPI "Personal info incl. email (fieldEmail)", иначе OAPI не вернёт поле email.', + syncDept: 'Синхронизировать отдел', + syncDeptHint: 'Записывать полный путь отдела DingTalk в атрибут dingtalk_department при каждом входе (каждый раз запрашивается заново).', + syncDeptPermissionHint: 'Требуется выдать приложению в DingTalk open platform разрешение OAPI "Department info read (qyapi_get_department_list)", иначе путь отдела нельзя будет определить.', + syncDisplayNameTarget: 'Ключ атрибута', + syncDisplayNameTargetHint: 'По умолчанию dingtalk_name / DingTalk Name. При сохранении настроек автоматически создаётся атрибут пользователя по ключу и отображаемому имени выше (у существующего определения синхронизируется только отображаемое имя).', + syncCorpEmailTarget: 'Ключ атрибута', + syncCorpEmailTargetHint: 'По умолчанию dingtalk_email / DingTalk Corporate Email. При сохранении настроек автоматически создаётся атрибут пользователя по ключу и отображаемому имени выше (у существующего определения синхронизируется только отображаемое имя).', + syncDeptTarget: 'Ключ атрибута', + syncDeptTargetHint: 'По умолчанию dingtalk_department / DingTalk Department. При сохранении настроек автоматически создаётся атрибут пользователя по ключу и отображаемому имени выше (у существующего определения синхронизируется только отображаемое имя).', + syncAttrDisplayName: 'Отображаемое имя' }, oidc: { - title: 'OIDC Login', - description: 'Configure a standard OIDC provider (for example Keycloak)', - enable: 'Enable OIDC Login', - enableHint: 'Show OIDC login on the login/register pages', - providerName: 'Provider Name', - providerNamePlaceholder: 'for example Keycloak', + title: 'Вход через OIDC', + description: 'Настройте стандартного провайдера OIDC (например Keycloak)', + enable: 'Включить вход через OIDC', + enableHint: 'Показывать вход через OIDC на страницах входа/регистрации', + providerName: 'Имя провайдера', + providerNamePlaceholder: 'например Keycloak', clientId: 'Client ID', clientIdPlaceholder: 'OIDC client id', clientSecret: 'Client Secret', clientSecretPlaceholder: '********', - clientSecretHint: 'Used by backend to exchange tokens (keep it secret)', + clientSecretHint: 'Используется backend для обмена токенов (храните в секрете)', clientSecretConfiguredPlaceholder: '********', - clientSecretConfiguredHint: 'Secret configured. Leave empty to keep the current value.', + clientSecretConfiguredHint: 'Secret настроен. Оставьте пустым, чтобы сохранить текущее значение.', issuerUrl: 'Issuer URL', issuerUrlPlaceholder: 'https://id.example.com/realms/main', discoveryUrl: 'Discovery URL', - discoveryUrlPlaceholder: 'Optional, leave empty to auto-derive from issuer', + discoveryUrlPlaceholder: 'Необязательно, оставьте пустым для автоопределения из issuer', authorizeUrl: 'Authorize URL', - authorizeUrlPlaceholder: 'Optional, can be discovered automatically', + authorizeUrlPlaceholder: 'Необязательно, может быть найдено автоматически', tokenUrl: 'Token URL', - tokenUrlPlaceholder: 'Optional, can be discovered automatically', + tokenUrlPlaceholder: 'Необязательно, может быть найдено автоматически', userinfoUrl: 'UserInfo URL', - userinfoUrlPlaceholder: 'Optional, can be discovered automatically', + userinfoUrlPlaceholder: 'Необязательно, может быть найдено автоматически', jwksUrl: 'JWKS URL', - jwksUrlPlaceholder: 'Optional, required when strict ID token validation is enabled', + jwksUrlPlaceholder: 'Необязательно, требуется при включённой строгой проверке ID token', scopes: 'Scopes', scopesPlaceholder: 'openid email profile', - scopesHint: 'Must include openid', + scopesHint: 'Должно включать openid', redirectUrl: 'Backend Redirect URL', redirectUrlPlaceholder: 'https://your-domain.com/api/v1/auth/oauth/oidc/callback', - redirectUrlHint: 'Must match the callback URL configured in the OIDC provider', - quickSetCopy: 'Generate & Copy (current site)', - redirectUrlSetAndCopied: 'Redirect URL generated and copied to clipboard', + redirectUrlHint: 'Должен совпадать с callback URL, настроенным у OIDC-провайдера', + quickSetCopy: 'Сгенерировать и скопировать (текущий сайт)', + redirectUrlSetAndCopied: 'Redirect URL сгенерирован и скопирован в буфер обмена', frontendRedirectUrl: 'Frontend Callback Path', frontendRedirectUrlPlaceholder: '/auth/oidc/callback', - frontendRedirectUrlHint: 'Frontend route used after backend callback', + frontendRedirectUrlHint: 'Frontend-маршрут, используемый после backend callback', tokenAuthMethod: 'Token Auth Method', clockSkewSeconds: 'Clock Skew (seconds)', allowedSigningAlgs: 'Allowed Signing Algs', allowedSigningAlgsPlaceholder: 'RS256,ES256,PS256', - usePkce: 'Use PKCE', - validateIdToken: 'Validate ID Token', - requireEmailVerified: 'Require Email Verified', + usePkce: 'Использовать PKCE', + validateIdToken: 'Проверять ID Token', + requireEmailVerified: 'Требовать подтверждённый email', userinfoEmailPath: 'UserInfo Email Path', userinfoEmailPathPlaceholder: 'for example data.email', userinfoIdPath: 'UserInfo ID Path', @@ -5510,265 +5510,265 @@ export default { userinfoUsernamePathPlaceholder: 'for example data.username' }, defaults: { - title: 'Default User Settings', - description: 'Default values for new users', - defaultBalance: 'Default Balance', - defaultBalanceHint: 'Initial balance for new users', - affiliateRebateRate: 'Affiliate Rebate Rate', + title: 'Настройки пользователя по умолчанию', + description: 'Значения по умолчанию для новых пользователей', + defaultBalance: 'Баланс по умолчанию', + defaultBalanceHint: 'Начальный баланс для новых пользователей', + affiliateRebateRate: 'Ставка партнёрского вознаграждения', affiliateRebateRateHint: - 'Rebate percentage credited to inviter after recharge (0-100%, e.g. 10 means 10%)', - defaultConcurrency: 'Default Concurrency', - defaultConcurrencyHint: 'Maximum concurrent requests for new users', - defaultUserRpmLimit: 'Default User RPM Limit', - defaultUserRpmLimitHint: 'Default max requests per minute for new users; 0 = unlimited. Only applied at new user creation.', - defaultSubscriptions: 'Default Subscriptions', - defaultSubscriptionsHint: 'Auto-assign these subscriptions when a new user is created or registered', - addDefaultSubscription: 'Add Default Subscription', - defaultSubscriptionsEmpty: 'No default subscriptions configured.', + 'Процент вознаграждения, начисляемый пригласившему после пополнения (0-100%, например 10 означает 10%)', + defaultConcurrency: 'Параллелизм по умолчанию', + defaultConcurrencyHint: 'Максимум одновременных запросов для новых пользователей', + defaultUserRpmLimit: 'Лимит RPM пользователя по умолчанию', + defaultUserRpmLimitHint: 'Максимум запросов в минуту по умолчанию для новых пользователей; 0 = безлимитно. Применяется только при создании нового пользователя.', + defaultSubscriptions: 'Подписки по умолчанию', + defaultSubscriptionsHint: 'Автоматически назначать эти подписки при создании или регистрации нового пользователя', + addDefaultSubscription: 'Добавить подписку по умолчанию', + defaultSubscriptionsEmpty: 'Подписки по умолчанию не настроены.', defaultSubscriptionsDuplicate: - 'Duplicate subscription group: {groupId}. Each group can only appear once.', - subscriptionGroup: 'Subscription Group', - subscriptionValidityDays: 'Validity (days)', - defaultPlatformQuotas: 'Default Platform Quotas (on signup)', - defaultPlatformQuotasHint: 'Automatically assigned to new users on signup; existing users are not affected. Leave blank = unlimited.', - platformQuotaNotice: 'Monthly quota uses a 30-day rolling window, not a calendar month.', + 'Дублирующаяся группа подписки: {groupId}. Каждая группа может быть указана только один раз.', + subscriptionGroup: 'Группа подписки', + subscriptionValidityDays: 'Срок действия (дни)', + defaultPlatformQuotas: 'Квоты платформ по умолчанию (при регистрации)', + defaultPlatformQuotasHint: 'Автоматически назначаются новым пользователям при регистрации; существующих пользователей не затрагивает. Пусто = безлимитно.', + platformQuotaNotice: 'Месячная квота использует 30-дневное скользящее окно, а не календарный месяц.', }, platformQuota: { platform: 'Платформа', - daily: 'Daily (USD)', - weekly: 'Weekly (USD)', - monthly: 'Monthly (USD, 30d rolling)', + daily: 'Дневная (USD)', + weekly: 'Недельная (USD)', + monthly: 'Месячная (USD, 30d rolling)', placeholder: 'Безлимитно', }, claudeCode: { - title: 'Claude Code Settings', - description: 'Control Claude Code client access requirements', - minVersion: 'Minimum Version', + title: 'Настройки Claude Code', + description: 'Управляйте требованиями доступа клиента Claude Code', + minVersion: 'Минимальная версия', minVersionPlaceholder: 'e.g. 2.1.63', minVersionHint: - 'Reject Claude Code clients below this version (semver format). Leave empty to disable version check.', - maxVersion: 'Maximum Version', + 'Отклонять клиентов Claude Code ниже этой версии (формат semver). Оставьте пустым, чтобы отключить проверку версии.', + maxVersion: 'Максимальная версия', maxVersionPlaceholder: 'e.g. 2.5.0', maxVersionHint: - 'Reject Claude Code clients above this version (semver format). Leave empty to allow any version.' + 'Отклонять клиентов Claude Code выше этой версии (формат semver). Оставьте пустым, чтобы разрешить любую версию.' }, scheduling: { - title: 'Gateway Scheduling Settings', - description: 'Control API Key scheduling behavior', - allowUngroupedKey: 'Allow Ungrouped Key Scheduling', - allowUngroupedKeyHint: 'When disabled, API Keys not assigned to any group cannot make requests (403 Forbidden). Keep disabled to ensure all Keys belong to a specific group.' + title: 'Настройки маршрутизации шлюза', + description: 'Управляйте поведением маршрутизации API Key', + allowUngroupedKey: 'Разрешить маршрутизацию ключей без группы', + allowUngroupedKeyHint: 'Когда отключено, API Keys без назначенной группы не могут выполнять запросы (403 Forbidden). Оставьте отключённым, чтобы все Keys принадлежали конкретной группе.' }, gatewayForwarding: { - title: 'Request Forwarding', - description: 'Control how requests are forwarded to upstream OAuth accounts', - fingerprintUnification: 'Fingerprint Unification', - fingerprintUnificationHint: 'Unify X-Stainless-* headers across users sharing the same OAuth account. Disabling passes through each client\'s original headers.', - metadataPassthrough: 'Metadata Passthrough', - metadataPassthroughHint: 'Pass through client\'s original metadata.user_id without rewriting. May improve upstream cache hit rates.', + title: 'Пересылка запросов', + description: 'Управляйте тем, как запросы пересылаются в upstream OAuth-аккаунты', + fingerprintUnification: 'Унификация fingerprint', + fingerprintUnificationHint: 'Унифицировать заголовки X-Stainless-* для пользователей, использующих один OAuth-аккаунт. При отключении исходные заголовки каждого клиента передаются как есть.', + metadataPassthrough: 'Passthrough metadata', + metadataPassthroughHint: 'Передавать исходный metadata.user_id клиента без перезаписи. Может улучшить долю попаданий в upstream cache.', cchSigning: 'CCH Signing', - cchSigningHint: 'Sign the billing header in forwarded requests with CCH hash. When disabled, the placeholder is preserved.', - anthropicCacheTTL1hInjection: 'Anthropic Cache TTL Injection', - anthropicCacheTTL1hInjectionHint: 'When enabled, existing ephemeral cache_control blocks in Anthropic OAuth/Setup Token request bodies are forced to 1h; response usage is billed back as 5m by default, with account-level TTL billing override taking priority.', - rewriteMessageCacheControl: 'Rewrite Message Cache Breakpoints', - rewriteMessageCacheControlHint: 'Default off: preserve client cache_control on message content blocks. When enabled, client breakpoints are stripped and proxy breakpoints are injected for clients that do not manage caching themselves.', + cchSigningHint: 'Подписывать billing header в пересылаемых запросах CCH hash. Когда отключено, placeholder сохраняется.', + anthropicCacheTTL1hInjection: 'Инъекция Anthropic cache TTL', + anthropicCacheTTL1hInjectionHint: 'Когда включено, существующие ephemeral cache_control блоки в телах запросов Anthropic OAuth/Setup Token принудительно переводятся на 1h; usage ответа по умолчанию списывается как 5m, а переопределение TTL billing на уровне аккаунта имеет приоритет.', + rewriteMessageCacheControl: 'Переписывать cache breakpoints сообщений', + rewriteMessageCacheControlHint: 'По умолчанию выключено: сохраняет клиентский cache_control на блоках содержимого сообщений. Когда включено, клиентские breakpoints удаляются, а proxy breakpoints внедряются для клиентов, которые сами не управляют кэшированием.', antigravityUserAgentVersion: 'Antigravity UA Version', antigravityUserAgentVersionPlaceholder: '1.23.2', - antigravityUserAgentVersionHint: 'Leave empty to use ANTIGRAVITY_USER_AGENT_VERSION or the built-in default 1.23.2; when set, the admin setting takes precedence.', + antigravityUserAgentVersionHint: 'Оставьте пустым, чтобы использовать ANTIGRAVITY_USER_AGENT_VERSION или встроенное значение по умолчанию 1.23.2; если задано, настройка администратора имеет приоритет.', openaiCodexUserAgent: 'OpenAI Codex UA', openaiCodexUserAgentPlaceholder: 'codex-tui/0.125.0 (Ubuntu 22.4.0; x86_64) xterm-256color (codex-tui; 0.125.0)', - openaiCodexUserAgentHint: 'Used to bypass Cloudflare browser-UA challenges on the OpenAI upstream. Only applies when the client User-Agent is detected as a browser (Mozilla/...). Leave empty to use the built-in default.', + openaiCodexUserAgentHint: 'Используется для обхода Cloudflare browser-UA challenges на OpenAI upstream. Применяется только когда клиентский User-Agent определяется как браузер (Mozilla/...). Оставьте пустым, чтобы использовать встроенное значение по умолчанию.', }, webSearchEmulation: { - title: 'Web Search Emulation', - description: 'Inject web search capability for Anthropic API Key accounts that don\'t natively support it', - enabled: 'Enable Web Search Emulation', - enabledHint: 'Global switch. When disabled, web search emulation is inactive for all channels and accounts.', - providers: 'Search Providers', - addProvider: 'Add Provider', - providerType: 'Provider Type', + title: 'Эмуляция Web Search', + description: 'Внедряет возможность Web Search для аккаунтов Anthropic API Key, которые не поддерживают её нативно', + enabled: 'Включить эмуляцию Web Search', + enabledHint: 'Глобальный переключатель. Когда отключено, эмуляция Web Search неактивна для всех каналов и аккаунтов.', + providers: 'Search-провайдеры', + addProvider: 'Добавить провайдера', + providerType: 'Тип провайдера', apiKey: 'API-ключ', - apiKeyPlaceholder: 'Enter API Key', - apiKeyConfigured: 'Configured', - showApiKey: 'Show', - hideApiKey: 'Hide', + apiKeyPlaceholder: 'Введите API Key', + apiKeyConfigured: 'Настроено', + showApiKey: 'Показать', + hideApiKey: 'Скрыть', copyApiKey: 'Копировать', copied: 'Скопировано', quotaLimit: 'Лимит квоты', - quotaLimitHint: 'Leave empty for unlimited; must be > 0 if set', - quotaLimitMustBePositive: 'Quota limit must be greater than 0', - subscribedAt: 'Subscribed At', - subscribedAtHint: 'Quota resets monthly from this date; leave empty to disable auto-reset', + quotaLimitHint: 'Оставьте пустым для безлимита; если задано, должно быть > 0', + quotaLimitMustBePositive: 'Лимит квоты должен быть больше 0', + subscribedAt: 'Дата подписки', + subscribedAtHint: 'Квота сбрасывается ежемесячно от этой даты; оставьте пустым, чтобы отключить автосброс', quotaUsage: 'Расход', resetUsage: 'Сбросить', - resetUsageConfirm: 'Reset usage counter for this provider?', - resetUsageSuccess: 'Usage counter reset', + resetUsageConfirm: 'Сбросить счётчик расхода для этого провайдера?', + resetUsageSuccess: 'Счётчик расхода сброшен', proxy: 'Proxy', removeProvider: 'Удалить', - noProviders: 'No search providers configured', - test: 'Test', - testDefaultQuery: 'Major world events this year', - testing: 'Searching...', - testResultTitle: 'Search Results', + noProviders: 'Search-провайдеры не настроены', + test: 'Проверить', + testDefaultQuery: 'Главные мировые события этого года', + testing: 'Поиск...', + testResultTitle: 'Результаты поиска', testResultProvider: 'Провайдер', - testNoResults: 'No results found', + testNoResults: 'Результаты не найдены', }, site: { - title: 'Site Settings', - description: 'Customize site branding', + title: 'Настройки сайта', + description: 'Настройте брендинг сайта', backendMode: 'Backend Mode', backendModeDescription: - 'Disables user registration, public site, and self-service features. Only admin can log in and manage the platform.', - siteName: 'Site Name', + 'Отключает регистрацию пользователей, публичный сайт и функции самообслуживания. Только администратор может входить и управлять платформой.', + siteName: 'Название сайта', siteNamePlaceholder: 'Sub2API', - siteNameHint: 'Displayed in emails and page titles', - siteSubtitle: 'Site Subtitle', - siteSubtitlePlaceholder: 'Subscription to API Conversion Platform', - siteSubtitleHint: 'Displayed on login and register pages', + siteNameHint: 'Отображается в email и заголовках страниц', + siteSubtitle: 'Подзаголовок сайта', + siteSubtitlePlaceholder: 'Платформа преобразования подписок в API', + siteSubtitleHint: 'Отображается на страницах входа и регистрации', apiBaseUrl: 'API Base URL', apiBaseUrlPlaceholder: 'https://api.example.com', apiBaseUrlHint: - 'Used for "Использовать ключ" and "Import to CC Switch" features. Leave empty to use current site URL.', - tablePreferencesTitle: 'Global Table Preferences', - tablePreferencesDescription: 'Configure default pagination behavior for shared table components', - tableDefaultPageSize: 'Default Rows Per Page', - tableDefaultPageSizeHint: 'Must be an integer between 5 and 1000', - tablePageSizeOptions: 'Rows Per Page Options', + 'Используется для функций "Использовать ключ" и "Import to CC Switch". Оставьте пустым, чтобы использовать URL текущего сайта.', + tablePreferencesTitle: 'Глобальные настройки таблиц', + tablePreferencesDescription: 'Настройте поведение пагинации по умолчанию для общих компонентов таблиц', + tableDefaultPageSize: 'Строк на странице по умолчанию', + tableDefaultPageSizeHint: 'Должно быть целым числом от 5 до 1000', + tablePageSizeOptions: 'Варианты строк на странице', tablePageSizeOptionsPlaceholder: '10, 20, 50, 100', - tablePageSizeOptionsHint: 'Use commas to separate integers between 5 and 1000; values are deduplicated and sorted on save', - tableDefaultPageSizeRangeError: 'Default rows per page must be between {min} and {max}', - tablePageSizeOptionsFormatError: 'Invalid options format. Enter comma-separated integers between {min} and {max}', + tablePageSizeOptionsHint: 'Разделяйте запятыми целые числа от 5 до 1000; при сохранении значения дедуплицируются и сортируются', + tableDefaultPageSizeRangeError: 'Количество строк на странице по умолчанию должно быть от {min} до {max}', + tablePageSizeOptionsFormatError: 'Неверный формат вариантов. Введите целые числа от {min} до {max}, разделённые запятыми', customEndpoints: { - title: 'Custom Endpoints', - description: 'Add additional API endpoint URLs for users to quickly copy on the API Keys page', + title: 'Дополнительные API endpoint-ы', + description: 'Добавьте дополнительные URL API endpoint-ов, чтобы пользователи могли быстро копировать их на странице API Keys', itemLabel: 'Endpoint #{n}', name: 'Имя', - namePlaceholder: 'e.g., OpenAI Compatible', + namePlaceholder: 'например OpenAI Compatible', endpointUrl: 'Endpoint URL', endpointUrlPlaceholder: 'https://api2.example.com', descriptionLabel: 'Описание', - descriptionPlaceholder: 'e.g., Supports OpenAI format requests', - add: 'Add Endpoint', + descriptionPlaceholder: 'например поддерживает запросы в формате OpenAI', + add: 'Добавить endpoint', }, - contactInfo: 'Contact Info', + contactInfo: 'Контактная информация', contactInfoPlaceholder: 'e.g., QQ: 123456789', - contactInfoHint: 'Customer support contact info, displayed on redeem page, profile, etc.', - docUrl: 'Documentation URL', + contactInfoHint: 'Контактная информация поддержки, отображается на странице активации кодов, в профиле и т. д.', + docUrl: 'URL документации', docUrlPlaceholder: 'https://docs.example.com', - docUrlHint: 'Link to your documentation site. Leave empty to hide the documentation link.', - siteLogo: 'Site Logo', - uploadImage: 'Upload Image', + docUrlHint: 'Ссылка на сайт документации. Оставьте пустым, чтобы скрыть ссылку на документацию.', + siteLogo: 'Логотип сайта', + uploadImage: 'Загрузить изображение', remove: 'Удалить', - logoHint: 'PNG, JPG, or SVG. Max 300KB. Recommended: 80x80px square image.', - logoSizeError: 'Image size exceeds 300KB limit ({size}KB)', - logoTypeError: 'Please select an image file', - logoReadError: 'Failed to read the image file', - homeContent: 'Home Page Content', - homeContentPlaceholder: 'Enter custom content for the home page. Supports Markdown & HTML. If a URL is entered, it will be displayed as an iframe.', - homeContentHint: 'Customize the home page content. Supports Markdown/HTML. If you enter a URL (starting with http:// or https://), it will be used as an iframe src to embed an external page. When set, the default status information will no longer be displayed.', - homeContentIframeWarning: '⚠️ iframe mode note: Some websites have X-Frame-Options or CSP security policies that prevent embedding in iframes. If the page appears blank or shows an error, please verify the target website allows embedding, or consider using HTML mode to build your own content.', - hideCcsImportButton: 'Hide CCS Import Button', - hideCcsImportButtonHint: 'When enabled, the "Импорт в CCS" button will be hidden on the API Keys page' + logoHint: 'PNG, JPG или SVG. Максимум 300KB. Рекомендуется квадратное изображение 80x80px.', + logoSizeError: 'Размер изображения превышает лимит 300KB ({size}KB)', + logoTypeError: 'Выберите файл изображения', + logoReadError: 'Не удалось прочитать файл изображения', + homeContent: 'Содержимое главной страницы', + homeContentPlaceholder: 'Введите пользовательское содержимое для главной страницы. Поддерживает Markdown и HTML. Если введён URL, он будет отображён как iframe.', + homeContentHint: 'Настройте содержимое главной страницы. Поддерживает Markdown/HTML. Если ввести URL (начинающийся с http:// или https://), он будет использован как iframe src для встраивания внешней страницы. Когда задано, стандартная информация о статусе больше не отображается.', + homeContentIframeWarning: '⚠️ Примечание о режиме iframe: некоторые сайты используют X-Frame-Options или политики безопасности CSP, запрещающие встраивание в iframe. Если страница пустая или показывает ошибку, проверьте, разрешает ли целевой сайт встраивание, либо используйте режим HTML для создания собственного содержимого.', + hideCcsImportButton: 'Скрыть кнопку импорта CCS', + hideCcsImportButtonHint: 'Когда включено, кнопка "Импорт в CCS" будет скрыта на странице API Keys' }, purchase: { - title: 'Recharge / Subscription Page', - description: 'Show a "Пополнение / подписка" entry in the sidebar and open the configured URL in an iframe', - enabled: 'Show Recharge / Subscription Entry', - enabledHint: 'Only shown in standard mode (not simple mode)', - url: 'Recharge / Subscription URL', + title: 'Страница пополнения / подписки', + description: 'Показывает пункт "Пополнение / подписка" в боковом меню и открывает настроенный URL в iframe', + enabled: 'Показывать пункт пополнения / подписки', + enabledHint: 'Показывается только в стандартном режиме (не в простом режиме)', + url: 'URL пополнения / подписки', urlPlaceholder: 'https://example.com/purchase', - urlHint: 'Must be an absolute http(s) URL', + urlHint: 'Должен быть абсолютным http(s) URL', iframeWarning: - '⚠️ iframe note: Some websites block embedding via X-Frame-Options or CSP (frame-ancestors). If the page is blank, provide an "Открыть в новой вкладке" alternative.', - integrationDoc: 'Payment Integration Docs', - integrationDocHint: 'Covers endpoint specs, idempotency semantics, and code samples' + '⚠️ Примечание об iframe: некоторые сайты блокируют встраивание через X-Frame-Options или CSP (frame-ancestors). Если страница пустая, предоставьте альтернативу "Открыть в новой вкладке".', + integrationDoc: 'Документация по интеграции оплаты', + integrationDocHint: 'Описывает спецификации endpoint-ов, семантику идемпотентности и примеры кода' }, soraClient: { - title: 'Sora Client', - description: 'Control whether to show the Sora client entry in the sidebar', - enabled: 'Enable Sora Client', - enabledHint: 'When enabled, the Sora entry will be shown in the sidebar for users to access Sora features' + title: 'Клиент Sora', + description: 'Управляет отображением пункта клиента Sora в боковом меню', + enabled: 'Включить клиент Sora', + enabledHint: 'Когда включено, пункт Sora будет показан в боковом меню, чтобы пользователи могли получить доступ к функциям Sora' }, customMenu: { - title: 'Custom Menu Pages', - description: 'Add custom iframe pages to the sidebar navigation. Each page can be visible to regular users or administrators.', - itemLabel: 'Menu Item #{n}', - name: 'Menu Name', - namePlaceholder: 'e.g. Help Center', - url: 'Page URL', + title: 'Пользовательские страницы меню', + description: 'Добавьте пользовательские iframe-страницы в навигацию бокового меню. Каждая страница может быть видна обычным пользователям или администраторам.', + itemLabel: 'Пункт меню #{n}', + name: 'Название меню', + namePlaceholder: 'например Help Center', + url: 'URL страницы', urlPlaceholder: 'https://example.com/page', iconSvg: 'SVG Icon', iconSvgPlaceholder: '...', - iconPreview: 'Icon Preview', - uploadSvg: 'Upload SVG', + iconPreview: 'Предпросмотр иконки', + uploadSvg: 'Загрузить SVG', removeSvg: 'Удалить', - visibility: 'Visible To', - visibilityUser: 'Regular Users', - visibilityAdmin: 'Administrators', - add: 'Add Menu Item', + visibility: 'Видно для', + visibilityUser: 'Обычные пользователи', + visibilityAdmin: 'Администраторы', + add: 'Добавить пункт меню', remove: 'Удалить', - moveUp: 'Move Up', - moveDown: 'Move Down', + moveUp: 'Переместить вверх', + moveDown: 'Переместить вниз', }, payment: { - title: 'Payment Settings', - description: 'Configure payment system options', - configGuide: 'Configuration Guide', - enabled: 'Enable Payment', - enabledHint: 'Enable or disable the payment system', - enabledPaymentTypes: 'Enabled Providers', - enabledPaymentTypesHint: 'Disabling a provider will also disable its instances.', - findProvider: 'Looking for a suitable EasyPay provider?', - minAmount: 'Minimum Amount', - maxAmount: 'Maximum Amount', + title: 'Настройки оплаты', + description: 'Настройте параметры системы оплаты', + configGuide: 'Руководство по настройке', + enabled: 'Включить оплату', + enabledHint: 'Включить или отключить систему оплаты', + enabledPaymentTypes: 'Включённые провайдеры', + enabledPaymentTypesHint: 'Отключение провайдера также отключит его экземпляры.', + findProvider: 'Ищете подходящего провайдера EasyPay?', + minAmount: 'Минимальная сумма', + maxAmount: 'Максимальная сумма', dailyLimit: 'Дневной лимит', - balanceRechargeMultiplier: 'Balance Recharge Multiplier', - balanceRechargeMultiplierHint: 'How many USD balance the user receives for each 1 CNY paid', - balanceRechargePreview: 'Preview: 1 CNY = {usd} USD', - rechargeFeeRate: 'Recharge Fee Rate', - rechargeFeeRateHint: 'Percentage of service fee charged on top of recharge amount, 0 means no fee', - rechargeFeePreview: 'Preview: Recharge 100, fee {fee}', - orderTimeout: 'Order Timeout', - orderTimeoutHint: 'In minutes, minimum 1', - maxPendingOrders: 'Max Pending Orders', - cancelRateLimit: 'Limit Cancel Rate', - cancelRateLimitHint: 'When enabled, users who exceed the cancel limit within the time window cannot create new orders', - cancelRateLimitEvery: 'Every', - cancelRateLimitAllowMax: 'allow max', - cancelRateLimitTimes: 'cancels', - cancelRateLimitWindow: 'Window', - cancelRateLimitUnit: 'Unit', - cancelRateLimitMax: 'Max Cancels', - cancelRateLimitUnitMinute: 'Minutes', - cancelRateLimitUnitHour: 'Hours', - cancelRateLimitUnitDay: 'Days', - cancelRateLimitWindowMode: 'Window Mode', - cancelRateLimitWindowModeRolling: 'Rolling', - cancelRateLimitWindowModeFixed: 'Fixed', - alipayForceQRCode: 'Force Alipay QR Code', - alipayForceQRCodeHint: 'When enabled, mobile Alipay users always see a QR code instead of being redirected to the mobile payment page', - helpText: 'Help Text', - helpImageUrl: 'Help Image URL', - manageProviders: 'Manage Providers', - balancePaymentDisabled: 'Disable Balance Recharge', - noLimit: 'Empty = no limit', - helpImage: 'Help Image', - helpImagePlaceholder: 'Upload or enter image URL', - helpTextPlaceholder: 'Enter help text...', + balanceRechargeMultiplier: 'Множитель пополнения баланса', + balanceRechargeMultiplierHint: 'Сколько USD баланса получает пользователь за каждый оплаченный 1 CNY', + balanceRechargePreview: 'Предпросмотр: 1 CNY = {usd} USD', + rechargeFeeRate: 'Комиссия пополнения', + rechargeFeeRateHint: 'Процент сервисной комиссии сверх суммы пополнения, 0 означает без комиссии', + rechargeFeePreview: 'Предпросмотр: пополнение 100, комиссия {fee}', + orderTimeout: 'Таймаут заказа', + orderTimeoutHint: 'В минутах, минимум 1', + maxPendingOrders: 'Максимум ожидающих заказов', + cancelRateLimit: 'Ограничить частоту отмен', + cancelRateLimitHint: 'Когда включено, пользователи, превысившие лимит отмен в пределах окна времени, не смогут создавать новые заказы', + cancelRateLimitEvery: 'Каждые', + cancelRateLimitAllowMax: 'разрешить максимум', + cancelRateLimitTimes: 'отмен', + cancelRateLimitWindow: 'Окно', + cancelRateLimitUnit: 'Единица', + cancelRateLimitMax: 'Максимум отмен', + cancelRateLimitUnitMinute: 'Минуты', + cancelRateLimitUnitHour: 'Часы', + cancelRateLimitUnitDay: 'Дни', + cancelRateLimitWindowMode: 'Режим окна', + cancelRateLimitWindowModeRolling: 'Скользящее', + cancelRateLimitWindowModeFixed: 'Фиксированное', + alipayForceQRCode: 'Принудительно показывать QR Code Alipay', + alipayForceQRCodeHint: 'Когда включено, мобильные пользователи Alipay всегда видят QR code вместо перенаправления на мобильную страницу оплаты', + helpText: 'Текст помощи', + helpImageUrl: 'URL изображения помощи', + manageProviders: 'Управление провайдерами', + balancePaymentDisabled: 'Отключить пополнение баланса', + noLimit: 'Пусто = без лимита', + helpImage: 'Изображение помощи', + helpImagePlaceholder: 'Загрузите или введите URL изображения', + helpTextPlaceholder: 'Введите текст помощи...', providerEasypay: 'EasyPay', providerAlipay: 'Alipay (Direct)', providerWxpay: 'WeChat Pay (Direct)', providerStripe: 'Stripe', providerAirwallex: 'Airwallex', - typeDisabled: 'type disabled', - enableTypesFirst: 'Enable at least one payment type above first', + typeDisabled: 'тип отключён', + enableTypesFirst: 'Сначала включите хотя бы один тип оплаты выше', easypayRedirect: 'Redirect', - paymentMode: 'Payment Mode', + paymentMode: 'Режим оплаты', modeRedirect: 'Redirect', modeQRCode: 'QR Code', modePopup: 'Popup', - validationNameRequired: 'Provider name is required', - validationTypesRequired: 'Please select at least one supported payment type', - validationFieldRequired: '{field} is required', + validationNameRequired: 'Имя провайдера обязательно', + validationTypesRequired: 'Выберите хотя бы один поддерживаемый тип оплаты', + validationFieldRequired: '{field} обязательно', field_apiBase: 'API Base URL', field_notifyUrl: 'Notify URL', field_returnUrl: 'Return URL', @@ -5779,120 +5779,120 @@ export default { field_mchId: 'Merchant ID', field_apiV3Key: 'API v3 Key', field_publicKeyId: 'Public Key ID', - field_certSerial: 'Certificate Serial', + field_certSerial: 'Серийный номер сертификата', field_h5AppName: 'H5 App Name', field_h5AppUrl: 'H5 App URL', - wxpayConfigHint: 'WeChat Pay usually only needs App ID. Fill MP App ID, H5 App Name, and H5 App URL only when your Official Account or H5 flow specifically requires them.', - wxpayAdvancedOptions: 'WeChat Pay Advanced Options', + wxpayConfigHint: 'WeChat Pay обычно требует только App ID. Заполняйте MP App ID, H5 App Name и H5 App URL только если ваш Official Account или H5-сценарий явно требует их.', + wxpayAdvancedOptions: 'Расширенные параметры WeChat Pay', field_secretKey: 'Secret Key', field_clientId: 'Client ID', field_apiKey: 'API-ключ', field_publishableKey: 'Publishable Key', field_webhookSecret: 'Webhook Secret', - field_countryCode: 'Country/region code', - field_currency: 'Payment currency', + field_countryCode: 'Код страны/региона', + field_currency: 'Валюта оплаты', field_accountId: 'Airwallex Account ID', - field_airwallexApiBaseHint: 'Must match the API key environment: use https://api-demo.airwallex.com/api/v1 for sandbox/demo keys, and https://api.airwallex.com/api/v1 for production keys. Mixed environments return credentials_invalid / Access Denied.', - field_paymentCurrencyHint: 'Default is CNY. Stripe and Airwallex can choose HKD, USD, or another listed currency supported by the account; WeChat Pay, Alipay, and EasyPay remain CNY.', - field_accountIdHint: 'Leave this empty unless you use multiple accounts, an organization-level key, or connected-account payments. A single-account scoped API key uses the selected account by default.', + field_airwallexApiBaseHint: 'Должен соответствовать окружению API key: используйте https://api-demo.airwallex.com/api/v1 для sandbox/demo keys и https://api.airwallex.com/api/v1 для production keys. Смешанные окружения возвращают credentials_invalid / Access Denied.', + field_paymentCurrencyHint: 'По умолчанию CNY. Stripe и Airwallex могут выбирать HKD, USD или другую валюту из списка, поддерживаемую аккаунтом; WeChat Pay, Alipay и EasyPay остаются CNY.', + field_accountIdHint: 'Оставьте пустым, если не используете несколько аккаунтов, ключ уровня организации или платежи connected-account. API key с областью одного аккаунта по умолчанию использует выбранный аккаунт.', field_cid: 'Channel ID', field_cidAlipay: 'Alipay Channel ID', field_cidWxpay: 'WeChat Channel ID', - stripeWebhookHint: 'Configure the following URL as a Webhook endpoint in Stripe Dashboard:', - stripeWebhookApiVersionHint: 'Set this Webhook endpoint API version to match the integrated Stripe SDK. Recommended: {version}. A mismatch can cause webhook parsing errors.', - airwallexWebhookHint: 'Configure the following URL as a Webhook endpoint in Airwallex. Select at least Payment Intent -> Succeeded (payment_intent.succeeded), preferably also Payment Intent -> Cancelled (payment_intent.cancelled). Use the account default or latest stable API version.', - airwallexGuideSummary: 'When creating an Airwallex scoped API key, select Read and Write for Payment Acceptance under account-level permissions.', - airwallexGuideNote: 'Do not grant unrelated permissions such as Spend, Payouts, Transfers, Funds Splits, or POS Terminals unless you explicitly need them. For webhooks, select at least payment_intent.succeeded, preferably also payment_intent.cancelled, and use the account default or latest stable API version.', - limitsTitle: 'Limits', - limitSingleMin: 'Min per order', - limitSingleMax: 'Max per order', - limitDaily: 'Daily limit', - limitsHint: 'All empty = use global config; partially filled = empty means no limit', - limitsUseGlobal: 'Use global', - limitsNoLimit: 'No limit', - productNamePrefix: 'Product Name Prefix', - productNameSuffix: 'Product Name Suffix', - preview: 'Preview', - loadBalanceStrategy: 'Load Balance Strategy', + stripeWebhookHint: 'Настройте следующий URL как Webhook endpoint в Stripe Dashboard:', + stripeWebhookApiVersionHint: 'Установите версию API этого Webhook endpoint так, чтобы она соответствовала интегрированному Stripe SDK. Рекомендуется: {version}. Несовпадение может вызвать ошибки разбора webhook.', + airwallexWebhookHint: 'Настройте следующий URL как Webhook endpoint в Airwallex. Выберите как минимум Payment Intent -> Succeeded (payment_intent.succeeded), желательно также Payment Intent -> Cancelled (payment_intent.cancelled). Используйте версию API аккаунта по умолчанию или последнюю стабильную.', + airwallexGuideSummary: 'При создании API-ключа Airwallex с ограниченной областью выберите Read и Write для Payment Acceptance в разрешениях уровня аккаунта.', + airwallexGuideNote: 'Не выдавайте несвязанные разрешения, такие как Spend, Payouts, Transfers, Funds Splits или POS Terminals, если они явно не нужны. Для webhooks выберите как минимум payment_intent.succeeded, желательно также payment_intent.cancelled, и используйте версию API аккаунта по умолчанию или последнюю стабильную.', + limitsTitle: 'Лимиты', + limitSingleMin: 'Минимум на заказ', + limitSingleMax: 'Максимум на заказ', + limitDaily: 'Дневной лимит', + limitsHint: 'Все пусто = использовать глобальную конфигурацию; частично заполнено = пустое поле означает без лимита', + limitsUseGlobal: 'Использовать глобальные', + limitsNoLimit: 'Без лимита', + productNamePrefix: 'Префикс названия продукта', + productNameSuffix: 'Суффикс названия продукта', + preview: 'Предпросмотр', + loadBalanceStrategy: 'Стратегия балансировки нагрузки', strategyRoundRobin: 'Round Robin', - strategyLeastAmount: 'Least Daily Amount', - providerManagement: 'Provider Management', - providerManagementDesc: 'Manage payment provider instances', - createProvider: 'Add Provider', - editProvider: 'Edit Provider', - deleteProvider: 'Delete Provider', - deleteProviderConfirm: 'Are you sure you want to delete this provider?', - providerName: 'Provider Name', - providerKey: 'Provider Type', - selectProviderKey: 'Select Provider Type', - providerConfig: 'Credentials', - paymentGuideTrigger: 'View payment guide', - guideOpenLabel: 'Enable: ', - guideCallLabel: 'Call: ', + strategyLeastAmount: 'Наименьшая дневная сумма', + providerManagement: 'Управление провайдерами', + providerManagementDesc: 'Управляйте экземплярами платёжных провайдеров', + createProvider: 'Добавить провайдера', + editProvider: 'Редактировать провайдера', + deleteProvider: 'Удалить провайдера', + deleteProviderConfirm: 'Удалить этого провайдера?', + providerName: 'Имя провайдера', + providerKey: 'Тип провайдера', + selectProviderKey: 'Выберите тип провайдера', + providerConfig: 'Учётные данные', + paymentGuideTrigger: 'Посмотреть руководство по оплате', + guideOpenLabel: 'Включить: ', + guideCallLabel: 'Вызов: ', guideFallbackLabel: 'Fallback: ', - alipayGuideSummary: 'Desktop prefers QR precreate and falls back to cashier; mobile prefers WAP checkout.', - alipayGuideFaceToFaceTitle: 'Face-to-face / QR Payment', - alipayGuideFaceToFaceOpen: 'Enable face-to-face or QR payment capability.', - alipayGuideFaceToFaceCall: 'Desktop orders call alipay.trade.precreate first and render the QR code directly.', - alipayGuideFaceToFaceFallback: 'If unavailable or failed, the flow falls back to website checkout automatically.', - alipayGuidePagePayTitle: 'Website Payment', - alipayGuidePagePayOpen: 'Enable website payment.', - alipayGuidePagePayCall: 'When face-to-face is unavailable on desktop, the flow calls alipay.trade.page.pay and still renders the returned link as a QR code.', - alipayGuidePagePayFallback: 'The cashier link stays available so users can reopen the checkout page manually.', - alipayGuideWapTitle: 'WAP Payment', - alipayGuideWapOpen: 'Enable mobile website payment.', - alipayGuideWapCall: 'Mobile orders call alipay.trade.wap.pay first and jump to Alipay checkout.', - alipayGuideWapFallback: 'If mobile payment is unavailable or fails, the frontend switches to QR payment and shows a notice.', - wxpayGuideSummary: 'Desktop prefers Native QR; mobile routes to JSAPI or H5 based on browser context.', - wxpayGuideNote: 'The current form defaults to one shared App ID, which fits the common single-subject web, mobile, and Official Account setup.', - wxpayGuideNativeTitle: 'Native / QR Payment', - wxpayGuideNativeOpen: 'Enable Native or QR payment capability.', - wxpayGuideNativeCall: 'Desktop orders use Native by default and the frontend renders the QR payload.', - wxpayGuideNativeFallback: 'Mobile flows also fall back here when JSAPI or H5 cannot be used.', + alipayGuideSummary: 'На desktop предпочтительно используется предварительное создание QR с fallback на кассу; на мобильных устройствах предпочтительна WAP-оплата.', + alipayGuideFaceToFaceTitle: 'Оплата Face-to-face / QR', + alipayGuideFaceToFaceOpen: 'Включите возможность оплаты face-to-face или QR.', + alipayGuideFaceToFaceCall: 'Desktop-заказы сначала вызывают alipay.trade.precreate и напрямую отображают QR code.', + alipayGuideFaceToFaceFallback: 'Если недоступно или произошла ошибка, сценарий автоматически переключается на оплату на сайте.', + alipayGuidePagePayTitle: 'Оплата на сайте', + alipayGuidePagePayOpen: 'Включите оплату на сайте.', + alipayGuidePagePayCall: 'Когда face-to-face недоступен на desktop, сценарий вызывает alipay.trade.page.pay и всё равно отображает возвращённую ссылку как QR code.', + alipayGuidePagePayFallback: 'Ссылка на кассу остаётся доступной, чтобы пользователи могли вручную открыть страницу оплаты повторно.', + alipayGuideWapTitle: 'WAP-оплата', + alipayGuideWapOpen: 'Включите оплату на мобильном сайте.', + alipayGuideWapCall: 'Мобильные заказы сначала вызывают alipay.trade.wap.pay и переходят к оплате Alipay.', + alipayGuideWapFallback: 'Если мобильная оплата недоступна или завершается ошибкой, frontend переключается на QR-оплату и показывает уведомление.', + wxpayGuideSummary: 'На desktop предпочтителен Native QR; на мобильных устройствах маршрутизация идёт в JSAPI или H5 в зависимости от контекста браузера.', + wxpayGuideNote: 'Текущая форма по умолчанию использует один общий App ID, что подходит для типичной схемы с одним субъектом для web, mobile и Official Account.', + wxpayGuideNativeTitle: 'Оплата Native / QR', + wxpayGuideNativeOpen: 'Включите возможность оплаты Native или QR.', + wxpayGuideNativeCall: 'Desktop-заказы по умолчанию используют Native, а frontend отображает QR payload.', + wxpayGuideNativeFallback: 'Мобильные сценарии также переключаются сюда, когда JSAPI или H5 нельзя использовать.', wxpayGuideJsapiTitle: 'JSAPI / Official Account', - wxpayGuideJsapiOpen: 'Enable Official Account payment and ensure the browser is inside WeChat with an available OpenID.', - wxpayGuideJsapiCall: 'Inside WeChat, the app calls JSAPI after authorization and launches WeChat Pay directly.', - wxpayGuideJsapiFallback: 'If configuration is missing, the bridge is unavailable, or launch fails, the flow falls back to QR payment.', - wxpayGuideH5Title: 'H5 Payment', - wxpayGuideH5Open: 'Enable H5 payment.', - wxpayGuideH5Call: 'On mobile browsers outside WeChat, the app calls H5 payment when a client IP is available.', - wxpayGuideH5Fallback: 'If H5 is unavailable or order creation fails, the flow falls back to QR payment.', - noProviders: 'No provider instances configured', - supportedTypes: 'Supported Payment Types', - supportedTypesHint: 'Comma-separated, e.g. alipay,wxpay', - refundEnabled: 'Allow Refund', - allowUserRefund: 'Allow User Refund', - enableConflict: '{method} already has an enabled provider instance: {provider}. Disable the existing instance before switching.', + wxpayGuideJsapiOpen: 'Включите оплату через Official Account и убедитесь, что браузер открыт внутри WeChat с доступным OpenID.', + wxpayGuideJsapiCall: 'Внутри WeChat приложение вызывает JSAPI после авторизации и напрямую запускает WeChat Pay.', + wxpayGuideJsapiFallback: 'Если конфигурация отсутствует, bridge недоступен или запуск завершается ошибкой, сценарий переключается на QR-оплату.', + wxpayGuideH5Title: 'H5-оплата', + wxpayGuideH5Open: 'Включите H5-оплату.', + wxpayGuideH5Call: 'В мобильных браузерах вне WeChat приложение вызывает H5-оплату, когда доступен IP клиента.', + wxpayGuideH5Fallback: 'Если H5 недоступен или создание заказа завершается ошибкой, сценарий переключается на QR-оплату.', + noProviders: 'Экземпляры провайдеров не настроены', + supportedTypes: 'Поддерживаемые типы оплаты', + supportedTypesHint: 'Через запятую, например alipay,wxpay', + refundEnabled: 'Разрешить возврат', + allowUserRefund: 'Разрешить возврат пользователем', + enableConflict: 'Для {method} уже есть включённый экземпляр провайдера: {provider}. Отключите существующий экземпляр перед переключением.', }, balanceNotify: { title: 'Уведомление о низком балансе', - description: 'Send email notification when user balance falls below threshold', + description: 'Отправлять email-уведомление, когда баланс пользователя опускается ниже порога', enabled: 'Включить уведомление о низком балансе', - threshold: 'Default Threshold', - thresholdHint: 'Used when user has not set a custom value', + threshold: 'Порог по умолчанию', + thresholdHint: 'Используется, когда пользователь не задал индивидуальное значение', thresholdPlaceholder: 'Введите сумму', - rechargeUrl: 'Recharge Page URL', + rechargeUrl: 'URL страницы пополнения', rechargeUrlPlaceholder: 'https://example.com/payment', - rechargeUrlHint: 'A top-up button will appear in the email when set', + rechargeUrlHint: 'Если задано, в email появится кнопка пополнения', }, quotaNotify: { - title: 'Account Quota Notification', - description: 'Notify admins when account quota usage reaches alert threshold', - enabled: 'Enable Account Quota Notification', + title: 'Уведомление о квоте аккаунта', + description: 'Уведомлять администраторов, когда расход квоты аккаунта достигает порога оповещения', + enabled: 'Включить уведомление о квоте аккаунта', emails: 'Email для уведомлений', - emailsHint: 'Leave empty to disable notifications', + emailsHint: 'Оставьте пустым, чтобы отключить уведомления', addEmail: 'Добавить email', emailPlaceholder: 'Введите email', }, subscriptionExpiryNotify: { - title: 'Subscription Expiry Reminder', - description: 'Control whether users receive subscription expiry reminder emails.', - enabled: 'Enable Subscription Expiry Reminder', - enabledHint: 'When enabled, the system sends reminders 7, 3, and 1 day before expiry.' + title: 'Напоминание об истечении подписки', + description: 'Управляет тем, получают ли пользователи email-напоминания об истечении подписки.', + enabled: 'Включить напоминание об истечении подписки', + enabledHint: 'Когда включено, система отправляет напоминания за 7, 3 и 1 день до истечения.' }, smtp: { - title: 'SMTP Settings', - description: 'Configure email sending for verification codes', + title: 'Настройки SMTP', + description: 'Настройте отправку email для кодов подтверждения', testConnection: 'Проверить подключение', testing: 'Проверка...', host: 'SMTP Host', @@ -5903,121 +5903,121 @@ export default { usernamePlaceholder: "your-email{'@'}gmail.com", password: 'SMTP Password', passwordPlaceholder: '********', - passwordHint: 'Leave empty to keep existing password', + passwordHint: 'Оставьте пустым, чтобы сохранить существующий пароль', passwordConfiguredPlaceholder: '********', - passwordConfiguredHint: 'Password configured. Leave empty to keep the current value.', - fromEmail: 'From Email', + passwordConfiguredHint: 'Пароль настроен. Оставьте пустым, чтобы сохранить текущее значение.', + fromEmail: 'Email отправителя', fromEmailPlaceholder: "noreply{'@'}example.com", - fromName: 'From Name', + fromName: 'Имя отправителя', fromNamePlaceholder: 'Sub2API', - useTls: 'Use TLS', - useTlsHint: 'Enable TLS encryption for SMTP connection' + useTls: 'Использовать TLS', + useTlsHint: 'Включить TLS-шифрование для SMTP-соединения' }, testEmail: { - title: 'Send Test Email', - description: 'Send a test email to verify your SMTP configuration', - recipientEmail: 'Recipient Email', + title: 'Отправить тестовый email', + description: 'Отправьте тестовый email, чтобы проверить конфигурацию SMTP', + recipientEmail: 'Email получателя', recipientEmailPlaceholder: "test{'@'}example.com", - sendTestEmail: 'Send Test Email', + sendTestEmail: 'Отправить тестовый email', sending: 'Отправка...', - enterRecipientHint: 'Please enter a recipient email address' + enterRecipientHint: 'Введите email-адрес получателя' }, emailTemplates: { - title: 'Email Templates', - description: 'Customize notification email subjects and HTML content for each event and locale.', - event: 'Event', - locale: 'Locale', + title: 'Email-шаблоны', + description: 'Настройте темы email-уведомлений и HTML-содержимое для каждого события и локали.', + event: 'Событие', + locale: 'Локаль', localeEn: 'English', localeZh: 'Chinese', - subject: 'Subject', - subjectPlaceholder: 'Enter the email subject', + subject: 'Тема', + subjectPlaceholder: 'Введите тему email', html: 'HTML Template', - htmlPlaceholder: 'Edit the email HTML template', - placeholders: 'Available Placeholders', - placeholdersHelp: 'Click a placeholder to copy it. The backend replaces these values when sending emails.', + htmlPlaceholder: 'Измените HTML-шаблон email', + placeholders: 'Доступные placeholders', + placeholdersHelp: 'Нажмите placeholder, чтобы скопировать его. Backend заменяет эти значения при отправке email.', livePreview: 'Live Preview', - previewSecurityHint: 'Preview HTML is generated by the backend preview endpoint and displayed in a sandboxed iframe with scripts disabled.', - preview: 'Preview / Refresh', - previewing: 'Previewing...', - save: 'Save Template', + previewSecurityHint: 'Preview HTML генерируется backend preview endpoint и отображается в sandboxed iframe с отключёнными скриптами.', + preview: 'Preview / обновить', + previewing: 'Формируется preview...', + save: 'Сохранить шаблон', saving: 'Сохранение...', - restoreOfficial: 'Restore Official', - restoring: 'Restoring...', - restoreConfirm: 'Restore the official template for this event and locale? Your custom version will be replaced.', - restoreSuccess: 'Official template restored', - saveSuccess: 'Email template saved', - placeholderCopied: 'Placeholder copied', - validationRequired: 'Subject and HTML template are required', - empty: 'No email template events or locales are available yet.', - noPreview: 'Refresh the preview to see the rendered email subject.', - customized: 'Customized' + restoreOfficial: 'Восстановить официальный', + restoring: 'Восстановление...', + restoreConfirm: 'Восстановить официальный шаблон для этого события и локали? Пользовательская версия будет заменена.', + restoreSuccess: 'Официальный шаблон восстановлен', + saveSuccess: 'Email-шаблон сохранён', + placeholderCopied: 'Placeholder скопирован', + validationRequired: 'Тема и HTML-шаблон обязательны', + empty: 'События или локали email-шаблонов пока недоступны.', + noPreview: 'Обновите preview, чтобы увидеть отрендеренную тему email.', + customized: 'Настроено' }, opsMonitoring: { title: 'Ops Monitoring', - description: 'Enable ops monitoring for troubleshooting and health visibility', - disabled: 'Ops monitoring is disabled', - enabled: 'Enable Ops Monitoring', - enabledHint: 'Enable the ops monitoring module (admin only)', - realtimeEnabled: 'Enable Realtime Monitoring', - realtimeEnabledHint: 'Enable realtime QPS/metrics push (WebSocket)', - queryMode: 'Default Query Mode', - queryModeHint: 'Default query mode for Ops Dashboard (auto/raw/preagg)', - queryModeAuto: 'Auto (recommended)', - queryModeRaw: 'Raw (most accurate, slower)', - queryModePreagg: 'Preagg (fastest, requires aggregation)', - metricsInterval: 'Metrics Collection Interval (seconds)', - metricsIntervalHint: 'How often to collect system/request metrics (60-3600 seconds)' + description: 'Включите ops monitoring для диагностики и видимости состояния', + disabled: 'Ops monitoring отключён', + enabled: 'Включить Ops Monitoring', + enabledHint: 'Включить модуль ops monitoring (только для администраторов)', + realtimeEnabled: 'Включить realtime-мониторинг', + realtimeEnabledHint: 'Включить realtime push QPS/метрик (WebSocket)', + queryMode: 'Режим запросов по умолчанию', + queryModeHint: 'Режим запросов по умолчанию для Ops Dashboard (auto/raw/preagg)', + queryModeAuto: 'Auto (рекомендуется)', + queryModeRaw: 'Raw (самый точный, медленнее)', + queryModePreagg: 'Preagg (самый быстрый, требует агрегации)', + metricsInterval: 'Интервал сбора метрик (секунды)', + metricsIntervalHint: 'Как часто собирать системные метрики и метрики запросов (60-3600 секунд)' }, adminApiKey: { - title: 'Admin API Key', - description: 'Global API key for external system integration with full admin access', - notConfigured: 'Admin API key not configured', - configured: 'Admin API key is active', - currentKey: 'Current Key', - regenerate: 'Regenerate', - regenerating: 'Regenerating...', + title: 'Админский API-ключ', + description: 'Глобальный API-ключ для интеграции внешних систем с полным админским доступом', + notConfigured: 'Админский API-ключ не настроен', + configured: 'Админский API-ключ активен', + currentKey: 'Текущий ключ', + regenerate: 'Перегенерировать', + regenerating: 'Перегенерация...', delete: 'Удалить', - deleting: 'Deleting...', - create: 'Create Key', - creating: 'Creating...', - regenerateConfirm: 'Are you sure? The current key will be immediately invalidated.', + deleting: 'Удаление...', + create: 'Создать ключ', + creating: 'Создание...', + regenerateConfirm: 'Вы уверены? Текущий ключ будет немедленно аннулирован.', deleteConfirm: - 'Are you sure you want to delete the admin API key? External integrations will stop working.', - keyGenerated: 'New admin API key generated', - keyDeleted: 'Admin API key deleted', - copyKey: 'Copy Key', - keyCopied: 'Key copied to clipboard', - keyWarning: 'This key will only be shown once. Please copy it now.', - securityWarning: 'Warning: This key provides full admin access. Keep it secure.', - usage: 'Usage: Add to request header - x-api-key: ' + 'Вы уверены, что хотите удалить админский API-ключ? Внешние интеграции перестанут работать.', + keyGenerated: 'Новый админский API-ключ сгенерирован', + keyDeleted: 'Админский API-ключ удалён', + copyKey: 'Копировать ключ', + keyCopied: 'Ключ скопирован в буфер обмена', + keyWarning: 'Этот ключ будет показан только один раз. Скопируйте его сейчас.', + securityWarning: 'Внимание: этот ключ предоставляет полный админский доступ. Храните его безопасно.', + usage: 'Использование: добавьте в header запроса - x-api-key: ' }, soraS3: { - title: 'Sora Storage', - description: 'Manage Sora media storage profiles with S3 and Google Drive support', - newProfile: 'New Profile', - reloadProfiles: 'Reload Profiles', - empty: 'No storage profiles yet, create one first', - createTitle: 'Create Storage Profile', - editTitle: 'Edit Storage Profile', - selectProvider: 'Select Storage Type', - providerS3Desc: 'S3-compatible object storage', - providerGDriveDesc: 'Google Drive cloud storage', + title: 'Хранилище Sora', + description: 'Управляйте профилями хранилища медиа Sora с поддержкой S3 и Google Drive', + newProfile: 'Новый профиль', + reloadProfiles: 'Перезагрузить профили', + empty: 'Профилей хранилища пока нет, сначала создайте профиль', + createTitle: 'Создать профиль хранилища', + editTitle: 'Редактировать профиль хранилища', + selectProvider: 'Выберите тип хранилища', + providerS3Desc: 'S3-совместимое объектное хранилище', + providerGDriveDesc: 'Облачное хранилище Google Drive', profileID: 'Profile ID', - profileName: 'Profile Name', - setActive: 'Set as active after creation', - saveProfile: 'Save Profile', - activateProfile: 'Activate', - profileCreated: 'Storage profile created', - profileSaved: 'Storage profile saved', - profileDeleted: 'Storage profile deleted', - profileActivated: 'Active storage profile switched', - profileIDRequired: 'Profile ID is required', - profileNameRequired: 'Profile name is required', - profileSelectRequired: 'Please select a profile first', - endpointRequired: 'S3 endpoint is required when enabled', - bucketRequired: 'Bucket is required when enabled', - accessKeyRequired: 'Access Key ID is required when enabled', - deleteConfirm: 'Delete storage profile {profileID}?', + profileName: 'Имя профиля', + setActive: 'Сделать активным после создания', + saveProfile: 'Сохранить профиль', + activateProfile: 'Активировать', + profileCreated: 'Профиль хранилища создан', + profileSaved: 'Профиль хранилища сохранён', + profileDeleted: 'Профиль хранилища удалён', + profileActivated: 'Активный профиль хранилища переключён', + profileIDRequired: 'ID профиля обязателен', + profileNameRequired: 'Имя профиля обязательно', + profileSelectRequired: 'Сначала выберите профиль', + endpointRequired: 'S3 endpoint обязателен, когда включено', + bucketRequired: 'Bucket обязателен, когда включено', + accessKeyRequired: 'Access Key ID обязателен, когда включено', + deleteConfirm: 'Удалить профиль хранилища {profileID}?', columns: { profile: 'Профиль', profileId: 'Profile ID', @@ -6026,351 +6026,351 @@ export default { active: 'Активен', endpoint: 'Endpoint', bucket: 'Bucket', - storagePath: 'Storage Path', - capacityUsage: 'Capacity / Used', + storagePath: 'Путь хранилища', + capacityUsage: 'Ёмкость / использовано', capacityUnlimited: 'Безлимитно', - videoCount: 'Videos', - videoCompleted: 'completed', - videoInProgress: 'in progress', - quota: 'Default Quota', - updatedAt: 'Updated At', + videoCount: 'Видео', + videoCompleted: 'завершено', + videoInProgress: 'в процессе', + quota: 'Квота по умолчанию', + updatedAt: 'Обновлено', actions: 'Действия', - rootFolder: 'Root folder', - testInTable: 'Test', + rootFolder: 'Корневая папка', + testInTable: 'Тест', testingInTable: 'Проверка...', - testTimeout: 'Test timed out (15s)' + testTimeout: 'Таймаут теста (15s)' }, - enabled: 'Enable Storage', - enabledHint: 'When enabled, Sora generated media files will be automatically uploaded', + enabled: 'Включить хранилище', + enabledHint: 'Когда включено, сгенерированные Sora медиафайлы будут автоматически загружаться', endpoint: 'S3 Endpoint', - region: 'Region', + region: 'Регион', bucket: 'Bucket', - prefix: 'Object Prefix', + prefix: 'Префикс объектов', accessKeyId: 'Access Key ID', secretAccessKey: 'Secret Access Key', - secretConfigured: '(Configured, leave blank to keep)', + secretConfigured: '(Настроено, оставьте пустым, чтобы сохранить)', cdnUrl: 'CDN URL', - cdnUrlHint: 'Optional. When configured, files are accessed via CDN URL', - forcePathStyle: 'Force Path Style', - defaultQuota: 'Default Storage Quota', - defaultQuotaHint: 'Default quota when not specified at user or group level. 0 means unlimited', + cdnUrlHint: 'Необязательно. Когда настроено, доступ к файлам выполняется через CDN URL', + forcePathStyle: 'Принудительный Path Style', + defaultQuota: 'Квота хранилища по умолчанию', + defaultQuotaHint: 'Квота по умолчанию, если не задана на уровне пользователя или группы. 0 означает безлимитно', testConnection: 'Проверить подключение', testing: 'Проверка...', - testSuccess: 'Connection test successful', - testFailed: 'Connection test failed', - saved: 'Storage settings saved successfully', - saveFailed: 'Failed to save storage settings', + testSuccess: 'Проверка соединения успешна', + testFailed: 'Проверка соединения не удалась', + saved: 'Настройки хранилища успешно сохранены', + saveFailed: 'Не удалось сохранить настройки хранилища', gdrive: { - authType: 'Authentication Method', + authType: 'Метод аутентификации', serviceAccount: 'Service Account', clientId: 'Client ID', clientSecret: 'Client Secret', - clientSecretConfigured: '(Configured, leave blank to keep)', + clientSecretConfigured: '(Настроено, оставьте пустым, чтобы сохранить)', refreshToken: 'Refresh Token', - refreshTokenConfigured: '(Configured, leave blank to keep)', + refreshTokenConfigured: '(Настроено, оставьте пустым, чтобы сохранить)', serviceAccountJson: 'Service Account JSON', - serviceAccountConfigured: '(Configured, leave blank to keep)', - folderId: 'Folder ID (optional)', - authorize: 'Authorize Google Drive', - authorizeHint: 'Get Refresh Token via OAuth2', - oauthFieldsRequired: 'Please fill in Client ID and Client Secret first', - oauthSuccess: 'Google Drive authorization successful', - oauthFailed: 'Google Drive authorization failed', - closeWindow: 'This window will close automatically', - processing: 'Processing authorization...', - testStorage: 'Test Storage', - testSuccess: 'Google Drive storage test passed (upload, access, delete all OK)', - testFailed: 'Google Drive storage test failed' + serviceAccountConfigured: '(Настроено, оставьте пустым, чтобы сохранить)', + folderId: 'Folder ID (необязательно)', + authorize: 'Авторизовать Google Drive', + authorizeHint: 'Получить Refresh Token через OAuth2', + oauthFieldsRequired: 'Сначала заполните Client ID и Client Secret', + oauthSuccess: 'Авторизация Google Drive успешна', + oauthFailed: 'Авторизация Google Drive не удалась', + closeWindow: 'Это окно закроется автоматически', + processing: 'Обработка авторизации...', + testStorage: 'Проверить хранилище', + testSuccess: 'Тест хранилища Google Drive пройден (upload, access, delete — всё OK)', + testFailed: 'Тест хранилища Google Drive не пройден' } }, overloadCooldown: { title: '529 Overload Cooldown', - description: 'Configure account scheduling pause strategy when upstream returns 529 (overloaded)', - enabled: 'Enable Overload Cooldown', - enabledHint: 'Pause account scheduling on 529 errors, auto-recover after cooldown', - cooldownMinutes: 'Cooldown Duration (minutes)', - cooldownMinutesHint: 'Duration to pause account scheduling (1-120 minutes)', - saved: 'Overload cooldown settings saved', - saveFailed: 'Failed to save overload cooldown settings' + description: 'Настройте стратегию паузы маршрутизации аккаунта, когда upstream возвращает 529 (overloaded)', + enabled: 'Включить overload cooldown', + enabledHint: 'Приостанавливать маршрутизацию аккаунта при ошибках 529, автоматически восстанавливать после cooldown', + cooldownMinutes: 'Длительность cooldown (минуты)', + cooldownMinutesHint: 'Длительность паузы маршрутизации аккаунта (1-120 минут)', + saved: 'Настройки overload cooldown сохранены', + saveFailed: 'Не удалось сохранить настройки overload cooldown' }, rateLimit429Cooldown: { - title: '429 Default Cooldown', - description: 'Configure the default account cooldown when upstream returns 429 without an explicit reset time', - enabled: 'Enable 429 Default Cooldown', - enabledHint: 'Pause account scheduling when a 429 has no reset time, then auto-recover after cooldown', - cooldownSeconds: 'Cooldown Duration (seconds)', - cooldownSecondsHint: 'Default cooldown duration (1-7200 seconds); explicit upstream reset times still take precedence', - saved: '429 default cooldown settings saved', - saveFailed: 'Failed to save 429 default cooldown settings' + title: '429 cooldown по умолчанию', + description: 'Настройте cooldown аккаунта по умолчанию, когда upstream возвращает 429 без явного времени сброса', + enabled: 'Включить 429 cooldown по умолчанию', + enabledHint: 'Приостанавливать маршрутизацию аккаунта, когда 429 не содержит времени сброса, затем автоматически восстанавливать после cooldown', + cooldownSeconds: 'Длительность cooldown (секунды)', + cooldownSecondsHint: 'Длительность cooldown по умолчанию (1-7200 секунд); явные времена сброса от upstream всё равно имеют приоритет', + saved: 'Настройки 429 cooldown по умолчанию сохранены', + saveFailed: 'Не удалось сохранить настройки 429 cooldown по умолчанию' }, streamTimeout: { - title: 'Stream Timeout Handling', - description: 'Configure account handling strategy when upstream response times out', - enabled: 'Enable Stream Timeout Handling', - enabledHint: 'Automatically handle problematic accounts when upstream times out', - timeoutSeconds: 'Timeout Threshold (seconds)', - timeoutSecondsHint: 'Stream data interval exceeding this time is considered timeout (30-300s)', - action: 'Action', - actionTempUnsched: 'Temporarily Unschedulable', - actionError: 'Mark as Error', - actionNone: 'No Action', - actionHint: 'Action to take on the account after timeout', - tempUnschedMinutes: 'Pause Duration (minutes)', - tempUnschedMinutesHint: 'Duration of temporary unschedulable state (1-60 minutes)', - thresholdCount: 'Trigger Threshold (count)', - thresholdCountHint: 'Number of timeouts before triggering action (1-10)', - thresholdWindowMinutes: 'Threshold Window (minutes)', - thresholdWindowMinutesHint: 'Time window for counting timeouts (1-60 minutes)', - saved: 'Stream timeout settings saved', - saveFailed: 'Failed to save stream timeout settings' + title: 'Обработка stream timeout', + description: 'Настройте стратегию обработки аккаунта, когда ответ upstream завершается timeout', + enabled: 'Включить обработку stream timeout', + enabledHint: 'Автоматически обрабатывать проблемные аккаунты при timeout upstream', + timeoutSeconds: 'Порог timeout (секунды)', + timeoutSecondsHint: 'Интервал stream-данных, превышающий это время, считается timeout (30-300s)', + action: 'Действие', + actionTempUnsched: 'Временно исключить из маршрутизации', + actionError: 'Пометить как ошибку', + actionNone: 'Без действия', + actionHint: 'Действие, применяемое к аккаунту после timeout', + tempUnschedMinutes: 'Длительность паузы (минуты)', + tempUnschedMinutesHint: 'Длительность состояния временного исключения из маршрутизации (1-60 минут)', + thresholdCount: 'Порог срабатывания (количество)', + thresholdCountHint: 'Количество timeout перед срабатыванием действия (1-10)', + thresholdWindowMinutes: 'Окно порога (минуты)', + thresholdWindowMinutesHint: 'Окно времени для подсчёта timeout (1-60 минут)', + saved: 'Настройки stream timeout сохранены', + saveFailed: 'Не удалось сохранить настройки stream timeout' }, rectifier: { - title: 'Request Rectifier', - description: 'Automatically fix request parameters and retry when upstream returns specific errors', - enabled: 'Enable Request Rectifier', - enabledHint: 'Master switch - disabling turns off all rectification features', - thinkingSignature: 'Thinking Signature Rectifier', - thinkingSignatureHint: 'Automatically strip signatures and retry when upstream returns thinking block signature validation errors', - thinkingBudget: 'Thinking Budget Rectifier', - thinkingBudgetHint: 'Automatically set budget to 32000 and retry when upstream returns budget_tokens constraint error (≥1024)', - apikeySignature: 'API Key Signature Rectifier', + title: 'Исправитель запросов', + description: 'Автоматически исправляет параметры запроса и повторяет попытку, когда upstream возвращает определённые ошибки', + enabled: 'Включить исправитель запросов', + enabledHint: 'Главный переключатель — отключение выключает все функции исправления', + thinkingSignature: 'Исправитель Thinking Signature', + thinkingSignatureHint: 'Автоматически удаляет signatures и повторяет запрос, когда upstream возвращает ошибки проверки signature блока thinking', + thinkingBudget: 'Исправитель Thinking Budget', + thinkingBudgetHint: 'Автоматически устанавливает budget в 32000 и повторяет запрос, когда upstream возвращает ошибку ограничения budget_tokens (≥1024)', + apikeySignature: 'Исправитель API Key Signature', apikeySignatureHint: - 'Automatically strip signatures and retry when API Key accounts receive signature-related errors (built-in patterns always apply)', - apikeyPatterns: 'Custom Match Patterns', + 'Автоматически удаляет signatures и повторяет запрос, когда аккаунты API Key получают ошибки, связанные с signature (встроенные patterns применяются всегда)', + apikeyPatterns: 'Пользовательские patterns сопоставления', apikeyPatternsHint: - 'Additional keywords matched against the response body (case-insensitive). Built-in patterns always apply; use these for supplementary matching.', + 'Дополнительные ключевые слова для сопоставления с телом ответа (без учёта регистра). Встроенные patterns применяются всегда; используйте эти для дополнительного сопоставления.', apikeyPatternPlaceholder: 'e.g., thinking_error', - addPattern: 'Add Pattern', - saved: 'Rectifier settings saved', - saveFailed: 'Failed to save rectifier settings' + addPattern: 'Добавить pattern', + saved: 'Настройки исправителя сохранены', + saveFailed: 'Не удалось сохранить настройки исправителя' }, betaPolicy: { - title: 'Beta Policy', - description: 'How to handle Beta features when configuring the forwarding of Anthropic API requests. Applicable only to the /v1/messages endpoint.', - action: 'Action', - actionPass: 'Pass (transparent)', - actionFilter: 'Filter (remove)', - actionBlock: 'Block (reject)', - scope: 'Scope', - scopeAll: 'All accounts', - scopeOAuth: 'OAuth only', - scopeAPIKey: 'API Key only', - scopeBedrock: 'Bedrock only', - errorMessage: 'Error message', - errorMessagePlaceholder: 'Custom error message when blocked', - errorMessageHint: 'Leave empty for default message', - saved: 'Beta policy settings saved', - saveFailed: 'Failed to save beta policy settings', - modelWhitelist: 'Model Whitelist', - modelWhitelistHint: 'Leave empty to apply to all models. Supports exact match and wildcard prefix (e.g., claude-opus-*)', + title: 'Политика Beta', + description: 'Как обрабатывать функции Beta при настройке пересылки Anthropic API запросов. Применимо только к endpoint /v1/messages.', + action: 'Действие', + actionPass: 'Пропустить (прозрачно)', + actionFilter: 'Фильтровать (удалить)', + actionBlock: 'Блокировать (отклонить)', + scope: 'Область', + scopeAll: 'Все аккаунты', + scopeOAuth: 'Только OAuth', + scopeAPIKey: 'Только API Key', + scopeBedrock: 'Только Bedrock', + errorMessage: 'Сообщение об ошибке', + errorMessagePlaceholder: 'Пользовательское сообщение об ошибке при блокировке', + errorMessageHint: 'Оставьте пустым для сообщения по умолчанию', + saved: 'Настройки политики Beta сохранены', + saveFailed: 'Не удалось сохранить настройки политики Beta', + modelWhitelist: 'Белый список моделей', + modelWhitelistHint: 'Оставьте пустым, чтобы применить ко всем моделям. Поддерживает точное совпадение и wildcard-префикс (например, claude-opus-*)', modelPatternPlaceholder: 'e.g., claude-opus-* or claude-opus-4-6', - addModelPattern: 'Add model pattern', + addModelPattern: 'Добавить pattern модели', removePattern: 'Удалить', - fallbackAction: 'Fallback Action', - fallbackActionHint: 'Action for models not matching the whitelist', - fallbackErrorMessagePlaceholder: 'Custom error message when non-whitelisted models are blocked', - quickPresets: 'Quick Presets', - presetOpusOnly: 'Opus only for 1M', - presetOpusOnlyDesc: 'Pass for Opus, filter others', - commonPatterns: 'Common patterns' + fallbackAction: 'Fallback-действие', + fallbackActionHint: 'Действие для моделей, не совпадающих с белым списком', + fallbackErrorMessagePlaceholder: 'Пользовательское сообщение об ошибке при блокировке моделей вне белого списка', + quickPresets: 'Быстрые пресеты', + presetOpusOnly: 'Только Opus для 1M', + presetOpusOnlyDesc: 'Пропускать для Opus, фильтровать остальные', + commonPatterns: 'Частые patterns' }, openaiFastPolicy: { - title: 'OpenAI Fast/Flex Policy', - description: 'Intercept, filter, or pass OpenAI fast(priority) / flex requests based on the request body service_tier field. Applies to the OpenAI gateway only.', - empty: 'No rules configured. Click the button below to add one.', - ruleHeader: 'Rule #{index}', - removeRule: 'Remove rule', - addRule: 'Add rule', - saveHint: 'Saved together with system settings (click the global Save button at the bottom of the page).', - serviceTier: 'service_tier match', - tierAll: 'All tiers', + title: 'Политика OpenAI Fast/Flex', + description: 'Перехватывает, фильтрует или пропускает OpenAI fast(priority) / flex запросы на основе поля service_tier в теле запроса. Применяется только к OpenAI gateway.', + empty: 'Правила не настроены. Нажмите кнопку ниже, чтобы добавить правило.', + ruleHeader: 'Правило #{index}', + removeRule: 'Удалить правило', + addRule: 'Добавить правило', + saveHint: 'Сохраняется вместе с настройками системы (нажмите глобальную кнопку сохранения внизу страницы).', + serviceTier: 'совпадение service_tier', + tierAll: 'Все tiers', tierPriority: 'priority (fast)', tierFlex: 'flex', - action: 'Action', - actionPass: 'Pass (keep service_tier)', - actionFilter: 'Filter (remove service_tier)', - actionBlock: 'Block (reject request)', - scope: 'Scope', - scopeAll: 'All accounts', - scopeOAuth: 'OAuth only', - scopeAPIKey: 'API Key only', - scopeBedrock: 'Bedrock only', - errorMessage: 'Error message', - errorMessagePlaceholder: 'Custom error message when blocked', - errorMessageHint: 'Leave empty for the default message.', - modelWhitelist: 'Model whitelist', - modelWhitelistHint: 'Leave empty to apply to all models. Supports exact match and wildcard prefix (e.g., gpt-5.5*).', + action: 'Действие', + actionPass: 'Пропустить (сохранить service_tier)', + actionFilter: 'Фильтровать (удалить service_tier)', + actionBlock: 'Блокировать (отклонить запрос)', + scope: 'Область', + scopeAll: 'Все аккаунты', + scopeOAuth: 'Только OAuth', + scopeAPIKey: 'Только API Key', + scopeBedrock: 'Только Bedrock', + errorMessage: 'Сообщение об ошибке', + errorMessagePlaceholder: 'Пользовательское сообщение об ошибке при блокировке', + errorMessageHint: 'Оставьте пустым для сообщения по умолчанию.', + modelWhitelist: 'Белый список моделей', + modelWhitelistHint: 'Оставьте пустым, чтобы применить ко всем моделям. Поддерживает точное совпадение и wildcard-префикс (например, gpt-5.5*).', modelPatternPlaceholder: 'e.g., gpt-5.5 or gpt-5.5*', - addModelPattern: 'Add model pattern', - fallbackAction: 'Fallback action', - fallbackActionHint: 'Action for models not matching the whitelist.', - fallbackErrorMessagePlaceholder: 'Custom error message when non-whitelisted models are blocked' + addModelPattern: 'Добавить pattern модели', + fallbackAction: 'Fallback-действие', + fallbackActionHint: 'Действие для моделей, не совпадающих с белым списком.', + fallbackErrorMessagePlaceholder: 'Пользовательское сообщение об ошибке при блокировке моделей вне белого списка' }, wechatConnect: { title: 'WeChat Connect', - description: 'Third-party login configuration for WeChat Open Platform or Official Account / Mini Program.', - enabledLabel: 'Enable WeChat Connect', - enabledHint: 'Enable this to configure WeChat OAuth callbacks and authorization.', + description: 'Настройка стороннего входа для WeChat Open Platform или Official Account / Mini Program.', + enabledLabel: 'Включить WeChat Connect', + enabledHint: 'Включите, чтобы настроить WeChat OAuth callbacks и авторизацию.', appIdLabel: 'App ID', appIdPlaceholder: 'WeChat App ID', appSecretLabel: 'App Secret', - appSecretConfiguredPlaceholder: 'Secret configured. Leave empty to keep the current value.', + appSecretConfiguredPlaceholder: 'Secret настроен. Оставьте пустым, чтобы сохранить текущее значение.', appSecretPlaceholder: 'WeChat App Secret', - appSecretConfiguredHint: 'Secret configured. Leave empty to keep the current value.', - appSecretHint: 'Enter a new secret to replace the current WeChat credential.', - modeLabel: 'Mode', - openModeLabel: 'Use Open outside WeChat', - openModeHint: 'Use Open Platform QR authorization outside the WeChat browser.', - mpModeLabel: 'Use MP inside WeChat', - mpModeHint: 'Use Official Account authorization inside the WeChat browser.', + appSecretConfiguredHint: 'Secret настроен. Оставьте пустым, чтобы сохранить текущее значение.', + appSecretHint: 'Введите новый secret, чтобы заменить текущие учётные данные WeChat.', + modeLabel: 'Режим', + openModeLabel: 'Использовать Open вне WeChat', + openModeHint: 'Использовать QR-авторизацию Open Platform вне браузера WeChat.', + mpModeLabel: 'Использовать MP внутри WeChat', + mpModeHint: 'Использовать авторизацию Official Account внутри браузера WeChat.', redirectUrlLabel: 'Redirect URL', redirectUrlPlaceholder: 'https://your-site.com/api/v1/auth/oauth/wechat/callback', - generateAndCopy: 'Generate & Copy (current site)', - redirectUrlSetAndCopied: 'Redirect URL generated and copied to clipboard', + generateAndCopy: 'Сгенерировать и скопировать (текущий сайт)', + redirectUrlSetAndCopied: 'Redirect URL сгенерирован и скопирован в буфер обмена', frontendRedirectUrlLabel: 'Frontend redirect URL', frontendRedirectUrlPlaceholder: '/auth/wechat/callback', - frontendRedirectUrlHint: 'Usually the frontend route callback path; keep it aligned with the backend.' + frontendRedirectUrlHint: 'Обычно это callback path frontend-маршрута; держите его согласованным с backend.' }, authSourceDefaults: { - title: 'Auth Source Defaults', - description: 'Configure per-source default balance, concurrency, subscriptions, and grant rules.', - requireEmailLabel: 'Require email on third-party signup', - requireEmailHint: 'When enabled, Linux DO, OIDC, and WeChat signups must provide an email before account creation.', - enabledHint: 'These defaults apply when a new user registers through this source. Grant on first bind only applies when an existing user binds this source.', + title: 'Значения по умолчанию для источника auth', + description: 'Настройте баланс, параллелизм, подписки и правила выдачи по умолчанию для каждого источника.', + requireEmailLabel: 'Требовать email при сторонней регистрации', + requireEmailHint: 'Когда включено, регистрации через Linux DO, OIDC и WeChat должны предоставить email перед созданием аккаунта.', + enabledHint: 'Эти значения по умолчанию применяются, когда новый пользователь регистрируется через этот источник. Grant on first bind применяется только когда существующий пользователь привязывает этот источник.', sources: { email: { - title: 'Email signup', - description: 'Default quota grants for email-password signups.' + title: 'Регистрация через email', + description: 'Выдачи квот по умолчанию для регистраций по email-паролю.' }, linuxdo: { - title: 'Linux DO signup', - description: 'Default quota grants for Linux DO signups.' + title: 'Регистрация через Linux DO', + description: 'Выдачи квот по умолчанию для регистраций через Linux DO.' }, oidc: { - title: 'OIDC signup', - description: 'Default quota grants for OIDC signups.' + title: 'Регистрация через OIDC', + description: 'Выдачи квот по умолчанию для регистраций через OIDC.' }, wechat: { - title: 'WeChat signup', - description: 'Default quota grants for WeChat signups.' + title: 'Регистрация через WeChat', + description: 'Выдачи квот по умолчанию для регистраций через WeChat.' } }, - grantOnFirstBindLabel: 'Grant on first bind', - grantOnFirstBindHint: 'Grant default entitlements when an existing user first binds this source.', - defaultSubscriptionsLabel: 'Default subscriptions', - defaultSubscriptionsHint: 'Applies only to this auth source. Leave empty to skip source-specific subscriptions.', - noSourceSubscriptions: 'No source-specific default subscriptions configured.', - platformQuotasOverride: 'Platform Quota Overrides', - platformQuotasOverrideHint: 'Blank fields inherit the system default. Set to 0 to fully block that window for this auth source.', + grantOnFirstBindLabel: 'Выдать при первой привязке', + grantOnFirstBindHint: 'Выдавать права по умолчанию, когда существующий пользователь впервые привязывает этот источник.', + defaultSubscriptionsLabel: 'Подписки по умолчанию', + defaultSubscriptionsHint: 'Применяется только к этому auth source. Оставьте пустым, чтобы пропустить подписки для конкретного источника.', + noSourceSubscriptions: 'Подписки по умолчанию для конкретного источника не настроены.', + platformQuotasOverride: 'Переопределения квот платформ', + platformQuotasOverrideHint: 'Пустые поля наследуют системное значение по умолчанию. Установите 0, чтобы полностью заблокировать это окно для данного auth source.', }, paymentVisibleMethods: { - methodLabel: '{title} visible method', - methodHint: 'Controls whether checkout shows this method and which source key it exposes.', - sourceLabel: 'Payment source', - sourceHint: 'Choose an explicit source before enabling the method. Not configured methods are not exposed.', - sourceRequiredError: 'Select a payment source before enabling {title}.' + methodLabel: 'Видимый метод {title}', + methodHint: 'Управляет тем, показывает ли checkout этот метод и какой source key он раскрывает.', + sourceLabel: 'Источник оплаты', + sourceHint: 'Выберите явный источник перед включением метода. Ненастроенные методы не раскрываются.', + sourceRequiredError: 'Выберите источник оплаты перед включением {title}.' }, openaiExperimentalScheduler: { - title: 'OpenAI experimental scheduler policy', - description: "Disabled by default. When enabled, this only changes the gateway's experimental account-selection policy for OpenAI traffic; it does not indicate an upstream OpenAI capability." + title: 'Экспериментальная политика OpenAI scheduler', + description: "По умолчанию отключено. Когда включено, меняет только экспериментальную политику выбора аккаунта шлюза для OpenAI traffic; это не означает наличие такой возможности у upstream OpenAI." }, - saveSettings: 'Save Settings', + saveSettings: 'Сохранить настройки', saving: 'Сохранение...', - settingsSaved: 'Settings saved successfully', - smtpConnectionSuccess: 'SMTP connection successful', - testEmailSent: 'Test email sent successfully', - failedToLoad: 'Failed to load settings', - failedToSave: 'Failed to save settings', - failedToTestSmtp: 'SMTP connection test failed', - failedToSendTestEmail: 'Failed to send test email' + settingsSaved: 'Настройки успешно сохранены', + smtpConnectionSuccess: 'SMTP-соединение успешно', + testEmailSent: 'Тестовый email успешно отправлен', + failedToLoad: 'Не удалось загрузить настройки', + failedToSave: 'Не удалось сохранить настройки', + failedToTestSmtp: 'Проверка SMTP-соединения не удалась', + failedToSendTestEmail: 'Не удалось отправить тестовый email' }, // Error Passthrough Rules errorPassthrough: { - title: 'Error Passthrough Rules', - description: 'Configure how upstream errors are returned to clients', - createRule: 'Create Rule', - editRule: 'Edit Rule', - deleteRule: 'Delete Rule', - noRules: 'No rules configured', - createFirstRule: 'Create your first error passthrough rule', - allPlatforms: 'All Platforms', - passthrough: 'Passthrough', + title: 'Правила проброса ошибок', + description: 'Настройте, как upstream-ошибки возвращаются клиентам', + createRule: 'Создать правило', + editRule: 'Редактировать правило', + deleteRule: 'Удалить правило', + noRules: 'Правила не настроены', + createFirstRule: 'Создайте первое правило проброса ошибок', + allPlatforms: 'Все платформы', + passthrough: 'Проброс', custom: 'Свой', code: 'Код', body: 'Body', - skipMonitoring: 'Skip Monitoring', + skipMonitoring: 'Пропустить мониторинг', // Columns columns: { - priority: 'Priority', + priority: 'Приоритет', name: 'Имя', - conditions: 'Conditions', - platforms: 'Platforms', - behavior: 'Behavior', + conditions: 'Условия', + platforms: 'Платформы', + behavior: 'Поведение', status: 'Статус', actions: 'Действия' }, // Match Mode matchMode: { - any: 'Code OR Keyword', - all: 'Code AND Keyword', - anyHint: 'Status code matches any error code, OR message contains any keyword', - allHint: 'Status code matches any error code, AND message contains any keyword' + any: 'Код ИЛИ ключевое слово', + all: 'Код И ключевое слово', + anyHint: 'Status code совпадает с любым кодом ошибки ИЛИ сообщение содержит любое ключевое слово', + allHint: 'Status code совпадает с любым кодом ошибки И сообщение содержит любое ключевое слово' }, // Form form: { - name: 'Rule Name', - namePlaceholder: 'e.g., Context Limit Passthrough', - priority: 'Priority', - priorityHint: 'Lower values have higher priority', + name: 'Название правила', + namePlaceholder: 'например Context Limit Passthrough', + priority: 'Приоритет', + priorityHint: 'Меньшие значения имеют более высокий приоритет', description: 'Описание', - descriptionPlaceholder: 'Describe the purpose of this rule...', - matchConditions: 'Match Conditions', - errorCodes: 'Error Codes', + descriptionPlaceholder: 'Опишите назначение этого правила...', + matchConditions: 'Условия сопоставления', + errorCodes: 'Коды ошибок', errorCodesPlaceholder: '422, 400, 429', - errorCodesHint: 'Separate multiple codes with commas', - keywords: 'Keywords', + errorCodesHint: 'Разделяйте несколько кодов запятыми', + keywords: 'Ключевые слова', keywordsPlaceholder: 'One keyword per line\ncontext limit\nmodel not supported', - keywordsHint: 'One keyword per line, case-insensitive', - matchMode: 'Match Mode', - platforms: 'Platforms', - platformsHint: 'Leave empty to apply to all platforms', - responseBehavior: 'Response Behavior', - passthroughCode: 'Passthrough upstream status code', - responseCode: 'Custom status code', - passthroughBody: 'Passthrough upstream error message', - customMessage: 'Custom error message', - customMessagePlaceholder: 'Error message to return to client...', + keywordsHint: 'Одно ключевое слово на строку, без учёта регистра', + matchMode: 'Режим сопоставления', + platforms: 'Платформы', + platformsHint: 'Оставьте пустым, чтобы применить ко всем платформам', + responseBehavior: 'Поведение ответа', + passthroughCode: 'Проброс upstream status code', + responseCode: 'Пользовательский status code', + passthroughBody: 'Проброс upstream-сообщения об ошибке', + customMessage: 'Пользовательское сообщение об ошибке', + customMessagePlaceholder: 'Сообщение об ошибке для возврата клиенту...', skipMonitoring: 'Skip monitoring', - skipMonitoringHint: 'When enabled, errors matching this rule will not be recorded in ops monitoring', - enabled: 'Enable this rule' + skipMonitoringHint: 'Когда включено, ошибки, соответствующие этому правилу, не будут записываться в ops monitoring', + enabled: 'Включить это правило' }, // Messages - nameRequired: 'Please enter rule name', - conditionsRequired: 'Please configure at least one error code or keyword', - ruleCreated: 'Rule created successfully', - ruleUpdated: 'Rule updated successfully', - ruleDeleted: 'Rule deleted successfully', - deleteConfirm: 'Are you sure you want to delete rule "{name}"?', - failedToLoad: 'Failed to load rules', - failedToSave: 'Failed to save rule', - failedToDelete: 'Failed to delete rule', - failedToToggle: 'Failed to toggle status' + nameRequired: 'Введите название правила', + conditionsRequired: 'Настройте хотя бы один код ошибки или ключевое слово', + ruleCreated: 'Правило успешно создано', + ruleUpdated: 'Правило успешно обновлено', + ruleDeleted: 'Правило успешно удалено', + deleteConfirm: 'Удалить правило "{name}"?', + failedToLoad: 'Не удалось загрузить правила', + failedToSave: 'Не удалось сохранить правило', + failedToDelete: 'Не удалось удалить правило', + failedToToggle: 'Не удалось переключить статус' }, // TLS Fingerprint Profiles tlsFingerprintProfiles: { - title: 'TLS Fingerprint Profiles', - description: 'Manage TLS fingerprint profiles for simulating specific client TLS handshake characteristics', - createProfile: 'Create Profile', + title: 'Профили TLS fingerprint', + description: 'Управляйте профилями TLS fingerprint для имитации характеристик TLS handshake конкретного клиента', + createProfile: 'Создать профиль', editProfile: 'Изменить профиль', - deleteProfile: 'Delete Profile', - noProfiles: 'No profiles configured', - createFirstProfile: 'Create your first TLS fingerprint profile', + deleteProfile: 'Удалить профиль', + noProfiles: 'Профили не настроены', + createFirstProfile: 'Создайте первый профиль TLS fingerprint', columns: { name: 'Имя', @@ -6381,41 +6381,41 @@ export default { }, form: { - pasteYaml: 'Paste YAML Configuration', - pasteYamlPlaceholder: 'Paste YAML output from TLS Fingerprint Collector here...', - pasteYamlHint: 'Paste the YAML copied from TLS Fingerprint Collector to auto-fill all fields.', - openCollector: 'Open Collector', - parseYaml: 'Parse YAML', - yamlParsed: 'YAML parsed successfully, fields auto-filled', - yamlParseFailed: 'Failed to parse YAML: name field not found', - name: 'Profile Name', - namePlaceholder: 'e.g. macOS Node.js v24', + pasteYaml: 'Вставить YAML-конфигурацию', + pasteYamlPlaceholder: 'Вставьте сюда YAML output из TLS Fingerprint Collector...', + pasteYamlHint: 'Вставьте YAML, скопированный из TLS Fingerprint Collector, чтобы автоматически заполнить все поля.', + openCollector: 'Открыть Collector', + parseYaml: 'Разобрать YAML', + yamlParsed: 'YAML успешно разобран, поля автоматически заполнены', + yamlParseFailed: 'Не удалось разобрать YAML: поле name не найдено', + name: 'Название профиля', + namePlaceholder: 'например macOS Node.js v24', description: 'Описание', - descriptionPlaceholder: 'Optional description for this profile', - enableGrease: 'Enable GREASE', - enableGreaseHint: 'Insert GREASE values in TLS ClientHello extensions', + descriptionPlaceholder: 'Необязательное описание для этого профиля', + enableGrease: 'Включить GREASE', + enableGreaseHint: 'Вставлять GREASE values в extensions TLS ClientHello', cipherSuites: 'Cipher Suites', - cipherSuitesHint: 'Comma-separated hex values, e.g. 0x1301, 0x1302, 0xc02c', + cipherSuitesHint: 'Hex-значения через запятую, например 0x1301, 0x1302, 0xc02c', curves: 'Elliptic Curves', - curvesHint: 'Comma-separated curve IDs', + curvesHint: 'Curve IDs через запятую', pointFormats: 'Point Formats', signatureAlgorithms: 'Signature Algorithms', alpnProtocols: 'ALPN Protocols', - alpnProtocolsHint: 'Comma-separated, e.g. h2, http/1.1', - supportedVersions: 'Supported TLS Versions', + alpnProtocolsHint: 'Через запятую, например h2, http/1.1', + supportedVersions: 'Поддерживаемые TLS Versions', keyShareGroups: 'Key Share Groups', pskModes: 'PSK Modes', extensions: 'Extensions' }, - deleteConfirm: 'Delete Profile', - deleteConfirmMessage: 'Are you sure you want to delete profile "{name}"? Accounts using this profile will fall back to the built-in default.', - createSuccess: 'Profile created successfully', + deleteConfirm: 'Удалить профиль', + deleteConfirmMessage: 'Удалить профиль "{name}"? Аккаунты, использующие этот профиль, вернутся к встроенному значению по умолчанию.', + createSuccess: 'Профиль успешно создан', updateSuccess: 'Профиль обновлён', - deleteSuccess: 'Profile deleted successfully', - loadFailed: 'Failed to load profiles', - saveFailed: 'Failed to save profile', - deleteFailed: 'Failed to delete profile' + deleteSuccess: 'Профиль успешно удалён', + loadFailed: 'Не удалось загрузить профили', + saveFailed: 'Не удалось сохранить профиль', + deleteFailed: 'Не удалось удалить профиль' } }, @@ -6540,8 +6540,8 @@ export default { restartTour: 'Перезапустить обучение', dontShowAgain: 'Больше не показывать', dontShowAgainTitle: 'Закрыть обучение навсегда', - confirmDontShow: "Are you sure you don't want to see the onboarding guide again?\n\nYou can restart it anytime from the user menu in the top right corner.", - confirmExit: 'Are you sure you want to exit the onboarding guide? You can restart it anytime from the top right menu.', + confirmDontShow: "Больше не показывать мастер настройки?\n\nВы сможете перезапустить его в любое время из меню пользователя в правом верхнем углу.", + confirmExit: 'Выйти из мастера настройки? Вы сможете перезапустить его в любое время из меню в правом верхнем углу.', interactiveHint: 'Нажмите Enter или кликните, чтобы продолжить', navigation: { flipPage: 'Перелистнуть', @@ -6550,132 +6550,132 @@ export default { // Admin tour steps admin: { welcome: { - title: '👋 Welcome to Sub2API', - description: '

Sub2API is a powerful AI service gateway platform that helps you easily manage and distribute AI services.

🎯 Core Features:

  • 📦 Group Management - Create service tiers (VIP, Free Trial, etc.)
  • 🔗 Account Pool - Connect multiple upstream AI service accounts
  • 🔑 Key Distribution - Generate independent API Keys for users
  • 💰 Billing Control - Flexible rate and quota management

Let\'s complete the initial setup in 3 minutes →

', - nextBtn: 'Start Setup 🚀', - prevBtn: 'Skip' + title: '👋 Добро пожаловать в Sub2API', + description: '

Sub2API — мощная платформа AI service gateway, которая помогает удобно управлять AI-сервисами и распределять их.

🎯 Основные возможности:

  • 📦 Управление группами - создание тарифных уровней (VIP, пробный доступ и т. д.)
  • 🔗 Пул аккаунтов - подключение нескольких upstream AI-аккаунтов
  • 🔑 Выдача ключей - генерация независимых API Key для пользователей
  • 💰 Контроль списаний - гибкое управление тарифами и квотами

Завершим первичную настройку за 3 минуты →

', + nextBtn: 'Начать 🚀', + prevBtn: 'Пропустить' }, groupManage: { - title: '📦 Step 1: Group Management', - description: '

What is a Group?

Groups are the core concept of Sub2API, like a "service package":

  • 🎯 Each group can contain multiple upstream accounts
  • 💰 Each group has independent billing multiplier
  • 👥 Can be set as public or exclusive

💡 Example: You can create "VIP Premium" (high rate) and "Free Trial" (low rate) groups

👉 Click "Group Management" on the left sidebar

' + title: '📦 Шаг 1: Управление группами', + description: '

Что такое группа?

Группы — ключевая концепция Sub2API, похожая на "пакет услуг":

  • 🎯 Каждая группа может содержать несколько upstream аккаунтов
  • 💰 У каждой группы свой тарифный коэффициент
  • 👥 Группа может быть публичной или эксклюзивной

💡 Пример: можно создать группы "VIP Premium" (высокий тариф) и "Free Trial" (низкий тариф)

👉 Нажмите "Управление группами" в левом меню

' }, createGroup: { - title: '➕ Create New Group', - description: '

Let\'s create your first group.

📝 Tip: Recommend creating a test group first to familiarize yourself with the process

👉 Click the "Create Group" button

' + title: '➕ Новая группа', + description: '

Создадим первую группу.

📝 Совет: сначала создайте тестовую группу, чтобы познакомиться с процессом

👉 Нажмите кнопку "Создать группу"

' }, groupName: { - title: '✏️ 1. Group Name', - description: '

Give your group an easy-to-identify name.

💡 Naming Suggestions:
  • "Test Group" - For testing
  • "VIP Premium" - High-quality service
  • "Free Trial" - Trial version

Click "Далее" when done

', + title: '✏️ 1. Название группы', + description: '

Задайте группе понятное название.

💡 Примеры названий:
  • "Test Group" - для тестов
  • "VIP Premium" - сервис высокого качества
  • "Free Trial" - пробная версия

Когда закончите, нажмите "Далее"

', nextBtn: 'Далее' }, groupPlatform: { - title: '🤖 2. Select Platform', - description: '

Choose the AI platform this group supports.

📌 Platform Guide:
  • Anthropic - Claude models
  • OpenAI - GPT models
  • Google - Gemini models

One group can only have one platform

', + title: '🤖 2. Выбор платформы', + description: '

Выберите AI-платформу, которую поддерживает эта группа.

📌 Подсказка по платформам:
  • Anthropic - модели Claude
  • OpenAI - модели GPT
  • Google - модели Gemini

Одна группа может иметь только одну платформу

', nextBtn: 'Далее' }, groupMultiplier: { - title: '💰 3. Rate Multiplier', - description: '

Set the billing multiplier to control user charges.

⚙️ Billing Rules:
  • 1.0 - Original price (cost price)
  • 1.5 - User consumes $1, charged $1.5
  • 2.0 - User consumes $1, charged $2
  • 0.8 - Subsidy mode (loss-making)

Recommend setting test group to 1.0

', + title: '💰 3. Тарифный коэффициент', + description: '

Задайте коэффициент списания, чтобы управлять начислениями пользователям.

⚙️ Правила списания:
  • 1.0 - исходная цена (себестоимость)
  • 1.5 - пользователь расходует $1, списывается $1.5
  • 2.0 - пользователь расходует $1, списывается $2
  • 0.8 - режим субсидии (убыточный)

Для тестовой группы рекомендуется 1.0

', nextBtn: 'Далее' }, groupExclusive: { - title: '🔒 4. Exclusive Group (Optional)', - description: '

Control group visibility and access permissions.

🔐 Permission Guide:
  • Off - Public group, visible to all users
  • On - Exclusive group, only for specified users

💡 Use Cases: VIP exclusive, internal testing, special customers

', + title: '🔒 4. Эксклюзивная группа', + description: '

Управляйте видимостью группы и правами доступа.

🔐 Подсказка по доступу:
  • Off - публичная группа, видна всем пользователям
  • On - эксклюзивная группа, только для указанных пользователей

💡 Сценарии: VIP-доступ, внутреннее тестирование, особые клиенты

', nextBtn: 'Далее' }, groupSubmit: { - title: '✅ Save Group', - description: '

Confirm the information and click create to save the group.

⚠️ Note: Platform type cannot be changed after creation, but other settings can be edited anytime

📌 Next Step: After creation, we\'ll add upstream accounts to this group

👉 Click "Создать" button

' + title: '✅ Сохранить группу', + description: '

Проверьте данные и нажмите создание, чтобы сохранить группу.

⚠️ Важно: тип платформы нельзя изменить после создания, но остальные настройки можно редактировать в любое время

📌 Следующий шаг: после создания добавим upstream аккаунты в эту группу

👉 Нажмите кнопку "Создать"

' }, accountManage: { - title: '🔗 Step 2: Add Account', - description: '

Great! Group created successfully 🎉

Now add upstream AI service accounts to enable actual service delivery.

🔑 Account Purpose:
  • Connect to upstream AI services (Claude, GPT, etc.)
  • One group can contain multiple accounts (load balancing)
  • Supports OAuth and Session Key methods

👉 Click "Управление аккаунтами" on the left sidebar

' + title: '🔗 Шаг 2: Добавить аккаунт', + description: '

Отлично! Группа успешно создана 🎉

Теперь добавьте upstream AI-аккаунты, чтобы включить фактическую выдачу сервиса.

🔑 Назначение аккаунта:
  • Подключение к upstream AI-сервисам (Claude, GPT и т. д.)
  • Одна группа может содержать несколько аккаунтов (балансировка нагрузки)
  • Поддерживаются методы OAuth и Session Key

👉 Нажмите "Управление аккаунтами" в левом меню

' }, createAccount: { - title: '➕ Add New Account', - description: '

Click the button to start adding your first upstream account.

💡 Tip: Recommend using OAuth method - more secure and no manual key extraction needed

👉 Click "Add Account" button

' + title: '➕ Новый аккаунт', + description: '

Нажмите кнопку, чтобы добавить первый upstream аккаунт.

💡 Совет: рекомендуется использовать OAuth — это безопаснее и не требует ручного извлечения ключа

👉 Нажмите кнопку "Добавить аккаунт"

' }, accountName: { - title: '✏️ 1. Account Name', - description: '

Set an easy-to-identify name for the account.

💡 Naming Suggestions: "Claude Main", "GPT Backup 1", "Test Account", etc.

', + title: '✏️ 1. Название аккаунта', + description: '

Задайте аккаунту понятное название.

💡 Примеры названий: "Claude Main", "GPT Backup 1", "Test Account" и т. д.

', nextBtn: 'Далее' }, accountPlatform: { - title: '🤖 2. Select Platform', - description: '

Choose the service provider platform for this account.

⚠️ Important: Platform must match the group you just created

', + title: '🤖 2. Выбор платформы', + description: '

Выберите платформу провайдера сервиса для этого аккаунта.

⚠️ Важно: платформа должна совпадать с группой, которую вы только что создали

', nextBtn: 'Далее' }, accountType: { - title: '🔐 3. Authorization Method', - description: '

Choose the account authorization method.

✅ Recommended: OAuth Method
  • No manual key extraction needed
  • More secure with auto-refresh support
  • Works with Claude Code, ChatGPT OAuth
📌 Session Key Method
  • Requires manual extraction from browser
  • May need periodic updates
  • For platforms without OAuth support
', + title: '🔐 3. Способ авторизации', + description: '

Выберите способ авторизации аккаунта.

✅ Рекомендуется: OAuth
  • Не нужно вручную извлекать ключ
  • Безопаснее, есть поддержка автообновления
  • Работает с Claude Code, ChatGPT OAuth
📌 Метод Session Key
  • Требует ручного извлечения из браузера
  • Может требовать периодического обновления
  • Для платформ без поддержки OAuth
', nextBtn: 'Далее' }, accountPriority: { - title: '⚖️ 4. Priority (Optional)', - description: '

Set the account call priority.

📊 Priority Rules:
  • Lower number = higher priority
  • System uses low-value accounts first
  • Same priority = random selection

💡 Use Case: Set main account to lower value, backup accounts to higher value

', + title: '⚖️ 4. Приоритет', + description: '

Задайте приоритет вызова аккаунта.

📊 Правила приоритета:
  • Меньше число = выше приоритет
  • Система сначала использует аккаунты с меньшим значением
  • Одинаковый приоритет = случайный выбор

💡 Сценарий: основному аккаунту задайте меньшее значение, резервным — большее

', nextBtn: 'Далее' }, accountGroups: { - title: '🎯 5. Assign Groups', - description: '

Key Step! Assign the account to the group you just created.

⚠️ Important Reminder:
  • Must select at least one group
  • Unassigned accounts cannot be used
  • One account can be assigned to multiple groups

💡 Tip: Select the test group you just created

', + title: '🎯 5. Назначить группы', + description: '

Ключевой шаг! Назначьте аккаунт группе, которую только что создали.

⚠️ Важно:
  • Нужно выбрать хотя бы одну группу
  • Неназначенные аккаунты нельзя использовать
  • Один аккаунт можно назначить нескольким группам

💡 Совет: выберите тестовую группу, которую только что создали

', nextBtn: 'Далее' }, accountSubmit: { - title: '✅ Save Account', - description: '

Confirm the information and click save.

📌 OAuth Flow:
  • Will redirect to service provider page after clicking save
  • Complete login and authorization on provider page
  • Auto-return after successful authorization

📌 Next Step: After adding account, we\'ll create an API key

👉 Click "Сохранить" button

' + title: '✅ Сохранить аккаунт', + description: '

Проверьте данные и нажмите сохранение.

📌 OAuth Flow:
  • После сохранения будет переход на страницу провайдера
  • Выполните вход и авторизацию на странице провайдера
  • После успешной авторизации произойдёт автоматический возврат

📌 Следующий шаг: после добавления аккаунта создадим API-ключ

👉 Нажмите кнопку "Сохранить"

' }, keyManage: { - title: '🔑 Step 3: Generate Key', - description: '

Congratulations! Account setup complete 🎉

Final step: generate an API Key to test if the service works properly.

🔑 API Key Purpose:
  • Credential for calling AI services
  • Each key is bound to one group
  • Can set quota and expiration
  • Supports independent usage statistics

👉 Click "API-ключи" on the left sidebar

' + title: '🔑 Шаг 3: Создать ключ', + description: '

Поздравляем! Настройка аккаунта завершена 🎉

Последний шаг: создайте API Key, чтобы проверить работу сервиса.

🔑 Назначение API Key:
  • Учётные данные для вызова AI-сервисов
  • Каждый ключ привязан к одной группе
  • Можно задать квоту и срок действия
  • Поддерживает независимую статистику расхода

👉 Нажмите "API-ключи" в левом меню

' }, createKey: { - title: '➕ Create Key', - description: '

Click the button to create your first API Key.

💡 Tip: Copy and save immediately after creation - key is only shown once

👉 Click "Create Key" button

' + title: '➕ Создать ключ', + description: '

Нажмите кнопку, чтобы создать первый API Key.

💡 Совет: сразу скопируйте и сохраните ключ после создания — он показывается только один раз

👉 Нажмите кнопку "Создать ключ"

' }, keyName: { - title: '✏️ 1. Key Name', - description: '

Set an easy-to-manage name for the key.

💡 Naming Suggestions: "Test Key", "Production", "Mobile", etc.

', + title: '✏️ 1. Название ключа', + description: '

Задайте ключу удобное название.

💡 Примеры названий: "Test Key", "Production", "Mobile" и т. д.

', nextBtn: 'Далее' }, keyGroup: { - title: '🎯 2. Select Group', - description: '

Select the group you just configured.

📌 Group Determines:
  • Which accounts this key can use
  • What billing multiplier applies
  • Whether it\'s an exclusive key

💡 Tip: Select the test group you just created

', + title: '🎯 2. Выбор группы', + description: '

Выберите группу, которую только что настроили.

📌 Группа определяет:
  • Какие аккаунты может использовать этот ключ
  • Какой тарифный коэффициент применяется
  • Является ли ключ эксклюзивным

💡 Совет: выберите тестовую группу, которую только что создали

', nextBtn: 'Далее' }, keySubmit: { - title: '🎉 Generate and Copy', - description: '

System will generate a complete API Key after clicking create.

⚠️ Important Reminder:
  • Key is only shown once, copy immediately
  • Need to regenerate if lost
  • Keep it safe, don\'t share with others
🚀 Next Steps:
  • Copy the generated sk-xxx key
  • Use in any OpenAI-compatible client
  • Start experiencing AI services!

👉 Click "Создать" button

' + title: '🎉 Создать и скопировать', + description: '

После нажатия на создание система сгенерирует полноценный API Key.

⚠️ Важно:
  • Ключ показывается только один раз, скопируйте сразу
  • Если потеряете, потребуется сгенерировать заново
  • Храните безопасно и не передавайте другим
🚀 Следующие шаги:
  • Скопируйте сгенерированный ключ sk-xxx
  • Используйте его в любом OpenAI-compatible клиенте
  • Начните пользоваться AI-сервисами!

👉 Нажмите кнопку "Создать"

' } }, // User tour steps user: { welcome: { - title: '👋 Welcome to Sub2API', - description: '

Hello! Welcome to the Sub2API AI service platform.

🎯 Quick Start:

  • 🔑 Create API Key
  • 📋 Copy key to your application
  • 🚀 Start using AI services

Just 1 minute, let\'s get started →

', - nextBtn: 'Start 🚀', - prevBtn: 'Skip' + title: '👋 Добро пожаловать в Sub2API', + description: '

Здравствуйте! Добро пожаловать на платформу AI-сервисов Sub2API.

🎯 Быстрый старт:

  • 🔑 Создайте API Key
  • 📋 Скопируйте ключ в своё приложение
  • 🚀 Начните пользоваться AI-сервисами

Всего 1 минута — начнём →

', + nextBtn: 'Начать 🚀', + prevBtn: 'Пропустить' }, keyManage: { - title: '🔑 API Key Management', - description: '

Manage all your API access keys here.

📌 What is an API Key?
An API key is your credential for accessing AI services, like a key that allows your application to call AI capabilities.

👉 Click to enter key page

' + title: '🔑 Управление API Key', + description: '

Здесь можно управлять всеми вашими API-ключами доступа.

📌 Что такое API Key?
API Key — это ваши учётные данные для доступа к AI-сервисам, как ключ, позволяющий приложению вызывать AI-возможности.

👉 Нажмите, чтобы перейти на страницу ключей

' }, createKey: { - title: '➕ Create New Key', - description: '

Click the button to create your first API key.

💡 Tip: Key is only shown once after creation, make sure to copy and save

👉 Click "Create Key"

' + title: '➕ Новый ключ', + description: '

Нажмите кнопку, чтобы создать первый API Key.

💡 Совет: ключ показывается только один раз после создания, обязательно скопируйте и сохраните его

👉 Нажмите "Создать ключ"

' }, keyName: { - title: '✏️ Key Name', - description: '

Give your key an easy-to-identify name.

💡 Examples: "My First Key", "For Testing", etc.

', + title: '✏️ Название ключа', + description: '

Дайте ключу понятное название.

💡 Примеры: "My First Key", "For Testing" и т. д.

', nextBtn: 'Далее' }, keyGroup: { - title: '🎯 Select Group', - description: '

Select the service group assigned by the administrator.

📌 Group Info:
Different groups may have different service quality and billing rates, choose according to your needs.

', + title: '🎯 Выбор группы', + description: '

Выберите сервисную группу, назначенную администратором.

📌 Информация о группе:
У разных групп могут быть разное качество сервиса и тарифы, выбирайте по своим потребностям.

', nextBtn: 'Далее' }, keySubmit: { - title: '🎉 Complete Creation', - description: '

Click to confirm and create your API key.

⚠️ Important:
  • Copy the key (sk-xxx) immediately after creation
  • Key is only shown once, need to regenerate if lost

🚀 How to Use:
Configure the key in any OpenAI-compatible client (like ChatBox, OpenCat, etc.) and start using!

👉 Click "Создать" button

' + title: '🎉 Завершить создание', + description: '

Нажмите, чтобы подтвердить и создать API Key.

⚠️ Важно:
  • Скопируйте ключ (sk-xxx) сразу после создания
  • Ключ показывается только один раз, при потере потребуется создать заново

🚀 Как использовать:
Настройте ключ в любом OpenAI-compatible клиенте (например ChatBox, OpenCat и т. д.) и начинайте пользоваться!

👉 Нажмите кнопку "Создать"

' } } }, @@ -6787,49 +6787,49 @@ export default { airwallexLoadFailed: 'Не удалось загрузить компонент Airwallex. Обновите страницу и попробуйте снова.', airwallexMissingParams: 'Отсутствуют параметры платежа Airwallex', errors: { - tooManyPending: 'Too many pending orders (max {max}). Please complete or cancel existing orders first.', - cancelRateLimited: 'Too many cancellations. Please try again later.', - wechatH5NotAuthorized: 'This merchant has not enabled WeChat H5 payment. Open this page in WeChat to continue.', - wechatPaymentMpNotConfigured: 'This site has not completed WeChat MP/JSAPI payment setup, so in-app WeChat payment is unavailable right now.', - wechatJsapiUnavailable: 'WeChat payment could not be invoked in the current environment. Reopen this page inside WeChat and try again.', - wechatJsapiFailed: 'WeChat payment did not complete. Try invoking it again or switch to QR payment.', - wechatUnavailable: 'WeChat payment is temporarily unavailable. Please try again later.', - wechatOpenInWeChatHint: 'Open the current page inside WeChat, or switch to desktop WeChat QR payment.', - wechatScanOnDesktopHint: 'On desktop, use WeChat Scan to pay; on mobile, reopen the current page inside WeChat.', - wechatSwitchBrowserHint: 'Switch to desktop WeChat QR payment, or reopen this page in an external browser and retry.', - mobilePaymentFallbackToQr: 'This merchant has not enabled mobile payment. The flow has been switched to QR payment automatically.', - alipayDesktopUnavailable: 'The desktop Alipay flow could not generate a QR code.', - alipayDesktopQrHint: 'Desktop Alipay should render a QR code. Refresh and retry, or make sure the payment page was not blocked.', - alipayMobileUnavailable: 'This page could not hand off to Alipay.', - alipayMobileOpenHint: 'Allow the current page to open the Alipay app, or retry from the system browser.', + tooManyPending: 'Слишком много ожидающих заказов (макс. {max}). Сначала завершите или отмените существующие заказы.', + cancelRateLimited: 'Слишком много отмен. Повторите попытку позже.', + wechatH5NotAuthorized: 'Этот merchant не включил WeChat H5 payment. Откройте эту страницу в WeChat, чтобы продолжить.', + wechatPaymentMpNotConfigured: 'На этом сайте настройка оплаты WeChat MP/JSAPI ещё не завершена, поэтому оплата внутри WeChat сейчас недоступна.', + wechatJsapiUnavailable: 'Не удалось вызвать WeChat Pay в текущей среде. Откройте эту страницу внутри WeChat и повторите попытку.', + wechatJsapiFailed: 'Оплата WeChat Pay не завершена. Попробуйте вызвать её снова или переключитесь на оплату по QR.', + wechatUnavailable: 'WeChat Pay временно недоступен. Повторите попытку позже.', + wechatOpenInWeChatHint: 'Откройте текущую страницу внутри WeChat или переключитесь на оплату WeChat по QR на компьютере.', + wechatScanOnDesktopHint: 'На компьютере используйте сканирование WeChat для оплаты; на мобильном откройте текущую страницу внутри WeChat.', + wechatSwitchBrowserHint: 'Переключитесь на оплату WeChat по QR на компьютере или откройте страницу во внешнем браузере и повторите попытку.', + mobilePaymentFallbackToQr: 'Этот merchant не включил мобильную оплату. Поток автоматически переключён на оплату по QR.', + alipayDesktopUnavailable: 'Desktop-потоку Alipay не удалось сгенерировать QR-код.', + alipayDesktopQrHint: 'Desktop Alipay должен показать QR-код. Обновите страницу и повторите попытку либо убедитесь, что страница оплаты не заблокирована.', + alipayMobileUnavailable: 'Эта страница не смогла передать оплату в Alipay.', + alipayMobileOpenHint: 'Разрешите текущей странице открыть приложение Alipay или повторите попытку из системного браузера.', // Structured error codes (reason strings from backend ApplicationError) - PAYMENT_DISABLED: 'Payment system is disabled.', - USER_INACTIVE: 'Your account is disabled.', - BALANCE_PAYMENT_DISABLED: 'Balance recharge has been disabled.', - INVALID_AMOUNT: 'Invalid amount.', - INVALID_INPUT: 'Invalid request.', - PLAN_NOT_AVAILABLE: 'Plan not found or no longer available.', - GROUP_NOT_FOUND: 'Subscription group is no longer available.', - GROUP_TYPE_MISMATCH: 'Group is not a subscription type.', - TOO_MANY_PENDING: 'Too many pending orders (max {max}). Please complete or cancel existing orders first.', - DAILY_LIMIT_EXCEEDED: 'Daily recharge limit reached. Remaining: {remaining}.', - PAYMENT_GATEWAY_ERROR: 'Payment method is unavailable.', - NO_AVAILABLE_INSTANCE: 'No payment channel available right now.', - PAYMENT_PROVIDER_MISCONFIGURED: 'Payment provider misconfigured. Please contact an administrator.', - WXPAY_CONFIG_MISSING_KEY: 'WeChat Pay config missing required key: {key}.', - WXPAY_CONFIG_INVALID_KEY_LENGTH: 'WeChat Pay {key} length is invalid (expected {expected} bytes, got {actual}).', - WXPAY_CONFIG_INVALID_KEY: 'WeChat Pay {key} is malformed. Make sure you copied the full PEM content.', - PENDING_ORDERS: 'This provider has pending orders. Please wait for them to complete before making changes.', - PAYMENT_PROVIDER_CONFLICT: 'Another enabled provider instance is already serving this payment method. Disable it before continuing.', - CANCEL_RATE_LIMITED: 'Too many cancellations. Please try again later.', - NOT_FOUND: 'Order not found.', - FORBIDDEN: 'No permission for this order.', - CONFLICT: 'Order status has changed. Please refresh.', - INVALID_ORDER_TYPE: 'Only balance orders can request a refund.', - INVALID_STATUS: 'The current order status does not allow this operation.', - BALANCE_NOT_ENOUGH: 'Refund amount exceeds balance.', - REFUND_AMOUNT_EXCEEDED: 'Refund amount exceeds the recharge amount.', - REFUND_FAILED: 'Refund failed.', + PAYMENT_DISABLED: 'Система оплаты отключена.', + USER_INACTIVE: 'Ваш аккаунт отключён.', + BALANCE_PAYMENT_DISABLED: 'Пополнение баланса отключено.', + INVALID_AMOUNT: 'Некорректная сумма.', + INVALID_INPUT: 'Некорректный запрос.', + PLAN_NOT_AVAILABLE: 'Тариф не найден или больше недоступен.', + GROUP_NOT_FOUND: 'Группа подписки больше недоступна.', + GROUP_TYPE_MISMATCH: 'Группа не является типом подписки.', + TOO_MANY_PENDING: 'Слишком много ожидающих заказов (макс. {max}). Сначала завершите или отмените существующие заказы.', + DAILY_LIMIT_EXCEEDED: 'Достигнут дневной лимит пополнения. Осталось: {remaining}.', + PAYMENT_GATEWAY_ERROR: 'Способ оплаты недоступен.', + NO_AVAILABLE_INSTANCE: 'Сейчас нет доступного платёжного канала.', + PAYMENT_PROVIDER_MISCONFIGURED: 'Провайдер оплаты настроен неверно. Обратитесь к администратору.', + WXPAY_CONFIG_MISSING_KEY: 'В конфигурации WeChat Pay отсутствует обязательный ключ: {key}.', + WXPAY_CONFIG_INVALID_KEY_LENGTH: 'Некорректная длина WeChat Pay {key} (ожидалось {expected} байт, получено {actual}).', + WXPAY_CONFIG_INVALID_KEY: 'WeChat Pay {key} имеет неверный формат. Убедитесь, что скопировали полный PEM content.', + PENDING_ORDERS: 'У этого provider есть ожидающие заказы. Дождитесь их завершения перед внесением изменений.', + PAYMENT_PROVIDER_CONFLICT: 'Другой включённый экземпляр provider уже обслуживает этот способ оплаты. Отключите его перед продолжением.', + CANCEL_RATE_LIMITED: 'Слишком много отмен. Повторите попытку позже.', + NOT_FOUND: 'Заказ не найден.', + FORBIDDEN: 'Нет прав для этого заказа.', + CONFLICT: 'Статус заказа изменился. Обновите страницу.', + INVALID_ORDER_TYPE: 'Запросить возврат можно только для заказов пополнения баланса.', + INVALID_STATUS: 'Текущий статус заказа не позволяет выполнить эту операцию.', + BALANCE_NOT_ENOUGH: 'Сумма возврата превышает баланс.', + REFUND_AMOUNT_EXCEEDED: 'Сумма возврата превышает сумму пополнения.', + REFUND_FAILED: 'Возврат не удался.', }, airwallexPay: 'Оплата Airwallex', stripePay: 'Оплатить', @@ -6862,130 +6862,130 @@ export default { perYear: 'год', admin: { tabs: { - overview: 'Overview', + overview: 'Обзор', orders: 'Заказы', channels: 'Каналы', plans: 'Планы', }, - todayRevenue: 'Today Revenue', - totalRevenue: 'Total Revenue', - todayOrders: 'Today Orders', - orderCount: 'Order Count', - avgAmount: 'Average Amount', - revenue: 'Revenue', - dailyRevenue: 'Daily Revenue', - paymentDistribution: 'Payment Distribution', + todayRevenue: 'Выручка сегодня', + totalRevenue: 'Общая выручка', + todayOrders: 'Заказы сегодня', + orderCount: 'Количество заказов', + avgAmount: 'Средняя сумма', + revenue: 'Выручка', + dailyRevenue: 'Дневная выручка', + paymentDistribution: 'Распределение оплат', colUser: 'Пользователь', - topUsers: 'Top Users', + topUsers: 'Топ пользователей', noData: 'Нет данных', days: 'дней', - weeks: 'weeks', + weeks: 'недель', months: 'месяцев', - searchOrders: 'Search orders...', - allStatuses: 'All Statuses', - allPaymentTypes: 'All Payment Types', - allOrderTypes: 'All Order Types', - orderDetail: 'Order Detail', + searchOrders: 'Поиск заказов...', + allStatuses: 'Все статусы', + allPaymentTypes: 'Все типы оплаты', + allOrderTypes: 'Все типы заказов', + orderDetail: 'Детали заказа', orderType: 'Тип заказа', orders: 'Заказы', - balanceOrder: 'Balance Top-Up', - subscriptionOrder: 'Subscription', - paidAt: 'Paid At', - completedAt: 'Completed At', + balanceOrder: 'Пополнение баланса', + subscriptionOrder: 'Подписка', + paidAt: 'Оплачен', + completedAt: 'Завершён', expiresAt: 'Истекает', - feeRate: 'Fee Rate', - refund: 'Refund', - refundOrder: 'Refund Order', - refundAmount: 'Refund Amount', - maxRefundable: 'Max Refundable', + feeRate: 'Ставка комиссии', + refund: 'Возврат', + refundOrder: 'Возврат заказа', + refundAmount: 'Сумма возврата', + maxRefundable: 'Максимум к возврату', refundReason: 'Причина возврата', - refundReasonPlaceholder: 'Please enter refund reason', - confirmRefund: 'Confirm Refund', - refundSuccess: 'Refund successful', - refundInfo: 'Refund Info', - refundEnabled: 'Refund Enabled', - allowUserRefund: 'Allow User Refund', - alreadyRefunded: 'Already Refunded', - deductBalance: 'Deduct Balance', - deductBalanceHint: 'Subtract recharged amount from user balance', - userBalance: 'User Balance', - orderAmount: 'Order Amount', - insufficientBalance: 'Insufficient balance — will deduct to $0', - noDeduction: 'Will NOT deduct user balance', - forceRefund: 'Force refund (ignore balance check)', + refundReasonPlaceholder: 'Введите причину возврата', + confirmRefund: 'Подтвердить возврат', + refundSuccess: 'Возврат выполнен', + refundInfo: 'Информация о возврате', + refundEnabled: 'Возвраты включены', + allowUserRefund: 'Разрешить возврат пользователю', + alreadyRefunded: 'Уже возвращено', + deductBalance: 'Списать с баланса', + deductBalanceHint: 'Списать сумму пополнения с баланса пользователя', + userBalance: 'Баланс пользователя', + orderAmount: 'Сумма заказа', + insufficientBalance: 'Недостаточно баланса — будет списано до $0', + noDeduction: 'Баланс пользователя НЕ будет списан', + forceRefund: 'Принудительный возврат (игнорировать проверку баланса)', orderCancelled: 'Заказ отменён', retry: 'Повторить', - retrySuccess: 'Retry successful', - approveRefund: 'Approve Refund', - retryRefund: 'Retry Refund', - refundRequestInfo: 'Refund Request Info', - refundRequestedAt: 'Requested At', - refundRequestedBy: 'Requested By', - refundRequestReason: 'Request Reason', - auditLogs: 'Audit Logs', - operator: 'Operator', - channelName: 'Channel Name', - channelDescription: 'Channel Description', - createChannel: 'Create Channel', - editChannel: 'Edit Channel', - deleteChannel: 'Delete Channel', - deleteChannelConfirm: 'Are you sure you want to delete this channel?', - planName: 'Plan Name', - planDescription: 'Plan Description', - createPlan: 'Create Plan', - editPlan: 'Edit Plan', - deletePlan: 'Delete Plan', - deletePlanConfirm: 'Are you sure you want to delete this plan?', - originalPrice: 'Original Price', - price: 'Price', - validityDays: 'Validity (days)', - validityUnit: 'Validity Unit', - sortOrder: 'Sort Order', - forSale: 'For Sale', - onSale: 'On Sale', - offSale: 'Off Sale', + retrySuccess: 'Повтор успешно выполнен', + approveRefund: 'Одобрить возврат', + retryRefund: 'Повторить возврат', + refundRequestInfo: 'Информация о запросе возврата', + refundRequestedAt: 'Запрошен', + refundRequestedBy: 'Кем запрошен', + refundRequestReason: 'Причина запроса', + auditLogs: 'Журнал аудита', + operator: 'Оператор', + channelName: 'Название канала', + channelDescription: 'Описание канала', + createChannel: 'Создать канал', + editChannel: 'Редактировать канал', + deleteChannel: 'Удалить канал', + deleteChannelConfirm: 'Удалить этот канал?', + planName: 'Название тарифа', + planDescription: 'Описание тарифа', + createPlan: 'Создать тариф', + editPlan: 'Редактировать тариф', + deletePlan: 'Удалить тариф', + deletePlanConfirm: 'Удалить этот тариф?', + originalPrice: 'Исходная цена', + price: 'Цена', + validityDays: 'Срок действия (дни)', + validityUnit: 'Единица срока действия', + sortOrder: 'Порядок сортировки', + forSale: 'В продаже', + onSale: 'Продаётся', + offSale: 'Снято с продажи', group: 'Группа', - groupId: 'Group ID', + groupId: 'ID группы', features: 'Возможности', - featuresHint: 'One feature per line', - featuresPlaceholder: 'Enter plan features...', - providerManagement: 'Provider Management', - providerManagementDesc: 'Manage payment provider instances', - createProvider: 'Create Provider', - editProvider: 'Edit Provider', - deleteProvider: 'Delete Provider', - deleteProviderConfirm: 'Are you sure you want to delete this provider?', - providerName: 'Provider Name', - providerKey: 'Provider Key', - selectProviderKey: 'Select Provider Key', - providerConfig: 'Provider Config', - noProviders: 'No providers configured', - noProvidersHint: 'Create a provider instance to start accepting payments', - supportedTypes: 'Supported Payment Types', - supportedTypesHint: 'Select the payment types this provider supports', - rateMultiplier: 'Rate Multiplier', + featuresHint: 'Одна возможность на строку', + featuresPlaceholder: 'Введите возможности тарифа...', + providerManagement: 'Управление провайдерами', + providerManagementDesc: 'Управляйте экземплярами провайдеров оплаты', + createProvider: 'Создать провайдера', + editProvider: 'Редактировать провайдера', + deleteProvider: 'Удалить провайдера', + deleteProviderConfirm: 'Удалить этого провайдера?', + providerName: 'Название provider', + providerKey: 'Ключ provider', + selectProviderKey: 'Выберите ключ provider', + providerConfig: 'Конфигурация provider', + noProviders: 'Провайдеры не настроены', + noProvidersHint: 'Создайте экземпляр provider, чтобы начать принимать оплаты', + supportedTypes: 'Поддерживаемые типы оплаты', + supportedTypesHint: 'Выберите типы оплаты, которые поддерживает этот provider', + rateMultiplier: 'Тарифный коэффициент', dashboardTitle: 'Платежная панель', - dashboardDesc: 'Recharge order analytics and insights', - daySuffix: 'd', + dashboardDesc: 'Аналитика и сводка заказов пополнения', + daySuffix: 'д', paymentConfigTitle: 'Настройки платежей', - paymentConfigDesc: 'Configure payment providers and settings', - plansPageTitle: 'Subscription Plans', - plansPageDesc: 'Manage subscription plan configuration', - tabPlanConfig: 'Plan Configuration', - tabUserSubs: 'User Subscriptions', + paymentConfigDesc: 'Настройка провайдеров оплаты и параметров', + plansPageTitle: 'Тарифы подписки', + plansPageDesc: 'Управление конфигурацией тарифов подписки', + tabPlanConfig: 'Конфигурация тарифа', + tabUserSubs: 'Подписки пользователей', selectGroup: 'Выберите группу', - groupRequired: 'Please select a subscription group', - priceRequired: 'Price must be greater than 0', - validityDaysRequired: 'Validity days must be greater than 0', - groupMissing: 'Missing', - groupInfo: 'Group Info', + groupRequired: 'Выберите группу подписки', + priceRequired: 'Цена должна быть больше 0', + validityDaysRequired: 'Срок действия в днях должен быть больше 0', + groupMissing: 'Отсутствует', + groupInfo: 'Информация о группе', platform: 'Платформа', rateMultiplierLabel: 'Тариф', dailyLimit: 'Дневной лимит', weeklyLimit: 'Недельный лимит', monthlyLimit: 'Месячный лимит', unlimited: 'Безлимитно', - searchUserSubs: 'Search user subscriptions...', + searchUserSubs: 'Поиск подписок пользователей...', daily: 'D', weekly: 'W', monthly: 'M', From 23da62c8e8c8c9e1604d4dc251be2067a2df5b35 Mon Sep 17 00:00:00 2001 From: imdbcooper Date: Fri, 29 May 2026 12:06:36 +0300 Subject: [PATCH 4/4] Update Docker CI workflow to build and publish image --- .github/workflows/docker-image.yml | 35 ++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/docker-image.yml diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 00000000000..f9eb0bff696 --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,35 @@ +name: Build and publish Docker image + +on: + workflow_dispatch: + push: + branches: + - main + +permissions: + contents: read + packages: write + +jobs: + docker: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set image name + run: echo "IMAGE_NAME=ghcr.io/${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV + + - name: Build Docker image + run: docker build -t $IMAGE_NAME:latest . + + - name: Push Docker image + run: docker push $IMAGE_NAME:latest