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 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..6cec530b071 --- /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: 'Создание кэша', + cacheReadTokens: 'Чтение кэша', + cacheWriteTokens: 'Запись кэша', + totalTokens: 'Всего токенов', + cost: 'Стоимость', + // Status + quotaMode: 'Режим квоты ключа', + walletBalance: 'Баланс кошелька', + // Ring card titles + totalQuota: 'Общая квота', + limit5h: 'Лимит на 5 часов', + limitDaily: 'Дневной лимит', + limit7d: 'Лимит на 7 дней', + limitWeekly: 'Недельный лимит', + limitMonthly: 'Месячный лимит', + // Detail rows + remainingQuota: 'Остаток квоты', + expiresAt: 'Истекает', + todayExpires: '(истекает сегодня)', + daysLeft: '({days} дн.)', + usedQuota: 'Использовано квоты', + resetNow: 'Скоро сброс', + subscriptionType: 'Тип подписки', + subscriptionExpires: 'Подписка истекает', + // Usage stat cells + todayRequests: 'Запросов сегодня', + todayInputTokens: 'Вход сегодня', + todayOutputTokens: 'Выход сегодня', + todayTokens: 'Токенов сегодня', + todayCacheCreation: 'Создание кэша сегодня', + todayCacheRead: 'Чтение кэша сегодня', + todayCost: 'Расход сегодня', + rpmTpm: 'RPM / TPM', + totalRequests: 'Всего запросов', + totalInputTokens: 'Всего входных', + totalOutputTokens: 'Всего выходных', + totalTokensLabel: 'Всего токенов', + totalCacheCreation: 'Всего созданий кэша', + totalCacheRead: 'Всего чтений кэша', + totalCost: 'Общая стоимость', + avgDuration: 'Средняя длительность', + // Messages + enterApiKey: 'Введите API-ключ', + querySuccess: 'Запрос выполнен', + queryFailed: 'Запрос не удался', + queryFailedRetry: 'Не удалось выполнить запрос. Попробуйте позже.', + noDailyUsage: 'Нет данных за дни', + }, + + // Setup Wizard + setup: { + title: 'Настройка Sub2API', + description: 'Настройте ваш экземпляр Sub2API', + database: { + title: 'Настройка базы данных', + description: 'Подключение к базе PostgreSQL', + host: 'Хост', + port: 'Порт', + username: 'Имя пользователя', + password: 'Пароль', + databaseName: 'Имя базы данных', + sslMode: 'Режим SSL', + passwordPlaceholder: 'Пароль', + ssl: { + disable: 'Отключить', + require: 'Требовать', + verifyCa: 'Проверять CA', + verifyFull: 'Полная проверка' + } + }, + redis: { + title: 'Настройка Redis', + description: 'Подключение к серверу Redis', + host: 'Хост', + port: 'Порт', + password: 'Пароль (необязательно)', + database: 'База данных', + passwordPlaceholder: 'Пароль', + enableTls: 'Включить TLS', + enableTlsHint: 'Использовать TLS при подключении к Redis (публичные CA-сертификаты)' + }, + admin: { + title: 'Аккаунт администратора', + description: 'Создайте аккаунт администратора', + email: 'Email', + password: 'Пароль', + confirmPassword: 'Подтвердите пароль', + passwordPlaceholder: 'Минимум 8 символов', + confirmPasswordPlaceholder: 'Подтвердите пароль', + passwordMismatch: 'Пароли не совпадают' + }, + ready: { + title: 'Готово к установке', + description: 'Проверьте настройки и завершите установку', + database: 'База данных', + redis: 'Redis', + adminEmail: 'Email администратора' + }, + status: { + testing: 'Проверка...', + success: 'Подключение успешно', + testConnection: 'Проверить подключение', + installing: 'Установка...', + completeInstallation: 'Завершить установку', + completed: 'Установка завершена!', + redirecting: 'Переход на страницу входа...', + restarting: 'Сервис перезапускается, подождите...', + timeout: 'Перезапуск занимает больше времени, чем ожидалось. Обновите страницу вручную.' + } + }, + + // 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: 'мин', + 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: 'Операции', + 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: 'Этот домен email не разрешён для регистрации.', + emailSuffixNotAllowedWithAllowed: + 'Этот домен email не разрешён. Разрешённые домены: {suffixes}', + emailSuffixAllowedMore: 'и ещё {count}', + loginSuccess: 'Вход выполнен. С возвращением!', + accountCreatedSuccess: 'Аккаунт создан. Добро пожаловать в {siteName}!', + reloginRequired: 'Сессия истекла. Войдите снова.', + turnstileExpired: 'Проверка истекла, попробуйте снова', + turnstileFailed: 'Проверка не удалась, попробуйте снова', + completeVerification: 'Завершите проверку', + verifyYourEmail: 'Подтвердите email', + sessionExpired: 'Сессия истекла', + sessionExpiredDesc: 'Вернитесь на страницу регистрации и начните заново.', + verificationCode: 'Код подтверждения', + verificationCodeHint: 'Введите 6-значный код из письма', + sendingCode: 'Отправка...', + sendCode: 'Отправить код', + clickToResend: 'Нажмите, чтобы отправить код повторно', + resendCode: 'Отправить код повторно', + sendCodeDesc: "Мы отправим код подтверждения на", + codeSentSuccess: 'Код отправлен. Проверьте почту.', + verifying: 'Проверка...', + verifyAndCreate: 'Подтвердить и создать аккаунт', + resendCountdown: 'Повторная отправка через {countdown}с', + backToRegistration: 'Назад к регистрации', + sendCodeFailed: 'Не удалось отправить код подтверждения. Попробуйте снова.', + verifyFailed: 'Проверка не удалась. Попробуйте снова.', + codeRequired: 'Введите код подтверждения', + invalidCode: 'Введите корректный 6-значный код', + promoCodeLabel: 'Промокод', + promoCodePlaceholder: 'Введите промокод (необязательно)', + promoCodeValid: 'Промокод принят! Вы получите бонус ${amount}', + promoCodeInvalid: 'Недействительный промокод', + promoCodeNotFound: 'Промокод не найден', + promoCodeExpired: 'Срок действия промокода истёк', + promoCodeDisabled: 'Промокод отключён', + promoCodeMaxUsed: 'Лимит использования промокода исчерпан', + promoCodeAlreadyUsed: 'Вы уже использовали этот промокод', + promoCodeValidating: 'Проверка промокода, подождите', + promoCodeInvalidCannotRegister: 'Недействительный промокод. Проверьте его или очистите поле.', + invitationCodeLabel: 'Код приглашения', + invitationCodePlaceholder: 'Введите код приглашения', + invitationCodeRequired: 'Код приглашения обязателен', + invitationCodeValid: 'Код приглашения действителен', + invitationCodeInvalid: 'Недействительный или использованный код приглашения', + invitationCodeValidating: 'Проверка кода приглашения...', + invitationCodeInvalidCannotRegister: 'Недействительный код приглашения. Проверьте и попробуйте снова.', + oauthOrContinue: 'или продолжить другим способом', + linuxdo: { + signIn: 'Продолжить через Linux.do', + orContinue: 'или продолжить через email', + callbackTitle: 'Выполняется вход', + callbackProcessing: 'Завершаем вход, подождите...', + callbackHint: 'Если перенаправление не произошло, вернитесь на страницу входа и попробуйте снова.', + callbackMissingToken: 'Отсутствует токен входа, попробуйте снова.', + backToLogin: 'Назад ко входу', + invitationRequired: 'Этот аккаунт Linux.do ещё не зарегистрирован. Для регистрации нужен код приглашения.', + invalidPendingToken: 'Токен регистрации истёк. Войдите через Linux.do снова.', + completeRegistration: 'Завершить регистрацию', + completing: 'Завершение регистрации…', + completeRegistrationFailed: 'Регистрация не удалась. Проверьте код приглашения и попробуйте снова.' + }, + dingtalk: { + signIn: 'Продолжить через DingTalk', + callbackTitle: 'Вход через DingTalk', + callbackProcessing: 'Завершаем вход через DingTalk, подождите...', + callbackHint: 'Если перенаправление не произошло, вернитесь на страницу входа и попробуйте снова.', + callbackMissingToken: 'Отсутствует токен входа, попробуйте снова.', + backToLogin: 'Назад ко входу', + invitationRequired: 'Этот аккаунт DingTalk ещё не зарегистрирован. Для регистрации нужен код приглашения.', + invalidPendingToken: 'Токен регистрации истёк. Войдите через DingTalk снова.', + completeRegistration: 'Завершить регистрацию', + completing: 'Завершение регистрации…', + completeRegistrationFailed: 'Регистрация не удалась. Проверьте код приглашения и попробуйте снова.', + createAccountTitle: 'Создать аккаунт DingTalk', + registrationDisabledRedirectToBind: 'Регистрация новых аккаунтов отключена. Привяжите существующий аккаунт через email и пароль.', + error: { + 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: 'Продолжить через {providerName}' + }, + oidc: { + signIn: 'Продолжить через {providerName}', + callbackTitle: 'Вход через {providerName}', + callbackProcessing: 'Завершаем вход через {providerName}, подождите...', + callbackHint: 'Если перенаправление не произошло, вернитесь на страницу входа и попробуйте снова.', + callbackMissingToken: 'Отсутствует токен входа, попробуйте снова.', + backToLogin: 'Назад ко входу', + invitationRequired: + 'Этот аккаунт {providerName} ещё не зарегистрирован. Для регистрации нужен код приглашения.', + invalidPendingToken: 'Токен регистрации истёк. Войдите снова.', + completeRegistration: 'Завершить регистрацию', + completing: 'Завершение регистрации…', + completeRegistrationFailed: 'Регистрация не удалась. Проверьте код приглашения и попробуйте снова.' + }, + oauthFlow: { + 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: 'Callback входа LinuxDo', + dingtalkCallbackPageTitle: 'Callback входа DingTalk', + oidcCallbackPageTitle: 'Callback входа OIDC', + oauthCallbackPageTitle: 'OAuth callback', + wechatProviderName: 'WeChat', + wechatCallbackPageTitle: 'Callback входа WeChat', + wechatPaymentCallbackPageTitle: 'Callback платежа WeChat', + wechatPayment: { + callbackTitle: 'Возобновление платежа WeChat', + callbackProcessing: 'Возобновление платежа WeChat...', + backToPayment: 'Назад к оплате', + callbackMissingResumeToken: 'В callback платежа WeChat отсутствует токен возобновления.' + }, + oauth: { + callbackTitle: 'OAuth callback', + callbackHint: 'При необходимости скопируйте code и state обратно в admin authorization flow.', + invalidCallbackTitle: 'Недействительный callback входа', + invalidCallbackHint: 'Эта страница не содержит корректного результата авторизации. Вернитесь на страницу входа и начните быстрый вход заново.', + code: 'Код', + state: 'Состояние', + fullUrl: 'Полный URL' + }, + // Forgot password + forgotPassword: 'Забыли пароль?', + forgotPasswordTitle: 'Сброс пароля', + forgotPasswordHint: 'Введите email, и мы отправим ссылку для сброса пароля.', + sendResetLink: 'Отправить ссылку', + sendingResetLink: 'Отправка...', + sendResetLinkFailed: 'Не удалось отправить ссылку. Попробуйте снова.', + resetEmailSent: 'Ссылка отправлена', + resetEmailSentHint: 'Если аккаунт с таким email существует, ссылка для сброса скоро придёт на почту. Проверьте входящие и спам.', + backToLogin: 'Назад ко входу', + rememberedPassword: 'Вспомнили пароль?', + // Reset password + resetPasswordTitle: 'Новый пароль', + resetPasswordHint: 'Введите новый пароль ниже.', + newPassword: 'Новый пароль', + newPasswordPlaceholder: 'Введите новый пароль', + confirmPassword: 'Подтвердите пароль', + confirmPasswordPlaceholder: 'Подтвердите новый пароль', + confirmPasswordRequired: 'Подтвердите пароль', + passwordsDoNotMatch: 'Пароли не совпадают', + resetPassword: 'Сбросить пароль', + resettingPassword: 'Сброс...', + resetPasswordFailed: 'Не удалось сбросить пароль. Попробуйте снова.', + passwordResetSuccess: 'Пароль сброшен', + passwordResetSuccessHint: 'Пароль сброшен. Теперь можно войти с новым паролем.', + invalidResetLink: 'Недействительная ссылка', + invalidResetLinkHint: 'Ссылка для сброса недействительна или истекла. Запросите новую.', + requestNewResetLink: 'Запросить новую ссылку', + invalidOrExpiredToken: 'Ссылка для сброса недействительна или истекла. Запросите новую.' + }, + + // Dashboard + dashboard: { + title: 'Панель', + welcomeMessage: "С возвращением! Вот обзор вашего аккаунта.", + balance: 'Баланс', + apiKeys: 'API-ключи', + todayRequests: 'Запросов сегодня', + todayCost: 'Расход сегодня', + todayTokens: 'Токенов сегодня', + totalTokens: 'Всего токенов', + cacheToday: 'Кэш сегодня', + performance: 'Производительность', + avgResponse: 'Средний ответ', + averageTime: 'Среднее время', + timeRange: 'Период', + granularity: 'Детализация', + day: 'День', + hour: 'Час', + modelDistribution: 'Распределение моделей', + groupDistribution: 'Распределение расхода по группам', + platformBreakdown: 'Разбивка по платформам', + platformBreakdownEmpty: 'Расхода по платформам пока нет', + platformCount: 'Платформ: {count}', + platformOther: 'Другое', + platformQuota: { + title: 'Использование квоты', + daily: 'Дневная', + weekly: 'Недельная', + monthly: 'Месячный (скользящие 30 дней)', + resetsAt: 'Сброс {time}', + noLimit: 'безлимитно', + disabled: 'Отключено', + }, + tokenUsageTrend: 'Динамика расхода токенов', + noDataAvailable: 'Нет данных', + model: 'Модель', + group: 'Группа', + noGroup: 'Без группы', + requests: 'Запросы', + tokens: 'Токены', + actual: 'Факт', + standard: 'Стандарт', + input: 'Вход', + output: 'Выход', + cache: 'Кэш', + recentUsage: 'Последний расход', + last7Days: 'Последние 7 дней', + noUsageRecords: 'Нет записей расхода', + startUsingApi: 'Начните использовать API, чтобы увидеть историю расхода.', + viewAllUsage: 'Весь расход', + quickActions: 'Быстрые действия', + createApiKey: 'Создать API-ключ', + generateNewKey: 'Создать новый API-ключ', + viewUsage: 'Посмотреть расход', + checkDetailedLogs: 'Открыть подробные логи расхода', + redeemCode: 'Активировать код', + addBalanceWithCode: 'Пополнить баланс кодом' + }, + + // Groups (shared) + groups: { + subscription: 'Подписка' + }, + + // 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: "Удалить '{name}'? Это действие нельзя отменить.", + apiKey: 'API-ключ', + group: 'Группа', + noGroup: 'Без группы', + searchGroup: 'Поиск групп...', + noGroupFound: 'Группы не найдены', + created: 'Создан', + copyToClipboard: 'Копировать', + copied: 'Скопировано!', + importToCcSwitch: 'Импорт в CCS', + enable: 'Включить', + disable: 'Отключить', + nameLabel: 'Имя', + namePlaceholder: 'Мой API-ключ', + groupLabel: 'Группа', + selectGroup: 'Выберите группу', + statusLabel: 'Статус', + selectStatus: 'Выберите статус', + saving: 'Сохранение...', + noKeysYet: 'API-ключей пока нет', + createFirstKey: 'Создайте первый API-ключ, чтобы начать работу.', + keyCreatedSuccess: 'API-ключ создан', + keyUpdatedSuccess: 'API-ключ обновлён', + keyDeletedSuccess: 'API-ключ удалён', + keyEnabledSuccess: 'API-ключ включён', + keyDisabledSuccess: 'API-ключ отключён', + failedToLoad: 'Не удалось загрузить API-ключи', + failedToSave: 'Не удалось сохранить API-ключ', + failedToDelete: 'Не удалось удалить API-ключ', + failedToUpdateStatus: 'Не удалось обновить статус API-ключа', + clickToChangeGroup: 'Нажмите, чтобы сменить группу', + groupChangedSuccess: 'Группа изменена', + failedToChangeGroup: 'Не удалось изменить группу', + groupRequired: 'Выберите группу', + usage: 'Расход', + today: 'Сегодня', + total: 'Последние 30 дн.', + quota: 'Квота', + lastUsedAt: 'Последнее использование', + useKey: 'Использовать ключ', + useKeyModal: { + title: 'Использовать API-ключ', + description: + 'Добавьте эти переменные окружения в профиль терминала или выполните в терминале для настройки API-доступа.', + copy: 'Копировать', + copied: 'Скопировано', + note: 'Эти переменные окружения будут активны в текущей сессии терминала. Для постоянной настройки добавьте их в ~/.bashrc, ~/.zshrc или соответствующий файл конфигурации.', + noGroupTitle: 'Сначала назначьте группу', + noGroupDescription: 'Этот API-ключ не назначен группе. Сначала выберите группу в списке ключей.', + openai: { + description: 'Добавьте следующие файлы конфигурации в каталог настроек Codex CLI.', + configTomlHint: 'Убедитесь, что следующий контент находится в начале файла config.toml', + note: 'Убедитесь, что каталог конфигурации существует. Пользователи macOS/Linux могут выполнить mkdir -p ~/.codex, чтобы создать его.', + noteWindows: 'Нажмите Win+R и введите %userprofile%\\.codex, чтобы открыть каталог конфигурации. Создайте его вручную, если он не существует.', + }, + cliTabs: { + claudeCode: 'Claude Code', + geminiCli: 'Gemini CLI', + codexCli: 'Codex CLI', + codexCliWs: 'Codex CLI (WebSocket)', + opencode: 'OpenCode', + }, + antigravity: { + description: 'Настройте API-доступ для группы Antigravity. Выберите способ настройки под ваш клиент.', + claudeCode: 'Claude Code', + geminiCli: 'Gemini CLI', + claudeNote: 'Эти переменные окружения будут активны в текущей сессии терминала. Для постоянной настройки добавьте их в ~/.bashrc, ~/.zshrc или соответствующий файл конфигурации.', + geminiNote: 'Эти переменные окружения будут активны в текущей сессии терминала. Для постоянной настройки добавьте их в ~/.bashrc, ~/.zshrc или соответствующий файл конфигурации.', + }, + gemini: { + description: 'Добавьте следующие переменные окружения в профиль терминала или выполните их в терминале для настройки доступа Gemini CLI.', + modelComment: 'Если у вас есть доступ к Gemini 3, можно использовать: gemini-3-pro-preview', + note: 'Эти переменные окружения будут активны в текущей сессии терминала. Для постоянной настройки добавьте их в ~/.bashrc, ~/.zshrc или соответствующий файл конфигурации.', + }, + opencode: { + title: 'Пример OpenCode', + subtitle: 'opencode.json', + hint: 'Путь конфигурации: ~/.config/opencode/opencode.json (или opencode.jsonc), создайте файл при отсутствии. Используйте провайдеры по умолчанию (openai/anthropic/google) или свой provider_id. API-ключ можно настроить напрямую или через команду /connect. Это пример — при необходимости измените модели и параметры.', + }, + }, + customKeyLabel: 'Свой ключ', + customKeyPlaceholder: 'Введите свой ключ (мин. 16 символов)', + customKeyHint: 'Только буквы, цифры, подчёркивания и дефисы. Минимум 16 символов.', + customKeyTooShort: 'Свой ключ должен быть не короче 16 символов', + customKeyInvalidChars: 'Свой ключ может содержать только буквы, цифры, подчёркивания и дефисы', + customKeyRequired: 'Введите свой ключ', + ipRestriction: 'Ограничение по IP', + ipWhitelist: 'Белый список IP', + ipWhitelistPlaceholder: '192.168.1.100\n10.0.0.0/8', + ipWhitelistHint: 'Один IP или CIDR на строку. Если задано, только эти IP смогут использовать ключ.', + ipBlacklist: 'Чёрный список IP', + ipBlacklistPlaceholder: '1.2.3.4\n5.6.0.0/16', + ipBlacklistHint: 'Один IP или CIDR на строку. Эти IP будут заблокированы для ключа.', + ipRestrictionEnabled: 'Ограничение по IP включено', + ccSwitchNotInstalled: 'CC-Switch не установлен или обработчик протокола не зарегистрирован. Сначала установите CC-Switch или скопируйте API-ключ вручную.', + ccsClientSelect: { + title: 'Выберите клиент', + description: 'Выберите тип клиента для импорта в CC-Switch:', + claudeCode: 'Claude Code', + claudeCodeDesc: 'Импортировать как конфигурацию Claude Code', + geminiCli: 'Gemini CLI', + geminiCliDesc: 'Импортировать как конфигурацию Gemini CLI', + }, + // Quota and expiration + quotaLimit: 'Лимит квоты', + quotaAmount: 'Размер квоты (USD)', + quotaAmountPlaceholder: 'Введите лимит квоты в USD', + quotaAmountHint: 'Максимальная сумма расхода для ключа. 0 = без лимита.', + quotaUsed: 'Квота использована', + reset: 'Сбросить', + resetQuotaUsed: 'Сбросить использованную квоту в 0', + resetQuotaTitle: 'Подтвердите сброс квоты', + resetQuotaConfirmMessage: 'Сбросить использованную квоту (${used}) для ключа "{name}" в 0? Это действие нельзя отменить.', + quotaResetSuccess: 'Квота сброшена', + failedToResetQuota: 'Не удалось сбросить квоту', + rateLimitColumn: 'Лимит частоты', + rateLimitSection: 'Лимит частоты', + resetUsage: 'Сбросить', + rateLimit5h: 'Лимит на 5 часов (USD)', + rateLimit1d: 'Дневной лимит (USD)', + rateLimit7d: 'Лимит на 7 дней (USD)', + rateLimitHint: 'Максимальный расход ключа в каждом периоде. 0 = без лимита.', + rateLimitUsage: 'Использование лимита частоты', + resetRateLimitUsage: 'Сбросить расход лимита частоты', + resetRateLimitTitle: 'Подтвердите сброс лимита частоты', + resetRateLimitConfirmMessage: 'Сбросить расход лимита частоты для ключа "{name}"? Расход во всех окнах будет обнулён. Это действие нельзя отменить.', + rateLimitResetSuccess: 'Расход лимита частоты сброшен', + failedToResetRateLimit: 'Не удалось сбросить расход лимита частоты', + resetNow: 'Скоро сброс', + expiration: 'Срок действия', + expiresInDays: '{days} дн.', + extendDays: '+{days} дн.', + customDate: 'Свой', + expirationDate: 'Дата истечения', + expirationDateHint: 'Выберите срок действия API-ключа.', + currentExpiration: 'Текущий срок действия', + expiresAt: 'Истекает', + noExpiration: 'Никогда', + status: { + active: 'Активен', + inactive: 'Неактивен', + quota_exhausted: 'Квота исчерпана', + expired: 'Истекла', + }, + }, + + // Usage + usage: { + title: 'История расхода', + description: 'Просматривайте и анализируйте историю использования API', + costDetails: 'Разбивка стоимости', + tokenDetails: 'Разбивка токенов', + cacheTtlOverriddenHint: 'Переопределение TTL кэша включено', + cacheTtlOverriddenLabel: 'Переопределение TTL', + cacheTtlOverridden5m: 'Тарифицируется как 5 мин', + cacheTtlOverridden1h: 'Тарифицируется как 1 ч', + totalRequests: 'Всего запросов', + totalTokens: 'Всего токенов', + totalCost: 'Общая стоимость', + standardCost: 'Стандарт', + actualCost: 'Факт', + accountCost: 'Стоимость', + userBilled: 'Списано с пользователя', + accountBilled: 'Списано с аккаунта', + accountMultiplier: 'Тариф аккаунта', + avgDuration: 'Средняя длительность', + inSelectedRange: 'за выбранный период', + perRequest: 'за запрос', + apiKeyFilter: 'API-ключ', + allApiKeys: 'Все API-ключи', + timeRange: 'Период', + exportCsv: 'Экспорт CSV', + exportExcel: 'Экспорт Excel', + exportingProgress: 'Экспорт данных...', + exportedCount: 'Экспортировано {current}/{total} записей', + estimatedTime: 'Осталось примерно: {time}', + cancelExport: 'Отменить экспорт', + exportCancelled: 'Экспорт отменён', + exporting: 'Экспорт...', + preparingExport: 'Подготовка экспорта...', + model: 'Модель', + requestedModel: 'Запрошенная', + upstreamModel: 'Апстрим', + reasoningEffort: 'Уровень рассуждения', + endpoint: 'Endpoint', + endpointDistribution: 'Распределение endpoint-ов', + inbound: 'Входящий', + upstream: 'Апстрим', + mapping: 'Сопоставление', + path: 'Путь', + inboundEndpoint: 'Входящий endpoint', + upstreamEndpoint: 'Апстрим endpoint', + type: 'Тип', + tokens: 'Токены', + cost: 'Стоимость', + firstToken: 'Первый токен', + duration: 'Длительность', + time: 'Время', + ws: 'WS', + stream: 'Stream', + sync: 'Sync', + unknown: 'Неизвестно', + 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: 'Исходная', + billed: 'Списано', + noRecords: 'Записи расхода не найдены. Попробуйте изменить фильтры.', + failedToLoad: 'Не удалось загрузить логи расхода', + noDataToExport: 'Нет данных для экспорта', + exportSuccess: 'Данные расхода экспортированы', + exportFailed: 'Не удалось экспортировать данные расхода', + exportExcelSuccess: 'Данные расхода экспортированы в Excel', + exportExcelFailed: 'Не удалось экспортировать данные расхода', + imageUnit: ' изображений', + userAgent: 'User-Agent' + }, + + // Shared keys for channel monitor (admin + user views) + monitorCommon: { + status: { + operational: 'Работает', + degraded: 'Проблемы', + failed: 'Ошибка', + error: 'Ошибка', + unknown: '-' + }, + providers: { + openai: 'OpenAI', + anthropic: 'Anthropic', + gemini: 'Gemini' + }, + extraModelsHeader: 'Дополнительные модели', + extraModelsEmpty: 'Нет дополнительных моделей', + latencyEmpty: '-', + availabilityPrefix: 'Доступность', + dialogLatency: 'Задержка диалога', + endpointPing: 'Endpoint PING', + history60pts: 'ИСТОРИЯ ({n} ТОЧ.)', + nextUpdateIn: 'ОБНОВЛЕНИЕ ЧЕРЕЗ {n}с', + past: 'ПРОШЛОЕ', + now: 'СЕЙЧАС', + maintenancePaused: 'Обслуживание · таймлайн приостановлен', + extraModelsCount: '+ {n} моделей', + pollEvery: 'Опрос каждые {n}с', + updatedAt: 'Обновлено {time}', + relativeSecondsAgo: '{n} с назад', + relativeMinutesAgo: '{n} мин назад', + relativeHoursAgo: '{n} ч назад', + relativeDaysAgo: '{n} дн назад' + }, + + // Channel Status (user-facing read-only view) + channelStatus: { + title: 'Статус каналов', + description: 'Проверяйте доступность каналов, задержку и последние статусы', + searchPlaceholder: 'Поиск каналов...', + allProviders: 'Все провайдеры', + loadError: 'Не удалось загрузить статус каналов', + detailLoadError: 'Не удалось загрузить детали канала', + detailTitle: 'Детали канала', + closeDetail: 'Закрыть', + windowTab: { + '7d': '7 дней', + '15d': '15 дней', + '30d': '30 дней' + }, + overall: { + operational: 'РАБОТАЕТ', + degraded: 'ПРОБЛЕМЫ', + unavailable: 'НЕДОСТУПЕН' + }, + columns: { + name: 'Имя', + provider: 'Провайдер', + groupName: 'Группа', + primaryModel: 'Основная модель', + availability7d: 'Доступность 7 дн.', + latency: 'Задержка (мс)' + }, + detailColumns: { + model: 'Модель', + latestStatus: 'Последний статус', + latestLatency: 'Последняя задержка (мс)', + availability7d: 'Доступность 7 дн.', + availability15d: 'Доступность 15 дн.', + availability30d: 'Доступность 30 дн.', + avgLatency7d: 'Средняя задержка 7 дн. (мс)' + }, + empty: { + title: 'Нет доступных каналов', + description: 'Отслеживаемые каналы ещё не настроены.' + } + }, + + // Available Channels (user-facing) + availableChannels: { + title: 'Доступные каналы', + description: 'Доступные вам каналы, поддерживаемые модели и цены', + searchPlaceholder: 'Поиск каналов или моделей...', + empty: 'Нет доступных каналов', + noModels: 'Модели не настроены', + noPricing: 'Цены не настроены', + exclusive: 'Эксклюзивная', + public: 'Публичная', + exclusiveTooltip: 'Эксклюзивные группы, выданные администратором', + publicTooltip: 'Группы, открытые всем пользователям', + columns: { + name: 'Канал', + description: 'Описание', + platform: 'Платформа', + groups: 'Ваши доступные группы', + supportedModels: 'Поддерживаемые модели' + }, + pricing: { + billingMode: 'Режим биллинга', + billingModeToken: 'За токен', + billingModePerRequest: 'За запрос', + billingModeImage: 'За изображение', + inputPrice: 'Вход', + outputPrice: 'Выход', + cacheWritePrice: 'Запись кэша', + cacheReadPrice: 'Чтение кэша', + imageOutputPrice: 'Выход изображения', + perRequestPrice: 'За запрос', + intervals: 'Ступенчатые цены', + unitPerMillion: '/ 1 млн токенов', + unitPerRequest: '/ запрос' + } + }, + + affiliate: { + title: 'Партнёрские бонусы', + description: 'Приглашайте пользователей и переводите партнёрские бонусы на баланс', + yourCode: 'Ваш партнёрский код', + inviteLink: 'Ссылка приглашения', + copyCode: 'Скопировать код', + copyLink: 'Скопировать ссылку', + codeCopied: 'Партнёрский код скопирован', + linkCopied: 'Ссылка приглашения скопирована', + loadFailed: 'Не удалось загрузить партнёрские данные', + transferFailed: 'Не удалось перевести партнёрскую квоту', + stats: { + rebateRate: 'Моя ставка бонуса', + rebateRateHint: 'Сколько вы получаете при пополнении приглашённого пользователя', + invitedUsers: 'Приглашённые пользователи', + availableQuota: 'Доступная бонусная квота', + frozenQuota: 'Заморожено', + frozenQuotaHint: 'Недавние бонусы ожидают разблокировки', + totalQuota: 'Бонусная квота за всё время' + }, + transfer: { + title: 'Перевести бонусную квоту', + description: 'Переведите доступную бонусную квоту на баланс аккаунта', + button: 'Перевести на баланс', + transferring: 'Перевод...', + empty: 'Нет доступной бонусной квоты', + success: '{amount} переведено на ваш баланс' + }, + invitees: { + title: 'Приглашённые пользователи', + empty: 'Приглашённых пользователей пока нет', + columns: { + email: 'Email', + username: 'Имя пользователя', + rebate: 'Бонус', + joinedAt: 'Дата регистрации' + } + }, + tips: { + title: 'Как это работает', + line1: 'Поделитесь партнёрским кодом или ссылкой с новыми пользователями.', + line2: 'Когда приглашённые пополняют баланс, вы получаете {rate} от пополнения как бонусную квоту.', + line3: 'Переводите бонусную квоту на баланс в любое время.', + line4: 'Для новых бонусов может быть период ожидания перед переводом.' + } + }, + + // Redeem + redeem: { + title: 'Активировать код', + description: 'Введите код, чтобы пополнить баланс или увеличить параллелизм', + currentBalance: 'Текущий баланс', + concurrency: 'Параллелизм', + requests: 'запросов', + redeemCodeLabel: 'Активировать код', + redeemCodePlaceholder: 'Введите код активации', + redeemCodeHint: 'Коды чувствительны к регистру', + redeeming: 'Активация...', + redeemButton: 'Активировать код', + redeemSuccess: 'Код успешно активирован!', + redeemFailed: 'Активация не удалась', + added: 'Добавлено', + concurrentRequests: 'одновременных запросов', + newBalance: 'Новый баланс', + newConcurrency: 'Новый параллелизм', + aboutCodes: 'О кодах активации', + codeRule1: 'Каждый код можно использовать только один раз', + codeRule2: 'Коды могут пополнять баланс, увеличивать параллелизм или выдавать тестовый доступ', + codeRule3: 'Обратитесь в поддержку, если не получается активировать код', + codeRule4: 'Баланс и параллелизм обновляются сразу', + recentActivity: 'Последняя активность', + historyWillAppear: 'Здесь появится история активаций', + balanceAddedRedeem: 'Баланс пополнен кодом', + balanceAddedAffiliate: 'Баланс пополнен партнёрским переводом', + balanceAddedAdmin: 'Баланс пополнен администратором', + balanceDeductedAdmin: 'Баланс списан администратором', + concurrencyAddedRedeem: 'Параллелизм увеличен кодом', + concurrencyAddedAdmin: 'Параллелизм увеличен администратором', + concurrencyReducedAdmin: 'Параллелизм уменьшен администратором', + adminAdjustment: 'Корректировка администратора', + subscriptionAssigned: 'Подписка назначена', + subscriptionAssignedDesc: 'Вам выдан доступ к {groupName}', + subscriptionDays: '{days} дн.', + days: ' days', + codeRedeemSuccess: 'Код успешно активирован!', + failedToRedeem: 'Не удалось активировать код. Проверьте код и попробуйте снова.', + subscriptionRefreshFailed: 'Код активирован, но не удалось обновить статус подписки.', + pleaseEnterCode: 'Введите код активации' + }, + + // Profile + profile: { + title: 'Настройки профиля', + description: 'Управляйте информацией и настройками аккаунта', + accountBalance: 'Баланс аккаунта', + concurrencyLimit: 'Лимит параллелизма', + rpmLimit: 'Лимит RPM', + rpmUnlimited: 'Безлимитно', + memberSince: 'Дата регистрации', + overviewTitle: 'Обзор аккаунта', + overviewDescription: 'Быстрый обзор статуса аккаунта, источников профиля и основных действий.', + basicsTitle: 'Профиль и аватар', + basicsDescription: 'Поддерживайте данные профиля и аватар актуальными.', + linkedProfileSources: 'Источники профиля', + linkedProfileSourcesDescription: 'Некоторые данные профиля могут синхронизироваться из сторонних способов входа.', + securityTitle: 'Настройки безопасности', + securityDescription: 'Пароль, двухфакторная аутентификация и уведомления находятся справа.', + administrator: 'Администратор', + user: 'Пользователь', + username: 'Имя пользователя', + email: 'Email', + status: 'Статус', + role: 'Роль', + enterUsername: 'Введите имя пользователя', + editProfile: 'Изменить профиль', + updateProfile: 'Обновить профиль', + updating: 'Обновление...', + updateSuccess: 'Профиль обновлён', + updateFailed: 'Не удалось обновить профиль', + usernameRequired: 'Введите имя пользователя', + changePassword: 'Сменить пароль', + currentPassword: 'Текущий пароль', + newPassword: 'Новый пароль', + confirmNewPassword: 'Подтвердите новый пароль', + passwordHint: 'Пароль должен быть не короче 8 символов', + changingPassword: 'Изменение...', + changePasswordButton: 'Сменить пароль', + passwordsNotMatch: 'Новые пароли не совпадают', + passwordTooShort: 'Пароль должен быть не короче 8 символов', + passwordChangeSuccess: 'Пароль изменён', + passwordChangeFailed: 'Не удалось изменить пароль', + // TOTP 2FA + totp: { + title: 'Двухфакторная аутентификация (2FA)', + description: 'Повысьте безопасность аккаунта через Google Authenticator или похожие приложения', + enabled: 'Включено', + enabledAt: 'Включено', + notEnabled: 'Не включено', + notEnabledHint: 'Включите двухфакторную аутентификацию для защиты аккаунта', + enable: 'Включить', + disable: 'Отключить', + 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: 'Сначала подтвердите email', + verifyPasswordFirst: 'Сначала подтвердите личность', + emailCode: 'Код подтверждения email', + enterEmailCode: 'Введите 6-значный код', + sendCode: 'Отправить код', + codeSent: 'Код подтверждения отправлен на email', + sendCodeFailed: 'Не удалось отправить код подтверждения' + }, + balanceNotify: { + title: 'Уведомление о низком балансе', + description: 'Отправлять email, когда баланс опускается ниже порога', + enabled: 'Включить уведомление о низком балансе', + threshold: 'Свой порог', + thresholdHint: 'Оставьте пустым, чтобы использовать системное значение', + thresholdPlaceholder: 'Введите сумму', + 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: 'Аватар профиля', + description: 'Загрузите аватар. Статичные изображения сжимаются до 20 КБ перед сохранением.', + uploadAction: 'Загрузить изображение', + uploadHint: 'Статичные изображения по возможности сжимаются до 20 КБ. GIF должен быть не больше 20 КБ.', + uploadRequired: 'Сначала загрузите изображение', + saveSuccess: 'Аватар обновлён', + deleteSuccess: 'Аватар удалён', + invalidType: 'Выберите файл изображения', + gifTooLarge: 'GIF-аватар должен быть не больше 20 КБ', + compressTooLarge: 'Не удалось сжать изображение до 20 КБ. Попробуйте меньший файл.', + compressFailed: 'Не удалось сжать выбранное изображение.', + readFailed: 'Не удалось прочитать выбранное изображение.', + emptyDeleteHint: 'Аватар уже пустой', + }, + authBindings: { + title: 'Подключённые способы входа', + description: 'Просматривайте текущие привязки и подключайте других провайдеров.', + bindAction: 'Привязать {providerName}', + bindSuccess: 'Аккаунт привязан', + emailPlaceholder: 'Введите email', + codePlaceholder: 'Введите код подтверждения', + passwordPlaceholder: 'Задайте пароль для входа', + replaceEmailPasswordPlaceholder: 'Введите текущий пароль', + sendCodeAction: 'Отправить код', + manageEmailAction: 'Управление email', + hideEmailFormAction: 'Скрыть форму email', + confirmEmailBindAction: 'Привязать email', + confirmEmailReplaceAction: 'Заменить основной email', + codeSentTo: 'Код отправлен на {email}', + replaceSuccess: 'Основной email обновлён', + unbindAction: 'Отвязать', + unbindSuccess: '{providerName} отвязан', + boundCount: 'Привязок: {count}', + status: { + bound: 'Привязан', + notBound: 'Не привязан', + }, + providers: { + email: 'Email', + linuxdo: 'LinuxDo', + dingtalk: 'DingTalk', + oidc: '{providerName}', + wechat: 'WeChat', + }, + notes: { + emailManagedFromProfile: 'Основной email управляется в форме профиля', + canUnbind: 'Этот способ входа можно отвязать', + bindAnotherBeforeUnbind: 'Перед отвязкой привяжите другой способ входа', + }, + source: { + avatar: 'Аватар синхронизируется из {providerName}', + username: 'Имя синхронизируется из {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: 'Панель администратора', + description: 'Обзор системы и статистика в реальном времени', + apiKeys: 'API-ключи', + accounts: 'Аккаунты', + users: 'Пользователи', + todayRequests: 'Запросов сегодня', + newUsersToday: 'Новых пользователей сегодня', + todayTokens: 'Токенов сегодня', + totalTokens: 'Всего токенов', + cacheToday: 'Кэш сегодня', + performance: 'Производительность', + avgResponse: 'Средний ответ', + active: 'активно', + ok: 'ok', + err: 'err', + activeUsers: 'активных пользователей', + create: 'Создать', + timeRange: 'Период', + granularity: 'Детализация', + day: 'День', + hour: 'Час', + modelDistribution: 'Распределение моделей', + groupDistribution: 'Распределение расхода по группам', + metricTokens: 'По токенам', + metricActualCost: 'По фактической стоимости', + tokenUsageTrend: 'Динамика расхода токенов', + userUsageTrend: 'Динамика расхода пользователей (топ-12)', + model: 'Модель', + group: 'Группа', + noGroup: 'Без группы', + requests: 'Запросы', + tokens: 'Токены', + actual: 'Факт', + standard: 'Стандарт', + accountCost: 'Стоимость', + noDataAvailable: 'Нет данных', + recentUsage: 'Последний расход', + viewModelDistribution: 'Распределение моделей', + viewSpendingRanking: 'Рейтинг расходов пользователей', + spendingRankingTitle: 'Рейтинг расходов пользователей', + spendingRankingUser: 'Пользователь', + spendingRankingRequests: 'Запросы', + spendingRankingTokens: 'Токены', + spendingRankingSpend: 'Расход', + spendingRankingOther: 'Остальные', + spendingRankingUsage: 'Расход', + spendShort: 'Расход', + requestsShort: 'Запр.', + tokensShort: 'Ток.', + failedToLoad: 'Не удалось загрузить статистику панели' + }, + + backup: { + title: 'Резервная копия базы данных', + description: 'Полная резервная копия базы в S3-совместимое хранилище с расписанием и восстановлением', + s3: { + title: 'Настройка S3-хранилища', + description: 'Настройте S3-совместимое хранилище (поддерживается Cloudflare R2)', + descriptionPrefix: 'Настройте S3-совместимое хранилище (поддерживается', + descriptionSuffix: ')', + enabled: 'Включить S3-хранилище', + endpoint: 'Endpoint', + region: 'Регион', + bucket: 'Bucket', + prefix: 'Префикс ключей', + accessKeyId: 'Access Key ID', + secretAccessKey: 'Secret Access Key', + secretConfigured: 'Уже настроено, оставьте пустым, чтобы сохранить', + forcePathStyle: 'Принудительный Path Style', + testConnection: 'Проверить подключение', + testSuccess: 'Проверка подключения S3 успешна', + testFailed: 'Проверка подключения S3 не удалась', + saved: 'Настройки S3 сохранены' + }, + schedule: { + title: 'Резервное копирование по расписанию', + description: 'Настройте автоматические резервные копии по расписанию', + enabled: 'Включить резервные копии по расписанию', + cronExpr: 'Cron-выражение', + cronHint: 'например, "0 2 * * *" означает каждый день в 02:00', + retainDays: 'Срок хранения резервных копий', + retainDaysHint: 'Файлы резервных копий удаляются через указанное число дней, 0 = не удалять', + retainCount: 'Максимум копий', + retainCountHint: 'Максимальное число резервных копий, 0 = без лимита', + saved: 'Расписание сохранено' + }, + operations: { + title: 'Записи резервных копий', + description: 'Создавайте резервные копии вручную и управляйте существующими записями', + createBackup: 'Создать резервную копию', + backing: 'Создание резервной копии...', + backupCreated: 'Резервная копия создана', + expireDays: 'Срок хранения', + alreadyInProgress: 'Резервное копирование уже выполняется', + backupRunning: 'Выполняется резервное копирование...', + backupFailed: 'Резервное копирование не удалось', + restoreRunning: 'Выполняется восстановление...', + restoreFailed: 'Восстановление не удалось', + }, + columns: { + status: 'Статус', + fileName: 'Имя файла', + size: 'Размер', + expiresAt: 'Истекает', + triggeredBy: 'Запущено', + startedAt: 'Начато', + actions: 'Действия' + }, + status: { + pending: 'Ожидает', + running: 'Выполняется', + completed: 'Завершено', + failed: 'Ошибка' + }, + progress: { + pending: 'Подготовка', + dumping: 'Дамп базы данных', + uploading: 'Загрузка', + }, + trigger: { + manual: 'Вручную', + scheduled: 'По расписанию' + }, + neverExpire: 'Никогда', + empty: 'Записей резервных копий нет', + actions: { + download: 'Скачать', + restore: 'Восстановить', + restoreConfirm: 'Восстановить из этой резервной копии? Текущая база данных будет перезаписана!', + restorePasswordPrompt: 'Введите пароль администратора для подтверждения восстановления', + restoreSuccess: 'База данных восстановлена', + deleteConfirm: 'Удалить эту резервную копию?', + deleted: 'Резервная копия удалена' + }, + r2Guide: { + title: 'Инструкция по настройке Cloudflare R2', + intro: 'Cloudflare R2 предоставляет S3-совместимое объектное хранилище с бесплатным уровнем 10GB + 1M запросов Class A в месяц — удобно для резервных копий базы.', + step1: { + title: 'Создайте R2 Bucket', + line1: 'Войдите в Cloudflare Dashboard (dash.cloudflare.com) и выберите "R2 Object Storage" в боковом меню', + line2: 'Нажмите "Create bucket", введите имя (например, sub2api-backups) и выберите регион', + line3: 'Нажмите create для завершения' + }, + step2: { + 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: 'Получите S3 Endpoint', + desc: 'Найдите Account ID на странице обзора R2 (в URL или правой панели). Формат endpoint:', + accountId: 'your_account_id' + }, + step4: { + title: 'Заполните конфигурацию', + checkEnabled: 'Отмечено', + bucketValue: 'Имя вашего bucket', + fromStep2: 'Значение из шага 2', + unchecked: 'Не отмечено' + }, + freeTier: 'Бесплатный уровень R2: 10GB хранилища + 1M запросов Class A + 10M запросов Class B в месяц — более чем достаточно для резервных копий базы.' + } + }, + + dataManagement: { + title: 'Управление данными', + description: 'Управляйте статусом агента, объектным хранилищем и задачами резервного копирования в одном месте', + agent: { + title: 'Статус агента управления данными', + description: 'Система проверяет фиксированный Unix-сокет и включает управление данными только при доступности.', + enabled: 'Агент управления данными готов. Операции доступны.', + disabled: 'Агент управления данными недоступен. Сейчас доступна только диагностика.', + socketPath: 'Путь сокета', + version: 'Version', + status: 'Статус', + uptime: 'Время работы', + reasonLabel: 'Причина недоступности', + reason: { + DATA_MANAGEMENT_AGENT_SOCKET_MISSING: 'Файл сокета управления данными отсутствует', + DATA_MANAGEMENT_AGENT_UNAVAILABLE: 'Агент управления данными недоступен', + BACKUP_AGENT_SOCKET_MISSING: 'Файл сокета резервного копирования отсутствует', + BACKUP_AGENT_UNAVAILABLE: 'Агент резервного копирования недоступен', + UNKNOWN: 'Неизвестная причина' + } + }, + sections: { + config: { + title: 'Настройка резервных копий', + description: 'Настройте источник резервных копий, срок хранения и параметры S3.' + }, + s3: { + title: 'Объектное хранилище S3', + description: 'Настройте и проверьте загрузку артефактов резервных копий в S3-совместимое хранилище.' + }, + backup: { + title: 'Операции резервного копирования', + description: 'Запускайте задачи резервного копирования PostgreSQL, Redis и полного бэкапа.' + }, + history: { + title: 'История резервных копий', + description: 'Просматривайте статусы задач, ошибки и метаданные артефактов.' + } + }, + form: { + sourceMode: 'Режим источника', + backupRoot: 'Корень резервных копий', + activePostgresProfile: 'Активный профиль PostgreSQL', + activeRedisProfile: 'Активный профиль Redis', + activeS3Profile: 'Активный профиль S3', + retentionDays: 'Дней хранения', + keepLast: 'Хранить последние задачи', + uploadToS3: 'Загружать в S3', + useActivePostgresProfile: 'Использовать активный профиль PostgreSQL', + useActiveRedisProfile: 'Использовать активный профиль Redis', + useActiveS3Profile: 'Использовать активный профиль', + idempotencyKey: 'Ключ идемпотентности (необязательно)', + secretConfigured: 'Уже настроено, оставьте пустым без изменений', + source: { + profileID: 'ID профиля (уникальный)', + profileName: 'Имя профиля', + setActive: 'Сделать активным после создания' + }, + postgres: { + title: 'PostgreSQL', + host: 'Хост', + port: 'Порт', + user: 'Пользователь', + password: 'Пароль', + database: 'База данных', + sslMode: 'Режим SSL', + containerName: 'Имя контейнера (режим docker_exec)' + }, + redis: { + title: 'Redis', + addr: 'Адрес (host:port)', + username: 'Имя пользователя', + password: 'Пароль', + db: 'Индекс базы данных', + containerName: 'Имя контейнера (режим docker_exec)' + }, + s3: { + enabled: 'Включить загрузку в S3', + profileID: 'ID профиля (уникальный)', + profileName: 'Имя профиля', + endpoint: 'Endpoint (необязательно)', + region: 'Регион', + bucket: 'Bucket', + accessKeyID: 'Access Key ID', + secretAccessKey: 'Secret Access Key', + prefix: 'Префикс объектов', + forcePathStyle: 'Принудительный Path Style', + useSSL: 'Использовать SSL', + setActive: 'Сделать активным после создания' + } + }, + sourceProfiles: { + createTitle: 'Создать профиль источника', + editTitle: 'Изменить профиль источника', + empty: 'Профилей источника пока нет, создайте первый', + deleteConfirm: 'Удалить профиль источника {profileID}?', + columns: { + profile: 'Профиль', + active: 'Активен', + connection: 'Подключение', + database: 'База данных', + updatedAt: 'Обновлено', + actions: 'Действия' + } + }, + s3Profiles: { + createTitle: 'Создать профиль S3', + editTitle: 'Изменить профиль S3', + empty: 'Профилей S3 пока нет, создайте первый', + editHint: 'Click "Изменить" to modify profile details in the right drawer.', + deleteConfirm: 'Удалить профиль S3 {profileID}?', + columns: { + profile: 'Профиль', + active: 'Активен', + storage: 'Хранилище', + updatedAt: 'Обновлено', + actions: 'Действия' + } + }, + history: { + total: 'Задач: {count}', + empty: 'Задач резервного копирования пока нет', + columns: { + jobID: 'ID задачи', + type: 'Тип', + status: 'Статус', + triggeredBy: 'Запущено', + pgProfile: 'Профиль PostgreSQL', + redisProfile: 'Профиль Redis', + s3Profile: 'Профиль S3', + finishedAt: 'Завершено', + artifact: 'Артефакт', + error: 'Ошибка' + }, + status: { + queued: 'В очереди', + running: 'Выполняется', + succeeded: 'Успешно', + failed: 'Ошибка', + partial_succeeded: 'Частично успешно' + } + }, + actions: { + 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: 'Просмотр связей пригласивших и приглашённых по всему сайту', + rebatesDescription: 'Просмотр заказов пополнения, по которым начислены affiliate-вознаграждения', + transfersDescription: 'Просмотр переводов affiliate-квоты на баланс аккаунта', + errors: { + loadFailed: 'Не удалось загрузить affiliate-записи' + }, + records: { + search: 'Поиск', + searchPlaceholder: 'Email, имя пользователя, ID пользователя или номер заказа', + startAt: 'Дата начала', + endAt: 'Дата окончания', + inviter: 'Пригласивший', + invitee: 'Приглашённый', + user: 'Пользователь', + affCode: 'Код приглашения', + order: 'Заказ', + totalRebate: 'Всего вознаграждений', + orderAmount: 'Сумма пополнения', + payAmount: 'Оплаченная сумма', + rebateAmount: 'Сумма вознаграждения', + paymentType: 'Способ оплаты', + orderStatus: 'Статус заказа', + transferAmount: 'Сумма перевода', + balanceAfter: 'Баланс после', + availableQuotaAfter: 'Доступно после', + frozenQuotaAfter: 'Заморожено после', + historyQuotaAfter: 'Историческое вознаграждение после', + invitedAt: 'Приглашён', + rebatedAt: 'Начислено', + transferredAt: 'Переведено' + }, + overview: { + title: 'Обзор affiliate-пользователя', + affCode: 'Код приглашения', + rebateRate: 'Ставка вознаграждения', + invitedCount: 'Приглашённые пользователи', + rebatedInviteeCount: 'Приглашённые с вознаграждением', + availableQuota: 'Доступная квота', + historyQuota: 'Историческое вознаграждение' + } + }, + + // Users + users: { + title: 'Управление пользователями', + description: 'Управляйте пользователями и их правами', + createUser: 'Создать пользователя', + editUser: 'Изменить пользователя', + deleteUser: 'Удалить пользователя', + searchUsers: 'Поиск по email, имени, заметкам или API-ключу...', + allRoles: 'Все роли', + allStatus: 'Все статусы', + allGroups: 'Все группы', + searchGroups: 'Поиск групп...', + fuzzySearch: 'Нечёткий поиск', + admin: 'Администратор', + user: 'Пользователь', + disabled: 'Отключено', + email: 'Email', + password: 'Пароль', + username: 'Имя пользователя', + notes: 'Заметки', + enterEmail: 'Введите email', + enterPassword: 'Введите пароль', + enterUsername: 'Введите имя пользователя (необязательно)', + enterNotes: 'Введите заметки (только для админов)', + notesHint: 'Эта заметка видна только администраторам', + enterNewPassword: 'Введите новый пароль (необязательно)', + leaveEmptyToKeep: 'Оставьте пустым, чтобы сохранить текущий пароль', + generatePassword: 'Сгенерировать случайный пароль', + copyPassword: 'Скопировать пароль', + creating: 'Создание...', + updating: 'Обновление...', + form: { + rpmLimit: 'Запросов в минуту (RPM)', + rpmLimitPlaceholder: '0 = без лимита', + rpmLimitHint: 'Максимум запросов в минуту для пользователя; 0 = без лимита. Используется как резерв, если у группы не задан rpm_limit.' + }, + columns: { + user: 'Пользователь', + id: 'ID', + email: 'Email', + username: 'Имя пользователя', + notes: 'Заметки', + role: 'Роль', + groups: 'Группы', + subscriptions: 'Подписки', + balance: 'Баланс', + balancePlatformQuota: 'Баланс (квота платформ)', + usage: 'Расход', + usageAnthropic: 'Расход (Claude)', + usageOpenAI: 'Расход (OpenAI)', + usageGemini: 'Расход (Gemini)', + usageAntigravity: 'Расход (Antigravity)', + concurrency: 'Параллелизм', + status: 'Статус', + lastActive: 'Последняя активность', + lastUsed: 'Последнее использование', + created: 'Создан', + actions: 'Действия' + }, + today: 'Сегодня', + total: 'Последние 30 дн.', + sortBy: 'Сортировать по', + sortCurrentPageOnly: 'Сортирует только текущую страницу', + noSubscription: 'Нет подписки', + publicGroupCount: '+{count} публичных', + exclusiveLabel: 'эксклюзивная', + publicLabel: 'публичная', + daysRemaining: '{days}d', + expired: 'Истекла', + disable: 'Отключить', + enable: 'Включить', + disableUser: 'Отключить пользователя', + enableUser: 'Включить пользователя', + viewApiKeys: 'Показать API-ключи', + groups: 'Группы', + apiKeys: 'API-ключи', + userApiKeys: 'API-ключи пользователя', + noApiKeys: 'У пользователя нет API-ключей', + group: 'Группа', + none: 'Нет', + 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: + 'Выберите стандартные группы, доступные пользователю. Группы подписки управляются отдельно.', + noStandardGroups: 'Нет доступных стандартных групп', + allowAllGroups: 'Разрешить все группы', + allowAllGroupsHint: 'Пользователь может использовать любую неэксклюзивную группу', + allowedGroupsUpdated: 'Разрешённые группы обновлены', + failedToLoadGroups: 'Не удалось загрузить группы', + failedToUpdateAllowedGroups: 'Не удалось обновить разрешённые группы', + // User Group Configuration + 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: + 'например: бонус за регистрацию, промокредит, компенсация и т. п.', + withdrawNotesPlaceholder: + 'например: возврат за сбой сервиса, отмена ошибочного начисления, закрытие аккаунта и т. п.', + notesOptional: 'Заметки необязательны, но полезны для истории', + amountHint: 'Введите положительную сумму', + newBalance: 'Новый баланс', + depositing: 'Пополнение...', + withdrawing: 'Списание...', + confirmDeposit: 'Подтвердить пополнение', + confirmWithdraw: 'Подтвердить списание', + depositSuccess: 'Пополнение выполнено', + withdrawSuccess: 'Списание выполнено', + failedToDeposit: 'Не удалось пополнить', + failedToWithdraw: 'Не удалось списать', + useDepositWithdrawButtons: 'Используйте кнопки пополнения/списания для изменения баланса', + // Balance History + balanceHistory: 'История пополнений', + balanceHistoryTip: 'Нажмите, чтобы открыть историю пополнений', + columnAlwaysVisible: 'Этот столбец всегда виден', + // Per-platform usage breakdown (hover tooltip) + platformBreakdown: 'Разбивка по платформам', + platformBreakdownEmpty: 'Расхода по платформам пока нет', + platformBreakdownHint: 'Наведите, чтобы увидеть расход по платформам', + platformOther: 'Другое', + balanceHistoryTitle: 'История пополнений и параллелизма пользователя', + noBalanceHistory: 'Записей для этого пользователя нет', + allTypes: 'Все типы', + typeBalance: 'Баланс по коду', + typeAffiliateBalance: 'Баланс (партнёрский перевод)', + typeAdminBalance: 'Баланс (админ)', + typeConcurrency: 'Параллелизм по коду', + typeAdminConcurrency: 'Параллелизм (админ)', + typeSubscription: 'Подписка', + failedToLoadBalanceHistory: 'Не удалось загрузить историю баланса', + createdAt: 'Создан', + totalRecharged: 'Всего пополнено', + roles: { + admin: 'Администратор', + user: 'Пользователь' + }, + // Settings Dropdowns + filterSettings: 'Настройки фильтра', + columnSettings: 'Настройки столбцов', + filterValue: 'Введите значение', + // User Attributes + attributes: { + title: 'Атрибуты пользователя', + description: 'Настройте пользовательские поля атрибутов', + configButton: 'Атрибуты', + addAttribute: 'Добавить атрибут', + editAttribute: 'Изменить атрибут', + deleteAttribute: 'Удалить атрибут', + deleteConfirm: "Удалить атрибут '{name}'? Все значения пользователей для него будут удалены.", + noAttributes: 'Нет пользовательских атрибутов', + noAttributesHint: 'Нажмите кнопку выше, чтобы добавить атрибуты', + key: 'Ключ атрибута', + keyHint: 'Для программного доступа: только буквы, цифры и подчёркивания', + name: 'Отображаемое имя', + nameHint: 'Имя, отображаемое в формах', + type: 'Тип атрибута', + fieldDescription: 'Описание', + fieldDescriptionHint: 'Описание атрибута', + placeholder: 'Placeholder', + placeholderHint: 'Текст placeholder для поля ввода', + required: 'Обязательно', + enabled: 'Включено', + options: 'Варианты', + optionsHint: 'Для типов select/multi-select', + addOption: 'Добавить вариант', + optionValue: 'Значение варианта', + optionLabel: 'Отображаемый текст', + validation: 'Правила валидации', + minLength: 'Мин. длина', + maxLength: 'Макс. длина', + min: 'Мин. значение', + max: 'Макс. значение', + pattern: 'Regex-шаблон', + patternMessage: 'Сообщение ошибки валидации', + types: { + text: 'Текст', + textarea: 'Многострочный текст', + number: 'Число', + email: 'Email', + url: 'URL', + date: 'Дата', + select: 'Список', + multi_select: 'Мультивыбор' + }, + created: 'Атрибут создан', + updated: 'Атрибут обновлён', + deleted: 'Атрибут удалён', + reordered: 'Порядок атрибутов обновлён', + failedToLoad: 'Не удалось загрузить атрибуты', + failedToCreate: 'Не удалось создать атрибут', + failedToUpdate: 'Не удалось обновить атрибут', + keyRequired: 'Введите ключ атрибута', + nameRequired: 'Введите отображаемое имя', + optionsRequired: 'Добавьте хотя бы один вариант', + failedToDelete: 'Не удалось удалить атрибут', + failedToReorder: 'Не удалось обновить порядок', + keyExists: 'Ключ атрибута уже существует', + dragToReorder: 'Перетащите для изменения порядка' + }, + platformQuota: { + menuItem: 'Квоты платформ', + title: 'Квоты платформ', + subtitle: 'Настройте дневные, недельные и месячные лимиты расхода в USD для каждой апстрим-платформы пользователя {email}', + columns: { + platform: 'Платформа', + daily: 'День (USD)', + weekly: 'Неделя (USD)', + monthly: 'Месяц (USD, скользящие 30 дней)', + usage: 'Текущий расход', + }, + placeholder: 'безлимитно', + save: 'Сохранить', + saving: 'Сохранение...', + cancel: 'Отмена', + clearAll: 'Очистить всё (убрать все лимиты)', + clearAllConfirm: 'Очистить дневные, недельные и месячные лимиты для ВСЕХ платформ? Все платформы станут "безлимитно" без локальной отмены — перед сохранением значения нужно будет ввести заново.', + reset: { + button: 'Сбросить окно', + confirm: 'Сбросить расход за {window} для {platform} у этого пользователя? Изменение вступит в силу сразу.', + success: 'Расход {platform} за {window} сброшен', + failed: 'Сброс не удался', + }, + updateSuccess: 'Квоты платформ обновлены', + updateFailed: 'Не удалось сохранить', + loadFailed: 'Не удалось загрузить', + hint: 'Пусто = без лимита для этого окна.', + windowDaily: 'день', + windowWeekly: 'неделя', + windowMonthly: 'месяц', + cellNotConfigured: 'Не настроено', + cellColumnTooltip: 'Показаны только платформы с лимитом', + subscriptionWarning: 'У пользователя активная подписка. Квоты платформ применяются только к запросам в режиме баланса (standard); запросы по подписке этими лимитами не ограничены.', + invalidNumber: 'В этих полях некорректные числа. Исправьте перед сохранением: {fields}', + } + }, + + // Groups + groups: { + title: 'Управление группами', + description: 'Управляйте группами API-ключей и коэффициентами тарифа', + searchGroups: 'Поиск групп...', + createGroup: 'Создать группу', + editGroup: 'Изменить группу', + deleteGroup: 'Удалить группу', + sortOrder: 'Сортировка', + sortOrderHint: 'Перетащите группы, чтобы изменить порядок показа. Верхние группы отображаются первыми', + sortOrderUpdated: 'Порядок сортировки обновлён', + failedToUpdateSortOrder: 'Не удалось обновить порядок сортировки', + allPlatforms: 'Все платформы', + allStatus: 'Все статусы', + allGroups: 'Все группы', + exclusive: 'Эксклюзивная', + nonExclusive: 'Неэксклюзивная', + public: 'Публичная', + columns: { + name: 'Имя', + platform: 'Платформа', + rateMultiplier: 'Коэффициент тарифа', + rpmOverride: 'Переопределение RPM', + rpmOverrideHint: 'Лимит RPM пользователя в этой группе; пусто = значение группы; 0 = без лимита', + rateDefault: 'по умолчанию', + rpmDefault: 'по умолчанию', + type: 'Тип', + accounts: 'Аккаунты', + capacity: 'Ёмкость', + usage: 'Расход', + status: 'Статус', + actions: 'Действия', + billingType: 'Тип списания', + userName: 'Имя пользователя', + userEmail: 'Email', + userNotes: 'Notes', + userStatus: 'Статус' + }, + usageToday: 'Сегодня', + usageTotal: 'Итого', + accountsAvailable: 'Доступно:', + accountsRateLimited: 'Ограничено:', + accountsTotal: 'Всего:', + accountsUnit: '', + rateAndAccounts: 'тариф {rate}x · аккаунтов: {count}', + accountsCount: 'Аккаунтов: {count}', + form: { + name: 'Имя', + description: 'Описание', + platform: 'Платформа', + rateMultiplier: 'Коэффициент тарифа', + status: 'Статус', + exclusive: 'Эксклюзивная группа', + rpmLimit: 'Запросов в минуту (RPM)', + rpmLimitPlaceholder: '0 = без лимита', + rpmLimitHint: 'Максимум запросов в минуту для каждого пользователя в группе; 0 = без лимита. Если задано, переопределяет пользовательский rpm_limit в этой группе.' + }, + enterGroupName: 'Введите имя группы', + optionalDescription: 'Описание (необязательно)', + platformHint: 'Выберите платформу, связанную с группой', + platformNotEditable: 'Платформу нельзя изменить после создания', + rateMultiplierHint: 'Коэффициент стоимости для группы (например, 1.5 = 150% базовой стоимости)', + exclusiveHint: 'Эксклюзивная группа, назначается пользователям вручную', + exclusiveTooltip: { + title: 'Что такое эксклюзивная группа?', + description: 'Если включено, пользователи не видят эту группу при создании API-ключей. Использовать её можно только после ручного назначения администратором.', + example: 'Пример:', + exampleContent: 'Тариф публичной группы 0.8. Создайте эксклюзивную группу с тарифом 0.7 и вручную назначьте VIP-пользователей для лучшей цены.' + }, + noGroupsYet: 'Групп пока нет', + createFirstGroup: 'Создайте первую группу для организации API-ключей.', + creating: 'Создание...', + updating: 'Обновление...', + limitDay: 'd', + limitWeek: 'w', + limitMonth: 'mo', + 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: 'Тарифы изменены', + rateSaved: 'Коэффициенты тарифа сохранены', + finalRate: 'Итоговый тариф', + unsavedChanges: 'Несохранённые изменения', + revertChanges: 'Откатить', + userInfo: 'Информация о пользователе', + platforms: { + all: 'All Platforms', + anthropic: 'Anthropic', + openai: 'OpenAI', + gemini: 'Gemini', + antigravity: 'Antigravity', + }, + deleteConfirm: + "Удалить группу '{name}'? Все связанные API-ключи больше не будут принадлежать группе.", + deleteConfirmSubscription: + "Удалить группу подписки '{name}'? Все API-ключи этой подписки станут недействительными, а связанные записи подписок будут удалены. Это действие нельзя отменить.", + subscription: { + title: 'Настройки подписки', + type: 'Тип списания', + typeHint: + 'В стандартном режиме списание идёт с баланса пользователя. Режим подписки использует лимиты квоты.', + typeNotEditable: 'Тип списания нельзя изменить после создания группы.', + standard: 'Стандартный режим (баланс)', + subscription: 'Подписка (квота)', + dailyLimit: 'Дневной лимит (USD)', + weeklyLimit: 'Недельный лимит (USD)', + monthlyLimit: 'Месячный лимит (USD)', + defaultValidityDays: 'Срок действия по умолчанию (дни)', + validityHint: 'Сколько дней действует подписка после назначения пользователю', + noLimit: 'Без лимита' + }, + imagePricing: { + title: 'Цены генерации изображений', + description: 'Настройте доступ к генерации изображений и базовые цены. Оставьте пустым для цен по умолчанию.', + allowImageGeneration: 'Разрешить генерацию изображений для этой группы', + independentMultiplier: 'Использовать отдельный коэффициент изображений', + imageMultiplier: 'Коэффициент изображений', + modeHint: 'По умолчанию списание за изображения: цена изображения × текущий коэффициент группы. Отдельный режим: цена изображения × коэффициент изображений.', + finalPricePreview: 'Итоговая цена за изображение', + notConfigured: 'Не настроено' + }, + modelsList: { + title: 'Свой список моделей /v1/models', + hint: 'Меняет только ответ /v1/models. Белый список вызовов моделей и маршрутизация аккаунтов не меняются.', + loading: 'Загрузка списка моделей...', + empty: 'Нет моделей для отображения' + }, + claudeCode: { + title: 'Ограничение клиента Claude Code', + tooltip: 'Если включено, группа разрешает только официальные клиенты Claude Code. Запросы не от Claude Code будут отклонены или отправлены в резервную группу.', + enabled: 'Только Claude Code', + disabled: 'Разрешить все клиенты', + fallbackGroup: 'Резервная группа', + fallbackHint: 'Запросы не от Claude Code будут использовать эту группу. Оставьте пустым для прямого отклонения.', + noFallback: 'Без резервной группы (отклонять)' + }, + openaiMessages: { + 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', + sonnetModelPlaceholder: 'e.g., gpt-5.3-codex', + haikuModel: 'Целевая модель Haiku', + haikuModelPlaceholder: 'e.g., gpt-5.4-mini', + exactMappingTitle: 'Точные переопределения моделей', + exactMappingHint: 'Точные переопределения моделей Claude имеют приоритет над семействами и могут направлять конкретную модель Claude на другую целевую модель.', + noExactMappings: 'Точных переопределений моделей пока нет', + addExactMapping: 'Добавить точный маппинг', + claudeModel: 'Claude Model', + claudeModelPlaceholder: 'e.g., claude-sonnet-4-5-20250929', + targetModel: 'Целевая модель', + targetModelPlaceholder: 'e.g., gpt-5.4', + removeExactMapping: 'Удалить точный маппинг' + }, + invalidRequestFallback: { + title: 'Резервная группа для некорректного запроса', + hint: 'Срабатывает только когда апстрим явно возвращает слишком длинный prompt. Оставьте пустым, чтобы отключить резерв.', + noFallback: 'Без резервной группы' + }, + copyAccounts: { + title: 'Скопировать аккаунты из групп', + tooltip: 'Выберите одну или несколько групп той же платформы. После создания все аккаунты из этих групп будут автоматически привязаны к новой группе (без дублей).', + tooltipEdit: 'Выберите одну или несколько групп той же платформы. После сохранения аккаунты текущей группы будут заменены аккаунтами из этих групп (без дублей).', + selectPlaceholder: 'Выберите группы, из которых копировать аккаунты...', + hint: 'Можно выбрать несколько групп, аккаунты будут дедуплицированы', + hintEdit: '⚠️ Внимание: это заменит все текущие привязки аккаунтов' + }, + modelRouting: { + title: 'Маршрутизация моделей', + tooltip: 'Настройте маршрутизацию запросов конкретных моделей на выбранные аккаунты. Поддерживаются wildcard, например claude-opus-* соответствует всем моделям opus.', + enabled: 'Включено', + disabled: 'Отключено', + disabledHint: 'Правила маршрутизации действуют только при включении', + addRule: 'Добавить правило маршрутизации', + modelPattern: 'Шаблон модели', + modelPatternPlaceholder: 'claude-opus-*', + modelPatternHint: 'Поддерживает wildcard *, например claude-opus-* соответствует всем моделям opus', + accounts: 'Приоритетные аккаунты', + selectAccounts: 'Выберите аккаунты', + noAccounts: 'В этой группе нет аккаунтов', + loadingAccounts: 'Загрузка аккаунтов...', + removeRule: 'Удалить правило', + noRules: 'Нет правил маршрутизации', + noRulesHint: 'Добавьте правила маршрутизации для отправки запросов конкретных моделей на выбранные аккаунты', + searchAccountPlaceholder: 'Поиск аккаунтов...', + accountsHint: 'Выберите аккаунты с приоритетом для этого шаблона модели' + }, + mcpXml: { + title: 'Вставка протокола MCP XML', + tooltip: 'Если включено и запрос содержит MCP-инструменты, в системный prompt будет вставлен prompt протокола вызова в формате XML. Отключите, чтобы избежать конфликтов с некоторыми клиентами.', + enabled: 'Включено', + disabled: 'Отключено' + }, + claudeMaxSimulation: { + title: 'Симуляция расхода Claude Max', + tooltip: + 'Если включено, для моделей Claude без расхода cache-write от апстрима система детерминированно распределяет токены на небольшой input и создание кэша 1h, сохраняя общий расход токенов.', + enabled: 'Включено (симуляция кэша 1h)', + disabled: 'Отключено', + hint: 'Меняются только категории токенов в логах списаний. Состояние маппинга по запросам не сохраняется.' + }, + supportedScopes: { + title: 'Поддерживаемые семейства моделей', + tooltip: 'Выберите семейства моделей, поддерживаемые группой. Невыбранные семейства не будут маршрутизироваться в эту группу.', + claude: 'Claude', + geminiText: 'Gemini Text', + geminiImage: 'Gemini Image', + hint: 'Выберите хотя бы одно семейство моделей' + } + }, + + // Available Channels (aggregated read-only view) + availableChannels: { + title: 'Доступные каналы', + description: 'Aggregated view: each channel with its linked groups and supported models (wildcards expanded)', + searchPlaceholder: 'Поиск каналов или моделей...', + columns: { + name: 'Канал', + status: 'Статус', + billingSource: 'Billing Model Source', + groups: 'Linked Groups', + supportedModels: 'Поддерживаемые модели' + }, + empty: 'Нет данных', + noGroups: 'No linked groups', + noModels: 'No model mapping configured', + noPricing: 'Цены не настроены', + statusActive: 'Активен', + statusDisabled: 'Отключено', + billingSource: { + requested: 'Requested model', + upstream: 'Upstream model', + channel_mapped: 'Channel-mapped model' + }, + pricing: { + billingMode: 'Режим биллинга', + billingModeToken: 'За токен', + billingModePerRequest: 'За запрос', + billingModeImage: 'За изображение', + inputPrice: 'Вход', + outputPrice: 'Выход', + cacheWritePrice: 'Запись кэша', + cacheReadPrice: 'Чтение кэша', + imageOutputPrice: 'Выход изображения', + perRequestPrice: 'За запрос', + intervals: 'Ступенчатые цены', + unitPerMillion: '/ 1 млн токенов', + unitPerRequest: '/ запрос' + } + }, + + // Channel Management + channels: { + title: 'Управление каналами', + description: 'Управляйте каналами и своими ценами моделей', + searchChannels: 'Поиск каналов...', + createChannel: 'Создать канал', + editChannel: 'Изменить канал', + deleteChannel: 'Удалить канал', + statusActive: 'Активен', + statusDisabled: 'Отключено', + allStatus: 'Все статусы', + 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: 'Цены', + createdAt: 'Создан', + actions: 'Действия' + }, + billingMode: { + token: 'Токены', + perRequest: 'За запрос', + image: 'Изображение (за запрос)' + }, + form: { + name: 'Имя', + namePlaceholder: 'Введите имя канала', + description: 'Описание', + descriptionPlaceholder: 'Описание (необязательно)', + status: 'Статус', + groups: 'Связанные группы', + noGroupsAvailable: 'Нет доступных групп', + inOtherChannel: 'В "{name}"', + modelPricing: 'Цены моделей', + models: 'Модели', + modelsPlaceholder: 'Введите полное имя модели и нажмите Enter', + modelInputHint: 'Нажмите Enter для добавления; поддерживается вставка списком.', + billingMode: 'Режим биллинга', + defaultPrices: 'Цены по умолчанию (резерв, если интервал не подошёл)', + inputPrice: 'Вход', + outputPrice: 'Выход', + cacheWritePrice: 'Запись кэша', + cacheReadPrice: 'Чтение кэша', + imageTokenPrice: 'Выход изображения', + imageOutputPrice: 'Цена выходного изображения', + pricePlaceholder: 'По умолчанию', + 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: 'Подходящих групп не найдено', + 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: 'Поиск аккаунтов...', + ruleAccountsHint: 'Оставьте пустым, чтобы совпадали все аккаунты', + ruleModelPricing: 'Цены моделей', + noGroupsInChannel: 'Вкладки платформ выше не содержат выбранных групп', + unnamed: 'Без имени', + syncLatestModels: 'Синхронизировать новые модели', + syncingModels: 'Синхронизация...', + syncModelsSuccess: 'Синхронизировано новых моделей: {count}', + syncModelsAlreadyUpToDate: 'Модели уже актуальны', + syncModelsError: 'Не удалось синхронизировать модели' + } + }, + + riskControl: { + title: 'Риск-контроль', + description: 'Настройте модерацию контента и просматривайте записи проверки', + loadFailed: 'Не удалось загрузить контроль рисков', + saveFailed: 'Не удалось сохранить настройки модерации контента', + logsFailed: 'Не удалось загрузить записи проверки', + saved: 'Настройки модерации контента сохранены', + refresh: 'Обновить', + 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-ключ', + apiKeys: 'OpenAI API-ключи', + apiKeyCount: 'Ключей: {count}', + apiKeyPlaceholder: 'Введите API-ключ', + apiKeysPlaceholder: 'Добавьте API-ключи, по одному на строку. При сохранении они будут добавлены.', + apiKeysPlaceholderReplace: 'Замените API-ключи, по одному на строку. При сохранении сохранённые ключи будут заменены.', + apiKeysPlaceholderKeep: 'Добавьте API-ключи, по одному на строку. При сохранении они будут добавлены.', + apiKeysHint: 'Сейчас сохранено ключей: {count}. Это поле только добавляет ключи; сохранение добавит их и удалит дубли.', + apiKeysWriteMode: 'Режим записи', + apiKeysModeAppend: 'Добавить', + apiKeysModeReplace: 'Заменить', + apiKeysModeAppendHint: 'По умолчанию: сохранение добавляет введённые ключи и оставляет сохранённые.', + apiKeysModeReplaceHint: 'Режим замены: сохранение заменяет все сохранённые ключи введёнными.', + apiKeysReplaceWarning: 'Режим замены', + apiKeysReplaceNoInput: 'Для режима замены нужен хотя бы 1 API-ключ', + apiKeyPlaceholderKeep: 'Оставьте пустым, чтобы сохранить текущий ключ', + apiKeyWillClear: 'Настроенный ключ будет очищен при сохранении', + apiKeyConfigured: 'Настроено', + apiKeyTemporary: 'Ожидает', + 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: 'Статуса ключей пока нет', + apiKeyHealthEmptyHint: 'Сохраните ключи или проверьте введённые, чтобы увидеть доступность.', + apiKeyStatusOk: 'Доступно', + apiKeyStatusError: 'Ошибка', + apiKeyStatusFrozen: 'Заморожено', + apiKeyStatusUnknown: 'Не проверено', + apiKeyFailureCount: 'Ошибок: {count}', + apiKeyLatency: '{ms} ms', + apiKeyHTTPStatus: 'HTTP {status}', + 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: 'Число повторов', + 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 отправлен', + emailNotSent: 'Без email', + autoBanned: 'Забанен', + unbanUser: 'Разбанить', + unbanSuccess: 'Пользователь разбанен', + unbanFailed: 'Не удалось разбанить пользователя', + inputDetailTitle: 'Детали сводки ввода', + inputDetailContent: 'Полный контент', + queueDelay: 'Queued {ms} ms', + allGroups: 'Все группы', + allGroupsHint: 'Проверяются все группы', + selectedGroupsHint: 'Проверяются выбранные группы', + groupScope: 'Проверяемые группы', + groupScopeHint: 'Включите для всех групп или отключите, чтобы выбрать конкретные группы.', + selectedGroups: 'Выбранные группы', + searchGroups: 'Поиск по названию группы или платформе', + noGroups: 'Нет доступных групп', + 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: 'Основное', + scope: 'Область', + runtime: 'Runtime', + response: 'Уведомление о срабатывании', + riskThresholds: 'Пороги риска', + keywords: 'Блокировка по ключевым словам', + retention: 'Хранение', + }, + blockedKeywords: 'Заблокированные ключевые слова', + blockedKeywordsPlaceholder: 'One keyword per line, e.g.:\nbadword1\nbadword2', + blockedKeywordsDescription: 'Сопоставление без учёта регистра. Будет ли upstream moderation API вызван после срабатывания, зависит от стратегии ниже.', + blockedKeywordsPreBlockHint: 'Блокировка по ключевым словам действует только в режиме "Pre-block".', + blockedKeywordsModeWarning: 'Текущий режим: "{mode}". Блокировка по ключевым словам не будет работать, пока вы не переключитесь в режим "Pre-block".', + blockedKeywordCount: 'Настроено ключевых слов: {count}', + blockedKeywordsLimit: 'До {max} ключевых слов, каждое не длиннее 200 символов. Дубликаты удаляются автоматически.', + keywordBlockingMode: 'Стратегия модерации', + keywordModeKeywordAndApi: 'Keyword + 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: 'Область', + logs: 'Записи проверки', + currentFilter: 'Текущий фильтр', + }, + filters: { + search: 'Search user/key/summary', + from: 'From', + to: 'To', + allGroups: 'Все группы', + allEndpoints: 'All Endpoints', + }, + table: { + 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: 'Отслеживайте доступность, задержку и статус каналов', + searchPlaceholder: 'Поиск имени монитора...', + allProviders: 'Все провайдеры', + allStatus: 'Все статусы', + enabledFilter: 'Включено', + onlyEnabled: 'Только включённые', + onlyDisabled: 'Только отключённые', + createButton: 'Создать монитор', + createTitle: 'Создать монитор канала', + editTitle: 'Изменить монитор канала', + runNow: 'Запустить сейчас', + runSuccess: 'Проверка завершена', + runFailed: 'Проверка не удалась', + apiKeyDecryptFailed: 'Не удалось расшифровать API-ключ. Отредактируйте монитор и укажите новый ключ.', + createSuccess: 'Монитор создан', + updateSuccess: 'Монитор обновлён', + deleteSuccess: 'Монитор удалён', + loadError: 'Не удалось загрузить мониторы', + deleteConfirm: 'Удалить монитор "{name}"? Это действие нельзя отменить.', + nameRequired: 'Введите имя монитора', + primaryModelRequired: 'Введите основную модель', + columns: { + name: 'Имя', + provider: 'Провайдер', + primaryModel: 'Основная модель', + availability7d: 'Доступность 7 дн.', + latency: 'Задержка (мс)', + enabled: 'Включено', + actions: 'Действия' + }, + form: { + name: 'Имя', + namePlaceholder: 'Введите имя монитора', + provider: 'Платформа', + apiMode: 'Протокол OpenAI', + apiModeChatCompletions: 'OpenAI-совместимый', + apiModeChatCompletionsHint: 'Использует /v1/chat/completions с messages; работает с большинством совместимых провайдеров.', + apiModeResponses: 'Responses API', + apiModeResponsesHint: 'Использует /v1/responses с default instructions + input; лучше для self-check/Codex путей.', + endpoint: 'Endpoint', + endpointPlaceholder: 'https://api.example.com', + useCurrentDomain: 'Использовать текущий сервис', + apiKey: 'API-ключ', + apiKeyPlaceholder: 'Введите API-ключ', + apiKeyEditPlaceholder: 'Оставьте пустым, чтобы сохранить текущий ключ', + useMyKey: 'Использовать мой ключ', + selectKeyTitle: 'Выберите мой API-ключ', + selectKeyHint: 'Показаны только ваши активные ключи без истёкшего срока.', + noActiveKey: 'Нет доступных активных API-ключей', + primaryModel: 'Основная модель', + primaryModelPlaceholder: 'gpt-4o-mini', + extraModels: 'Дополнительные модели', + extraModelsPlaceholder: 'Нажмите Enter, чтобы добавить модель', + groupName: 'Имя группы', + groupNamePlaceholder: 'Необязательно, используется для группировки строк в пользовательском виде', + intervalSeconds: 'Интервал (секунды)', + intervalSecondsHint: 'Диапазон: 15–3600 секунд', + enabled: 'Включить монитор', + kindRequired: 'Выберите провайдера' + }, + runResultTitle: 'Результат проверки', + noMonitorsYet: 'Мониторов пока нет', + createFirstMonitor: 'Создайте первый монитор для отслеживания доступности канала', + advanced: { + 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', + headerValuePlaceholder: 'Значение', + headerAddRow: 'Добавить header', + headerNameInvalid: 'Имя header не может содержать пробелы или двоеточие: {name}', + headersHint: 'Объединяются поверх значений адаптера (пользовательские имеют приоритет). Hop-by-hop headers (Host / Content-Length / ...) игнорируются.', + headersParseError: 'Не удалось разобрать строку: {line}', + bodyMode: 'Обработка body', + bodyModeOff: 'По умолчанию', + bodyModeMerge: 'Объединить', + bodyModeReplace: 'Заменить', + bodyModeHintOff: 'Использовать body адаптера по умолчанию (включает challenge validation).', + bodyModeHintMerge: 'Поверхностно объединить с body по умолчанию; пользовательские поля имеют приоритет, но model / messages / contents защищены (для изменения используйте Заменить).', + bodyModeHintReplace: 'Использовать JSON ниже как полный body. Challenge validation пропускается; HTTP 2xx + непустой текст ответа считается рабочим.', + bodyJson: 'Body JSON', + bodyJsonFormat: 'Форматировать', + bodyJsonHint: 'Разбирается при потере фокуса. Пусто = без переопределения.', + bodyJsonError: 'Не удалось разобрать JSON', + bodyJsonObjectError: 'Body должен быть JSON object (не массив и не примитив)' + }, + templateField: { + label: 'Шаблон запроса', + none: 'Без шаблона', + placeholder: 'Выберите шаблон (фильтр по текущему провайдеру)', + applyHint: 'Выбор шаблона копирует его headers и body в этот монитор (снимок). Последующие изменения шаблона не синхронизируются автоматически.' + }, + template: { + manageButton: 'Шаблоны', + managerTitle: 'Менеджер шаблонов запросов', + createButton: 'Новый шаблон', + emptyState: 'Шаблонов для этого провайдера пока нет', + missingName: 'Имя шаблона обязательно', + createSuccess: 'Шаблон создан', + updateSuccess: 'Шаблон обновлён', + deleteSuccess: 'Шаблон удалён', + applyButton: 'Применить к мониторам', + applyTooltip: 'Перезаписать поля снимка у связанных мониторов', + applyTitle: 'Применить шаблон', + applyConfirm: 'Применить', + 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: 'Имя шаблона', + namePlaceholder: 'например, Claude Code mimicry', + description: 'Описание', + descriptionPlaceholder: 'Необязательно: для чего шаблон, дата фиксации и т. п.' + } + } + }, + + // Subscriptions + subscriptions: { + title: 'Управление подписками', + description: 'Управляйте подписками пользователей и лимитами квот', + assignSubscription: 'Назначить подписку', + adjustSubscription: 'Изменить подписку', + revokeSubscription: 'Отозвать подписку', + allStatus: 'Все статусы', + allGroups: 'Все группы', + allPlatforms: 'Все платформы', + daily: 'Дневная', + weekly: 'Недельная', + monthly: 'Месячная', + noLimits: 'Лимиты не настроены', + unlimited: 'Безлимитно', + resetNow: 'Скоро сброс', + windowNotActive: 'Окно неактивно', + resetInMinutes: 'Сброс через {minutes} мин', + resetInHoursMinutes: 'Сброс через {hours} ч {minutes} мин', + resetInDaysHours: 'Сброс через {days} д {hours} ч', + quotaEndsInMinutes: 'Квота закончится через {minutes} мин', + quotaEndsInHoursMinutes: 'Квота закончится через {hours} ч {minutes} мин', + quotaEndsInDaysHours: 'Квота закончится через {days} д {hours} ч', + daysRemaining: 'дней осталось', + remainingDays: 'Осталось дней', + noExpiration: 'Без срока', + status: { + active: 'Активен', + expired: 'Истекла', + revoked: 'Отозвана' + }, + columns: { + user: 'Пользователь', + group: 'Группа', + usage: 'Расход', + expires: 'Истекает', + status: 'Статус', + actions: 'Действия' + }, + form: { + user: 'Пользователь', + group: 'Группа подписки', + validityDays: 'Срок действия (дни)', + adjustDays: 'Изменить на (дни)' + }, + selectUser: 'Выберите пользователя', + selectGroup: 'Выберите группу подписки', + groupHint: 'Показаны только группы с типом списания подписки', + validityHint: 'Количество дней действия подписки', + adjustingFor: 'Изменение подписки для', + currentExpiration: 'Текущий срок действия', + adjustDaysPlaceholder: 'Положительное — продлить, отрицательное — сократить', + adjustHint: 'Введите положительное число для продления, отрицательное для сокращения (остаток дней должен быть > 0)', + assign: 'Назначить', + assigning: 'Назначение...', + adjust: 'Изменить', + adjusting: 'Изменение...', + revoke: 'Отозвать', + resetQuota: 'Сбросить квоту', + resetQuotaTitle: 'Сбросить квоту расхода', + resetQuotaConfirm: "Сбросить дневную, недельную и месячную квоту расхода для '{user}'? Расход будет обнулён, окна начнутся заново с сегодняшнего дня.", + quotaResetSuccess: 'Квота сброшена', + failedToResetQuota: 'Не удалось сбросить квоту', + noSubscriptionsYet: 'Подписок пока нет', + assignFirstSubscription: 'Назначьте подписку, чтобы начать.', + subscriptionAssigned: 'Подписка назначена', + subscriptionAdjusted: 'Подписка изменена', + subscriptionRevoked: 'Подписка отозвана', + failedToLoad: 'Не удалось загрузить подписки', + failedToAssign: 'Не удалось назначить подписку', + failedToAdjust: 'Не удалось изменить подписку', + failedToRevoke: 'Не удалось отозвать подписку', + adjustWouldExpire: 'После изменения должно остаться больше 0 дней', + adjustOutOfRange: 'Изменение дней должно быть от -36500 до 36500', + pleaseSelectUser: 'Выберите пользователя', + pleaseSelectGroup: 'Выберите группу', + validityDaysRequired: 'Введите корректное число дней (минимум 1)', + revokeConfirm: + "Отозвать подписку пользователя '{user}'? Это действие нельзя отменить.", + guide: { + title: 'Руководство по управлению подписками', + subtitle: 'Режим подписки позволяет назначать пользователям временные квоты расхода с дневными, недельными и месячными лимитами. Выполните эти шаги, чтобы начать.', + showGuide: 'Руководство', + step1: { + title: 'Создайте группу подписки', + line1: 'Перейдите на страницу "Управление группами" и нажмите "Создать группу"', + line2: 'Установите тип списания "Подписка" и настройте дневные, недельные и месячные лимиты квоты', + line3: 'Сохраните группу и убедитесь, что её статус "Активен"', + link: 'Перейти к управлению группами' + }, + step2: { + title: 'Назначьте подписку пользователю', + line1: 'Нажмите кнопку "Назначить подписку" справа сверху', + line2: 'Найдите пользователя по email и выберите его', + line3: 'Выберите группу подписки, задайте срок действия и нажмите "Назначить"' + }, + step3: { + title: 'Управляйте существующими подписками' + }, + actions: { + adjust: 'Изменить', + adjustDesc: 'Продлить или сократить срок действия подписки', + resetQuota: 'Сбросить квоту', + resetQuotaDesc: 'Обнулить дневной, недельный и месячный расход', + revoke: 'Отозвать', + revokeDesc: 'Немедленно завершить подписку (необратимо)' + }, + tip: 'Совет: в списке групп отображаются только группы с типом списания "Подписка" и статусом "Активен". Если вариантов нет, сначала создайте группу в управлении группами.' + } + }, + + // Accounts + accounts: { + title: 'Управление аккаунтами', + description: 'Управляйте аккаунтами AI-платформ и учётными данными', + createAccount: 'Создать аккаунт', + autoRefresh: 'Автообновление', + enableAutoRefresh: 'Включить автообновление', + refreshInterval5s: '5 seconds', + refreshInterval10s: '10 seconds', + refreshInterval15s: '15 seconds', + refreshInterval30s: '30 seconds', + autoRefreshCountdown: 'Автообновление: {seconds}с', + listPendingSyncHint: 'Есть несинхронизированные изменения списка. Нажмите синхронизацию, чтобы загрузить актуальные строки.', + listPendingSyncAction: 'Синхронизировать сейчас', + syncFromCrs: 'Синхронизация из CRS', + dataExport: 'Экспорт', + dataExportSelected: 'Экспортировать выбранное', + dataExportIncludeProxies: 'Включить proxy, привязанные к экспортируемым аккаунтам', + dataImport: 'Импорт', + 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: + 'Синхронизируйте аккаунты из 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: 'Также синхронизировать proxy (сопоставить по host/port/auth или создать)', + syncNow: 'Синхронизировать', + syncing: 'Синхронизация...', + syncMissingFields: 'Заполните Base URL, имя пользователя и пароль', + syncResult: 'Результат синхронизации', + syncResultSummary: 'Создано {created}, обновлено {updated}, пропущено {skipped}, ошибок {failed}', + syncErrors: 'Ошибки / детали пропусков', + syncCompleted: 'Синхронизация завершена: создано {created}, обновлено {updated}, пропущено {skipped}', + syncCompletedWithErrors: + 'Синхронизация завершена с ошибками: ошибок {failed} (создано {created}, обновлено {updated}, пропущено {skipped})', + syncFailed: 'Синхронизация не удалась', + crsPreview: 'Предпросмотр', + crsPreviewing: 'Предпросмотр...', + crsPreviewFailed: 'Предпросмотр не удался', + crsExistingAccounts: 'Существующие аккаунты (будут обновлены)', + crsNewAccounts: 'Новые аккаунты (выберите для синхронизации)', + crsSelectAll: 'Выбрать все', + crsSelectNone: 'Снять выбор', + crsNoNewAccounts: 'Все аккаунты CRS уже синхронизированы.', + crsWillUpdate: 'Будут обновлены существующие аккаунты: {count}.', + crsSelectedCount: 'Выбрано новых аккаунтов: {count}', + crsUpdateBehaviorNote: + 'Для существующих аккаунтов синхронизируются только поля, которые вернул CRS; отсутствующие поля сохраняют текущие значения. Учётные данные объединяются по ключу — ключи, не возвращённые CRS, сохраняются. proxy сохраняются, если "Синхронизировать proxy" выключено.', + crsBack: 'Назад', + editAccount: 'Изменить аккаунт', + deleteAccount: 'Удалить аккаунт', + searchAccounts: 'Поиск аккаунтов...', + notes: 'Заметки', + notesPlaceholder: 'Введите заметки', + notesHint: 'Заметки необязательны', + allPlatforms: 'Все платформы', + allTypes: 'Все типы', + allStatus: 'Все статусы', + allGroups: 'Все группы', + ungroupedGroup: 'Без группы', + oauthType: 'OAuth', + setupToken: 'Setup Token', + apiKey: 'API-ключ', + // Schedulable toggle + schedulable: 'Доступен для маршрутизации', + schedulableHint: 'Включите, чтобы аккаунт участвовал в маршрутизации API-запросов', + schedulableEnabled: 'Участие в маршрутизации включено', + schedulableDisabled: 'Участие в маршрутизации отключено', + failedToToggleSchedulable: 'Не удалось изменить статус участия в маршрутизации', + groupCountTotal: 'Всего групп: {count}', + 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: 'Апстрим', + upstreamDesc: 'Connect via Base URL + API Key' + }, + status: { + active: 'Активен', + inactive: 'Неактивен', + error: 'Ошибка', + 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: 'Тип', + capacity: 'Ёмкость', + notes: 'Заметки', + priority: 'Приоритет', + billingRateMultiplier: 'Тарифный коэффициент', + weight: 'Вес', + status: 'Статус', + schedulable: 'Доступен для маршрутизации', + todayStats: 'Статистика за сегодня', + groups: 'Группы', + usageWindows: 'Окна расхода', + proxy: 'Proxy', + lastUsed: 'Последнее использование', + createdAt: 'Создан', + expiresAt: 'Истекает', + actions: 'Действия' + }, + allPrivacyModes: 'Все состояния приватности', + privacyUnset: 'Не задано', + privacyTrainingOff: 'Передача данных для обучения отключена', + privacyCfBlocked: 'Заблокировано Cloudflare, обучение может быть всё ещё включено', + privacyFailed: 'Не удалось отключить обучение', + privacyAntigravitySet: 'Телеметрия и маркетинговые письма отключены', + privacyAntigravityFailed: 'Не удалось настроить приватность', + setPrivacy: 'Настроить приватность', + subscriptionAbnormal: 'Аномалия', + subscriptionExpires: 'Истекает', + // Capacity status tooltips + capacity: { + windowCost: { + blocked: 'Расход за окно 5h превышен, маршрутизация аккаунта приостановлена', + stickyOnly: 'Расход за окно 5h на пороге, разрешены только sticky-сессии', + normal: 'Расход за окно 5h в норме' + }, + sessions: { + full: 'Активные сессии заполнены, новые сессии ждут (idle timeout: {idle} мин)', + normal: 'Активные сессии в норме (idle timeout: {idle} мин)' + }, + rpm: { + 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: 'Квота превышена, аккаунт приостановлен', + normal: 'Квота в норме' + }, + }, + tempUnschedulable: { + title: 'Временно исключён из маршрутизации', + statusTitle: 'Статус временного исключения из маршрутизации', + hint: 'Временно отключайте аккаунты, когда совпадают код ошибки и ключевое слово.', + notice: 'Правила проверяются по порядку и требуют совпадения кода ошибки и ключевого слова.', + addRule: 'Добавить правило', + ruleOrder: 'Порядок правил', + ruleIndex: 'Правило #{index}', + errorCode: 'Код ошибки', + errorCodePlaceholder: 'e.g. 429', + durationMinutes: 'Длительность (минуты)', + durationPlaceholder: 'e.g. 30', + keywords: 'Ключевые слова', + keywordsPlaceholder: 'e.g. overloaded, too many requests', + keywordsHint: 'Разделяйте ключевые слова запятыми; любое совпадение сработает.', + description: 'Описание', + descriptionPlaceholder: 'Заметка к правилу (необязательно)', + rulesInvalid: 'Добавьте хотя бы одно правило с кодом ошибки, ключевыми словами и длительностью.', + viewDetails: 'Показать детали временного исключения из маршрутизации', + accountName: 'Аккаунт', + triggeredAt: 'Сработало', + until: 'До', + remaining: 'Осталось', + matchedKeyword: 'Совпавшее ключевое слово', + errorMessage: 'Детали ошибки', + reset: 'Восстановить состояние', + resetSuccess: 'Состояние аккаунта восстановлено', + resetFailed: 'Не удалось восстановить состояние аккаунта', + failedToLoad: 'Не удалось загрузить статус временного исключения', + notActive: 'Этот аккаунт не исключён временно из маршрутизации.', + expired: 'Истекла', + remainingMinutes: 'Около {minutes} мин', + remainingHours: 'Около {hours} ч', + remainingHoursMinutes: 'Около {hours} ч {minutes} мин', + presets: { + overloadLabel: '529 Overloaded', + overloadDesc: 'Перегрузка — пауза 60 минут', + rateLimitLabel: '429 Rate Limit', + rateLimitDesc: 'Ограничен по лимиту — пауза 10 минут', + unavailableLabel: '503 Unavailable', + unavailableDesc: 'Недоступен — пауза 30 минут' + } + }, + clearRateLimit: 'Очистить лимит частоты', + resetQuota: 'Сбросить квоту', + quotaLimit: 'Лимит квоты', + quotaLimitPlaceholder: '0 = безлимитно', + quotaLimitHint: 'Задайте дневные, недельные и общие лимиты расхода (USD). Аккаунты Anthropic API key также могут настраивать client affinity. Изменение лимитов не сбрасывает расход.', + quotaLimitToggle: 'Включить лимит квоты', + quotaLimitToggleHint: 'Если включено, аккаунт будет приостановлен при достижении заданного лимита расхода', + quotaDailyLimit: 'Дневной лимит', + quotaDailyLimitHint: 'Автоматически сбрасывается каждые 24 часа с первого использования.', + quotaWeeklyLimit: 'Недельный лимит', + quotaWeeklyLimitHint: 'Автоматически сбрасывается каждые 7 дней с первого использования.', + quotaTotalLimit: 'Общий лимит', + quotaTotalLimitHint: 'Накопительный лимит расхода. Не сбрасывается автоматически — используйте "Сбросить квоту" для очистки.', + quotaResetMode: 'Режим сброса', + quotaResetModeRolling: 'Скользящее окно', + quotaResetModeFixed: 'Фиксированное время', + quotaResetHour: 'Час сброса', + quotaWeeklyResetDay: 'День сброса', + quotaResetTimezone: 'Часовой пояс сброса', + quotaDailyLimitHintFixed: 'Сбрасывается ежедневно в {hour}:00 ({timezone}).', + quotaWeeklyLimitHintFixed: 'Сбрасывается каждый {day} в {hour}:00 ({timezone}).', + dayOfWeek: { + monday: 'Понедельник', + tuesday: 'Вторник', + wednesday: 'Среда', + thursday: 'Четверг', + friday: 'Пятница', + saturday: 'Суббота', + sunday: 'Воскресенье', + }, + quotaLimitAmount: 'Общий лимит', + quotaLimitAmountHint: 'Накопительный лимит расхода. Не сбрасывается автоматически.', + quotaNotify: { + alert: 'Оповещение', + enabled: 'Включить оповещение', + threshold: 'Порог оповещения', + thresholdPlaceholder: 'Введите процент', + }, + testConnection: 'Проверить подключение', + reAuthorize: 'Повторная авторизация', + refreshToken: 'Обновить токен', + noAccountsYet: 'Аккаунтов пока нет', + createFirstAccount: 'Создайте первый аккаунт, чтобы начать использовать AI-сервисы.', + tokenRefreshed: 'Токен обновлён', + accountDeleted: 'Аккаунт удалён', + rateLimitCleared: 'Лимит частоты очищен', + bulkSchedulableEnabled: 'Участие в маршрутизации включено для аккаунтов: {count}', + bulkSchedulableDisabled: 'Участие в маршрутизации отключено для аккаунтов: {count}', + bulkSchedulablePartial: 'Маршрутизация обновлена частично: успешно {success}, ошибок {failed}', + bulkSchedulableResultUnknown: 'Результат массового изменения маршрутизации неполный. Повторите или обновите страницу.', + bulkActions: { + selected: 'Выбрано аккаунтов: {count}', + selectCurrentPage: 'Выбрать эту страницу', + clear: 'Снять выбор', + edit: 'Массовое редактирование', + delete: 'Массовое удаление', + enableScheduling: 'Включить маршрутизацию', + disableScheduling: 'Отключить маршрутизацию', + resetStatus: 'Сбросить статус', + refreshToken: 'Обновить токен', + resetStatusSuccess: 'Статус сброшен для аккаунтов: {count}', + refreshTokenSuccess: 'Токен обновлён для аккаунтов: {count}', + partialSuccess: 'Выполнено частично: успешно {success}, ошибок {failed}' + }, + bulkEdit: { + title: 'Массовое редактирование аккаунтов', + selectionInfo: + 'Выбрано аккаунтов: {count}. Будут обновлены только отмеченные или заполненные поля; остальные останутся без изменений.', + baseUrlPlaceholder: 'https://api.anthropic.com or https://api.openai.com', + baseUrlNotice: 'Применяется только к аккаунтам API Key; оставьте пустым, чтобы сохранить текущее значение', + submit: 'Обновить аккаунты', + updating: 'Обновление...', + 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: 'Имя аккаунта', + enterAccountName: 'Введите имя аккаунта', + accountType: 'Тип аккаунта', + claudeCode: 'Claude Code', + claudeConsole: 'Claude Console', + bedrockLabel: 'AWS Bedrock', + bedrockDesc: 'SigV4 / API Key', + vertexLabel: 'Vertex', + vertexDesc: 'Service Account', + 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 загружен', + 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: 'Добавить способ', + setupTokenLongLived: 'Setup Token (долгоживущий)', + baseUrl: 'Base URL', + baseUrlHint: 'Оставьте значение по умолчанию для официального Anthropic API', + apiKeyRequired: 'API Key *', + apiKeyPlaceholder: 'sk-ant-api03-...', + apiKeyHint: 'Ваш Claude Console API Key', + // OpenAI specific hints + openai: { + baseUrlHint: 'Оставьте значение по умолчанию для официального OpenAI API', + apiKeyHint: 'Ваш OpenAI API Key', + oauthPassthrough: 'Автоматический passthrough (только auth)', + oauthPassthroughDesc: + 'Если включено, этот аккаунт OpenAI использует автоматический passthrough: шлюз пересылает request/response как есть и только заменяет auth, сохраняя тарификацию, параллелизм, аудит и необходимую safety-фильтрацию.', + responsesWebsocketsV2: 'Responses WebSocket v2', + responsesWebsocketsV2Desc: + 'По умолчанию отключено. Включите, чтобы разрешить 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: + 'Когда режим WS включён, параллелизм аккаунта становится лимитом пула WS-соединений для этого аккаунта.', + wsModePassthroughHint: 'Режим passthrough не использует пул WS-соединений.', + oauthResponsesWebsocketsV2: 'OAuth WebSocket Mode', + oauthResponsesWebsocketsV2Desc: + 'Применяется только к OpenAI OAuth. Этот аккаунт может использовать OpenAI WebSocket Mode только когда функция включена.', + apiKeyResponsesWebsocketsV2: 'API Key WebSocket Mode', + apiKeyResponsesWebsocketsV2Desc: + 'Применяется только к OpenAI API Key. Этот аккаунт может использовать OpenAI WebSocket Mode только когда функция включена.', + responsesWebsocketsV2PassthroughHint: + 'Автоматический passthrough сейчас включён: он влияет только на HTTP passthrough и не отключает режим WS.', + responsesMode: 'Поддержка Responses API', + responsesModeDesc: + 'Применяется только к аккаунтам OpenAI API Key. Auto следует результатам probe; принудительные режимы переопределяют 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', + codexCLIOnlyDesc: + 'Применяется только к OpenAI OAuth. Если включено, разрешены только семейства официальных клиентов Codex; если отключено, шлюз обходит это ограничение и сохраняет прежнее поведение.', + codexImageGenerationBridge: 'Мост генерации изображений Codex', + codexImageGenerationBridgeDesc: + 'Политика аккаунта имеет приоритет над настройками канала и глобальными настройками. Управляет только тем, получают ли запросы 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: + 'Управляет участием этого аккаунта в маршрутизации /responses/compact. Auto следует результатам probe, Force On всегда разрешает, Force Off всегда исключает.', + compactModeAuto: 'Auto', + compactModeForceOn: 'Force On', + compactModeForceOff: 'Force Off', + compactModelMapping: 'Model mapping только для Compact', + compactModelMappingDesc: + 'Применяется только к /responses/compact. Используйте, когда upstream compact endpoint требует специальную compact-модель.', + compactSupported: 'Compact поддерживается', + compactUnsupported: 'Compact не поддерживается', + compactAuto: 'Compact Auto', + compactUnknown: 'Compact Auto', + compactLastChecked: 'Последний compact probe', + testMode: 'Режим теста', + testModeDefault: 'Запрос по умолчанию', + testModeCompact: 'Compact probe', + modelRestrictionDisabledByPassthrough: 'Автоматический passthrough включён: whitelist/mapping моделей не будет применяться.', + }, + anthropic: { + apiKeyPassthrough: 'Автоматический passthrough (только auth)', + apiKeyPassthroughDesc: + 'Применяется только к аккаунтам Anthropic API Key. Если включено, messages/count_tokens пересылаются в passthrough-режиме только с заменой auth, при этом тарификация, параллелизм, аудит и safety-фильтрация сохраняются. Отключите, чтобы сразу откатиться.', + webSearchEmulation: 'Эмуляция Web Search', + webSearchEmulationDesc: + 'Включите эмуляцию Web Search для этого аккаунта API Key. Когда обнаружен чистый запрос web_search, шлюз вызывает сторонний search API и собирает ответ локально. По умолчанию следует настройке канала.', + webSearchDefault: 'По умолчанию', + webSearchEnabled: 'Включено', + webSearchDisabled: 'Отключено', + }, + modelRestriction: 'Ограничение моделей (необязательно)', + modelWhitelist: 'Whitelist моделей', + modelMapping: 'Model Mapping', + selectAllowedModels: 'Выберите разрешённые модели. Оставьте пустым для поддержки всех моделей.', + mapRequestModels: + 'Сопоставьте запрошенные модели с фактическими. Слева — запрошенная модель, справа — фактическая модель, отправляемая в 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: 'Модель уже существует', + modelCount: 'Моделей: {count}', + poolMode: 'Pool Mode', + poolModeHint: 'Включите, когда upstream является пулом аккаунтов; ошибки не будут помечать локальный статус аккаунта', + poolModeInfo: + 'Если включено, upstream-ошибки 429/403/401 будут автоматически повторяться без пометки аккаунта как ограниченного по лимиту или ошибочного. Подходит для upstream, указывающего на другой экземпляр sub2api.', + poolModeRetryCount: 'Повторы на том же аккаунте', + poolModeRetryCountHint: + 'Применяется только в pool mode. Используйте 0, чтобы отключить повтор на месте. По умолчанию {default}, максимум {max}.', + poolModeRetryStatusCodes: 'HTTP-коды для повтора', + poolModeRetryStatusCodesHint: + 'HTTP status codes (100-599) через запятую, которые запускают повтор на том же аккаунте в pool mode. Оставьте пустым, чтобы использовать значения по умолчанию ({default}).', + customErrorCodes: 'Пользовательские коды ошибок', + customErrorCodesHint: 'Останавливать маршрутизацию только для выбранных кодов ошибок', + customErrorCodesWarning: + 'Только выбранные коды ошибок будут останавливать маршрутизацию. Остальные ошибки вернут 500.', + customErrorCodes429Warning: + '429 уже имеет встроенную обработку лимита частоты. Добавление его в пользовательские коды ошибок отключит аккаунт вместо временного ограничения по лимиту. Продолжить?', + customErrorCodes529Warning: + '529 уже имеет встроенную обработку перегрузки. Добавление его в пользовательские коды ошибок отключит аккаунт вместо временной пометки перегрузки. Продолжить?', + selectedErrorCodes: 'Выбрано', + noneSelectedUsesDefault: 'Ничего не выбрано (используется политика по умолчанию)', + enterErrorCode: 'Введите код ошибки (100-599)', + invalidErrorCode: 'Введите корректный HTTP-код ошибки (100-599)', + errorCodeExists: 'Этот код ошибки уже выбран', + interceptWarmupRequests: 'Перехватывать warmup-запросы', + interceptWarmupRequestsDesc: + 'Если включено, warmup-запросы вроде генерации заголовка будут возвращать mock-ответы без расхода upstream-токенов', + autoPauseOnExpired: 'Автопауза при истечении срока', + autoPauseOnExpiredDesc: 'Если включено, аккаунт автоматически приостановит маршрутизацию после истечения срока действия', + // Quota control (Anthropic OAuth/SetupToken only) + quotaControl: { + title: 'Управление квотой', + hint: 'Настройте окно расхода, лимиты сессий, client affinity и другие параметры маршрутизации.', + windowCost: { + label: 'Лимит расхода окна 5h', + hint: 'Ограничьте расход аккаунта в пределах 5-часового окна', + limit: 'Порог расхода', + limitPlaceholder: '50', + limitHint: 'Аккаунт не будет участвовать в новой маршрутизации после достижения порога', + stickyReserve: 'Sticky Reserve', + stickyReservePlaceholder: '10', + stickyReserveHint: 'Дополнительный резерв для sticky-сессий' + }, + sessionLimit: { + label: 'Лимит количества сессий', + hint: 'Ограничьте число активных параллельных сессий', + maxSessions: 'Макс. сессий', + maxSessionsPlaceholder: '3', + maxSessionsHint: 'Максимальное число активных параллельных сессий', + idleTimeout: 'Idle Timeout', + idleTimeoutPlaceholder: '5', + idleTimeoutHint: 'Сессии будут освобождены после idle timeout' + }, + rpmLimit: { + label: 'Лимит RPM', + hint: 'Ограничьте запросы в минуту для защиты upstream-аккаунтов', + baseRpm: 'Base RPM', + baseRpmPlaceholder: '15', + baseRpmHint: 'Максимум запросов в минуту; 0 или пусто = без лимита', + strategy: 'Стратегия RPM', + strategyTiered: 'Tiered Model', + strategyStickyExempt: 'Sticky Exempt', + strategyTieredHint: 'Green → Yellow → только sticky → Blocked, постепенное ограничение', + strategyStickyExemptHint: 'При превышении лимита разрешены только sticky-сессии', + strategyHint: 'Tiered: постепенно ограничивает при превышении; Sticky Exempt: существующие сессии не ограничиваются', + stickyBuffer: 'Sticky Buffer', + 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', + hint: 'Симулирует TLS fingerprint клиента Node.js/Claude Code', + defaultProfile: 'Встроенный профиль по умолчанию', + randomProfile: 'Случайный' + }, + sessionIdMasking: { + label: 'Маскирование Session ID', + hint: 'Если включено, фиксирует session ID в metadata.user_id на 15 минут, чтобы upstream считал запросы идущими из одной сессии' + }, + cacheTTLOverride: { + label: 'Переопределение Cache TTL', + hint: 'Принудительно тарифицировать все токены создания cache по выбранному уровню TTL (5m или 1h)', + target: 'Целевой TTL', + targetHint: 'Выберите уровень TTL для тарификации' + }, + customBaseUrl: { + label: 'Пользовательский Relay URL', + hint: 'Пересылать запросы в пользовательский relay-сервис. Proxy URL будет передан как query-параметр.', + urlHint: 'Relay service URL (e.g., https://relay.example.com)', + }, + clientAffinity: { + label: 'Маршрутизация Client Affinity', + hint: 'Если включено, новые сессии предпочитают аккаунты, которые этот клиент использовал раньше, чтобы сократить переключения аккаунтов' + } + }, + affinityNoClients: 'Нет affinity-клиентов', + affinityClients: 'Affinity-клиентов: {count}', + affinitySection: 'Client Affinity', + affinitySectionHint: 'Управляйте распределением клиентов по аккаунтам. Настройте пороги зон для балансировки нагрузки.', + affinityToggle: 'Включить Client Affinity', + affinityToggleHint: 'Новые сессии предпочитают аккаунты, которые этот клиент использовал раньше', + affinityBase: 'Базовый лимит (Green Zone)', + affinityBasePlaceholder: 'Пусто = без лимита', + affinityBaseHint: 'Максимум клиентов в green zone (полный приоритет маршрутизации)', + affinityBaseOffHint: 'Нет лимита green zone. Все клиенты получают полный приоритет маршрутизации.', + affinityBuffer: 'Buffer (Yellow Zone)', + affinityBufferPlaceholder: 'e.g. 3', + affinityBufferHint: 'Дополнительные клиенты, разрешённые в yellow zone (пониженный приоритет)', + affinityBufferInfinite: 'Безлимитно', + expired: 'Истекла', + proxy: 'Proxy', + noProxy: 'Без proxy', + concurrency: 'Параллелизм', + loadFactor: 'Коэффициент нагрузки', + loadFactorHint: 'Более высокий коэффициент нагрузки увеличивает частоту маршрутизации', + priority: 'Приоритет', + priorityHint: 'Аккаунты с меньшим значением используются первыми', + billingRateMultiplier: 'Тарифный коэффициент', + billingRateMultiplierHint: '0 = бесплатно, влияет только на тарификацию аккаунта', + expiresAt: 'Истекает', + expiresAtHint: 'Оставьте пустым, если срок действия не ограничен', + higherPriorityFirst: 'Меньшее значение означает более высокий приоритет', + mixedScheduling: 'Использовать в /v1/messages', + mixedSchedulingHint: 'Включите для участия в маршрутизации группы Anthropic/Gemini', + mixedSchedulingTooltip: + '!! ПРЕДУПРЕЖДЕНИЕ !! Antigravity Claude и Anthropic Claude нельзя использовать в одном контексте. Если у вас есть аккаунты Anthropic и Antigravity, включение этой опции приведёт к частым ошибкам 400. После включения используйте группы, чтобы изолировать аккаунты Antigravity от аккаунтов Anthropic. Убедитесь, что понимаете это перед включением!!', + aiCreditsBalance: 'AI Credits', + allowOverages: 'Разрешить перерасход (AI Credits)', + allowOveragesTooltip: + 'Использовать AI Credits только после явного исчерпания бесплатной квоты. Обычные параллельные лимиты 429 не будут переключаться на перерасход.', + creating: 'Создание...', + updating: 'Обновление...', + 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: 'Принудительный 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: 'Введите Bedrock API Key', + bedrockApiKeyLeaveEmpty: 'Оставьте пустым, чтобы сохранить текущий ключ', + apiKeyIsRequired: 'API Key обязателен', + leaveEmptyToKeep: 'Оставьте пустым, чтобы сохранить текущий ключ', + // Upstream type + upstream: { + baseUrl: 'Upstream Base URL', + baseUrlHint: 'Адрес upstream-сервиса Antigravity, например https://cloudcode-pa.googleapis.com', + apiKey: 'Upstream API Key', + apiKeyHint: 'API Key для upstream-сервиса', + pleaseEnterBaseUrl: 'Введите upstream Base URL', + pleaseEnterApiKey: 'Введите upstream API Key' + }, + // OAuth flow + oauth: { + title: 'Авторизация аккаунта Claude', + authMethod: 'Способ авторизации', + manualAuth: 'Ручная авторизация', + cookieAutoAuth: 'Автоавторизация через cookie', + cookieAutoAuthDesc: + 'Используйте sessionKey claude.ai, чтобы автоматически завершить OAuth-авторизацию без ручного открытия браузера.', + sessionKey: 'sessionKey', + keysCount: 'Ключей: {count}', + batchCreateAccounts: 'Будет массово создано аккаунтов: {count}', + sessionKeyPlaceholder: + 'Один sessionKey на строку, например:\nsk-ant-sid01-xxxxx...\nsk-ant-sid01-yyyyy...', + sessionKeyPlaceholderSingle: 'sk-ant-sid01-xxxxx...', + 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: + 'Откройте URL авторизации в новой вкладке, войдите в аккаунт Claude и авторизуйте доступ.', + proxyWarning: + 'Примечание: если настроен proxy, убедитесь, что браузер использует тот же proxy для доступа к странице авторизации.', + step3EnterCode: 'Введите Authorization Code', + authCodeDesc: + 'После завершения авторизации страница покажет Authorization Code. Скопируйте и вставьте его ниже:', + authCode: 'Authorization Code', + authCodePlaceholder: 'Вставьте Authorization Code со страницы Claude...', + authCodeHint: 'Вставьте Authorization Code, скопированный со страницы Claude', + completeAuth: 'Завершить авторизацию', + verifying: 'Проверка...', + pleaseEnterSessionKey: 'Введите хотя бы один корректный sessionKey', + authFailed: 'Авторизация не удалась', + cookieAuthFailed: 'Cookie-авторизация не удалась', + keyAuthFailed: 'Ключ {index}: {error}', + successCreated: 'Создано аккаунтов: {count}', + batchSuccess: 'Создано аккаунтов: {count}', + batchPartialSuccess: 'Частичный успех: успешно {success}, ошибок {failed}', + batchFailed: 'Массовое создание не удалось', + // OpenAI specific + openai: { + title: 'Авторизация аккаунта OpenAI', + followSteps: 'Выполните шаги для авторизации аккаунта OpenAI:', + step1GenerateUrl: 'Нажмите кнопку ниже, чтобы сгенерировать URL авторизации', + generateAuthUrl: 'Сгенерировать Auth URL', + step2OpenUrl: 'Откройте URL в браузере и завершите авторизацию', + openUrlDesc: + 'Откройте URL авторизации в новой вкладке, войдите в аккаунт OpenAI и авторизуйте доступ.', + importantNotice: + 'Важно: после авторизации страница может загружаться некоторое время. Подождите. Когда адресная строка браузера изменится на http://localhost..., авторизация завершена.', + step3EnterCode: 'Введите Authorization URL или код', + authCodeDesc: + 'После завершения авторизации, когда URL страницы станет http://localhost:xxx/auth/callback?code=...:', + authCode: 'Authorization URL или код', + authCodePlaceholder: + 'Вариант 1: скопируйте полный URL\n(http://localhost:xxx/auth/callback?code=...)\nВариант 2: скопируйте только значение параметра code', + authCodeHint: + 'Можно скопировать весь URL или только значение параметра code — система определит это автоматически', + failedToGenerateUrl: 'Не удалось сгенерировать auth URL OpenAI', + failedToExchangeCode: 'Не удалось обменять auth code OpenAI', + failedToValidateRT: 'Не удалось проверить refresh token', + errors: { + OPENAI_OAUTH_PROXY_REQUIRED: + 'Proxy не настроен, и этот сервер не смог напрямую подключиться к OpenAI, поэтому запрос OpenAI OAuth не удался. Выберите proxy с доступом к OpenAI и повторите попытку; если код авторизации истёк, сгенерируйте URL авторизации заново.' + }, + // Refresh Token auth + 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', + followSteps: 'Выполните шаги для авторизации аккаунта Gemini:', + step1GenerateUrl: 'Сгенерируйте URL авторизации', + generateAuthUrl: 'Сгенерировать Auth URL', + projectIdLabel: 'Project ID (необязательно)', + projectIdPlaceholder: 'e.g. my-gcp-project or cloud-ai-companion-xxxxx', + projectIdHint: + 'Оставьте пустым для автоопределения после обмена кода. Если автоопределение не сработает, заполните поле и заново сгенерируйте auth URL.', + howToGetProjectId: 'Как получить', + step2OpenUrl: 'Откройте URL в браузере и завершите авторизацию', + openUrlDesc: + 'Откройте URL авторизации в новой вкладке, войдите в аккаунт Google и авторизуйте доступ.', + step3EnterCode: 'Введите Authorization URL или код', + authCodeDesc: + 'После авторизации скопируйте callback URL (рекомендуется) или только код и вставьте ниже.', + authCode: 'Callback URL или код', + authCodePlaceholder: + 'Вариант 1 (рекомендуется): вставьте callback URL\nВариант 2: вставьте только значение кода', + authCodeHint: 'Система автоматически извлечёт code/state из URL.', + redirectUri: 'Redirect URI', + redirectUriHint: + 'Это должно быть настроено в вашем Google OAuth client и совпадать точно.', + confirmRedirectUri: + 'Я настроил этот 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: + 'Все запросы моделей пересылаются напрямую в 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 не настроен: задайте 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 не настроен: задайте GEMINI_OAUTH_CLIENT_ID / GEMINI_OAUTH_CLIENT_SECRET и добавьте Redirect URI: http://localhost:1455/auth/callback' + }, + // Antigravity specific + antigravity: { + title: 'Авторизация аккаунта Antigravity', + followSteps: 'Выполните шаги для авторизации аккаунта Antigravity:', + step1GenerateUrl: 'Сгенерируйте URL авторизации', + generateAuthUrl: 'Сгенерировать Auth URL', + step2OpenUrl: 'Откройте URL в браузере и завершите авторизацию', + openUrlDesc: 'Откройте URL авторизации в новой вкладке, войдите в аккаунт Google и авторизуйте доступ.', + importantNotice: + 'Важно: после авторизации страница может загружаться некоторое время. Подождите. Когда адресная строка браузера покажет http://localhost..., авторизация завершена.', + step3EnterCode: 'Введите Authorization URL или код', + authCodeDesc: + 'После авторизации, когда URL страницы станет http://localhost:xxx/auth/callback?code=...:', + authCode: 'Authorization URL или код', + authCodePlaceholder: + 'Вариант 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: 'Ручной RT', + refreshTokenDesc: 'Введите существующий Antigravity Refresh Token. Поддерживается массовый ввод (по одному на строку). Система автоматически проверит их и создаст аккаунты.', + refreshTokenPlaceholder: 'Вставьте Antigravity Refresh Token...\nПоддерживается несколько токенов, по одному на строку', + validating: 'Проверка...', + validateAndCreate: 'Проверить и создать', + pleaseEnterRefreshToken: 'Введите Refresh Token', + failedToValidateRT: 'Не удалось проверить Refresh Token' + } + }, // Gemini specific (platform-wide) + gemini: { + helpButton: 'Справка', + helpDialog: { + title: 'Руководство по использованию Gemini', + apiKeySection: 'Ссылки API Key' + }, + modelPassthrough: 'Passthrough моделей Gemini', + modelPassthroughDesc: + 'Все запросы моделей пересылаются напрямую в Gemini API без ограничений моделей и mappings.', + baseUrlHint: 'Оставьте значение по умолчанию для официального Gemini API', + apiKeyHint: 'Ваш Gemini API Key (начинается с AIza)', + tier: { + label: 'Уровень аккаунта', + hint: 'Подсказка: система сначала попробует автоматически определить уровень. Если автоопределение недоступно или не сработает, выбранный уровень используется как fallback (симулированная квота).', + aiStudioHint: + 'Квоты AI Studio задаются по моделям (Pro/Flash ограничиваются независимо). Если billing включён, выберите 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: 'Авторизуйтесь через аккаунт Google и выберите тип OAuth.', + apiKeyTitle: 'API Key (AI Studio)', + apiKeyDesc: 'Самая быстрая настройка. Используйте AIza API key.', + apiKeyNote: + 'Подходит для лёгкого тестирования. Free tier имеет строгие лимиты частоты, а данные могут использоваться для обучения.', + apiKeyLink: 'Получить API Key', + quotaLink: 'Руководство по квотам' + }, + oauthType: { + 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: 'Рекомендуется', + highConcurrency: 'Высокий параллелизм', + noAdmin: 'Без настройки админом', + orgManaged: 'Управляется организацией', + adminRequired: 'Требуется админ' + } + }, + setupGuide: { + title: 'Чек-лист настройки Gemini', + checklistTitle: 'Чек-лист', + checklistItems: { + usIp: 'Используйте US IP и убедитесь, что страна аккаунта установлена как US.', + age: 'Аккаунт должен быть 18+.' + }, + activationTitle: 'Активация в один клик', + activationItems: { + geminiWeb: 'Активируйте Gemini Web, чтобы избежать ошибки User not initialized.', + gcpProject: 'Активируйте проект GCP и получите Project ID для Code Assist.' + }, + links: { + countryCheck: 'Проверить привязку страны', + geminiWebActivation: 'Активировать Gemini Web', + gcpProject: 'Открыть GCP Console' + } + }, + quotaPolicy: { + title: 'Политика квот и лимитов Gemini (справка)', + note: 'Примечание: Gemini не предоставляет официальный API для запроса квоты. Показанная здесь "Daily Quota" — оценка, симулированная системой на основе уровней аккаунта только для справки при маршрутизации. Фактические лимиты смотрите в официальных ошибках Google.', + columns: { + channel: 'Канал авторизации', + account: 'Статус аккаунта', + limits: 'Политика лимитов', + docs: 'Официальная документация' + }, + docs: { + codeAssist: 'Code Assist Quotas', + aiStudio: 'AI Studio Pricing', + vertex: 'Vertex AI Quotas' + }, + simulatedNote: 'Симулированная квота, только для справки', + 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: 'Нет подписки Code Assist', + limits: 'RPD ~1000; RPM ~60 (preview)' + }, + aiStudio: { + channel: 'AI Studio API Key / OAuth', + 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: 'Проект без billing', + paid: 'Проект с billing', + limitsFree: 'RPD 50; RPM 2 (project quota)', + limitsPaid: 'RPD unlimited; RPM 1000+ (project quota)' + } + } + }, + rateLimit: { + ok: 'Не ограничен по лимиту', + unlimited: 'Безлимитно', + limited: 'Ограничен по лимиту {time}', + now: 'сейчас' + } + }, + // Re-Auth Modal + reAuthorizeAccount: 'Повторно авторизовать аккаунт', + claudeCodeAccount: 'Аккаунт Claude Code', + openaiAccount: 'Аккаунт OpenAI', + geminiAccount: 'Аккаунт Gemini', + antigravityAccount: 'Аккаунт Antigravity', + inputMethod: 'Способ ввода', + reAuthorizedSuccess: 'Аккаунт повторно авторизован', + // Test Modal + testAccountConnection: 'Проверить подключение аккаунта', + account: 'Аккаунт', + readyToTest: 'Готово к проверке. Нажмите "Начать тест"...', + connectingToApi: 'Подключение к API...', + testCompleted: 'Тест успешно завершён!', + testFailed: 'Тест не удался', + connectedToApi: 'Подключено к API', + usingModel: 'Используется модель: {model}', + sendingTestMessage: 'Отправка тестового сообщения: "hi"', + sendingImageRequest: 'Отправка тестового запроса генерации изображения...', + response: 'Ответ:', + startTest: 'Начать тест', + testing: 'Проверка...', + retry: 'Повторить', + copyOutput: 'Скопировать вывод', + outputCopied: 'Вывод скопирован', + startingTestForAccount: 'Запуск теста для аккаунта: {name}', + testAccountTypeLabel: 'Тип аккаунта: {type}', + selectTestModel: 'Выберите тестовую модель', + testModel: 'Тестовая модель', + testPrompt: 'Prompt: "hi"', + 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: 'Когда выбрана модель изображений, этот тест отправляет настоящий запрос генерации изображения и показывает предпросмотр ниже.', + imageTestMode: 'Режим: тест изображения', + imagePreview: 'Сгенерированные изображения:', + imageReceived: 'Получено тестовое изображение #{count}', + // Stats Modal + viewStats: 'Посмотреть статистику', + usageStatistics: 'Статистика расхода', + last30DaysUsage: 'Статистика расхода за 30 дней (по фактическим дням использования)', + stats: { + totalCost: 'Общая стоимость за 30 дней', + accumulatedCost: 'Накопленная стоимость', + standardCost: 'Стандарт', + totalRequests: 'Всего запросов за 30 дней', + totalCalls: 'Всего API-вызовов', + avgDailyCost: 'Средняя дневная стоимость', + basedOnActualDays: 'На основе фактических дней использования: {days}', + avgDailyRequests: 'Среднее число запросов в день', + avgDailyUsage: 'Средний дневной расход', + todayOverview: 'Обзор за сегодня', + cost: 'Стоимость', + requests: 'Запросы', + tokens: 'Токены', + highestCostDay: 'День с максимальной стоимостью', + highestRequestDay: 'День с максимумом запросов', + date: 'Дата', + accumulatedTokens: 'Накоплено токенов', + totalTokens: 'Всего за 30 дней', + dailyAvgTokens: 'Среднее за день', + performance: 'Производительность', + avgResponseTime: 'Средний ответ', + daysActive: 'Активных дней', + recentActivity: 'Последняя активность', + todayRequests: 'Запросов сегодня', + todayTokens: 'Токенов сегодня', + todayCost: 'Расход сегодня', + usageTrend: 'Тренд стоимости и запросов за 30 дней', + noData: 'Нет данных расхода для этого аккаунта' + }, + usageWindow: { + statsTitle: 'Статистика расхода за окно 5h', + statsTitleDaily: 'Дневная статистика расхода', + 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: 'Стандарт', + basic: 'Basic', + personal: 'Personal', + unlimited: 'Безлимитно' + }, + ineligibleWarning: + 'Этот аккаунт не подходит для Antigravity, но пересылка API всё равно работает. Используйте на свой риск.', + forbidden: 'Доступ запрещён', + forbiddenValidation: 'Требуется проверка', + forbiddenViolation: 'Блокировка за нарушение', + openVerification: 'Открыть ссылку проверки', + copyLink: 'Скопировать ссылку', + linkCopied: 'Ссылка скопирована', + needsReauth: 'Требуется повторная авторизация', + rateLimited: 'Ограничен по лимиту', + usageError: 'Ошибка получения данных' + }, + + // Scheduled Tests + scheduledTests: { + title: 'Плановые проверки', + addPlan: 'Добавить план', + editPlan: 'Редактировать план', + deletePlan: 'Удалить план', + model: 'Модель', + cronExpression: 'Cron expression', + enabled: 'Включено', + lastRun: 'Последний запуск', + nextRun: 'Следующий запуск', + maxResults: 'Максимум результатов', + noPlans: 'Нет планов плановых проверок', + confirmDelete: 'Удалить этот план?', + createSuccess: 'План успешно создан', + updateSuccess: 'План успешно обновлён', + deleteSuccess: 'План успешно удалён', + results: 'Результаты проверки', + noResults: 'Результатов проверок пока нет', + responseText: 'Ответ', + errorMessage: 'Ошибка', + success: 'Успешно', + failed: 'Ошибка', + 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', + description: 'Управляйте proxy-серверами для аккаунтов', + createProxy: 'Создать proxy', + editProxy: 'Изменить proxy', + deleteProxy: 'Удалить proxy', + ad: { + inline: 'Нужен proxy IP?' + }, + dataImport: 'Импорт', + 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: 'Экспортированные данные содержат чувствительную информацию proxy. Храните их в безопасном месте.', + dataExportConfirm: 'Подтвердить экспорт', + dataExported: 'Данные экспортированы', + dataExportFailed: 'Не удалось экспортировать данные', + copyProxyUrl: 'Скопировать proxy URL', + urlCopied: 'proxy URL скопирован', + searchProxies: 'Поиск proxy...', + allProtocols: 'Все протоколы', + allStatus: 'Все статусы', + protocols: { + http: 'HTTP', + https: 'HTTPS', + socks5: 'SOCKS5', + socks5h: 'SOCKS5H (Remote DNS)' + }, + columns: { + name: 'Имя', + protocol: 'Протокол', + address: 'Адрес', + auth: 'Auth', + location: 'Локация', + status: 'Статус', + accounts: 'Аккаунты', + latency: 'Задержка', + actions: 'Действия' + }, + testConnection: 'Проверить подключение', + qualityCheck: 'Проверка качества', + batchQualityCheck: 'Массовая проверка качества', + batchTest: 'Проверить все proxy', + testFailed: 'Ошибка', + latencyFailed: 'Подключение не удалось', + batchTestEmpty: 'Нет proxy для проверки', + batchTestDone: 'Массовая проверка завершена для proxy: {count}', + batchTestFailed: 'Массовая проверка не удалась', + batchDeleteAction: 'Удалить', + batchDelete: 'Массовое удаление', + batchDeleteConfirm: 'Удалить выбранные proxy ({count})? Используемые будут пропущены.', + batchDeleteDone: 'Удалено proxy: {deleted}, пропущено {skipped}', + batchDeleteSkipped: 'Пропущено proxy: {skipped}', + batchDeleteFailed: 'Массовое удаление не удалось', + deleteBlockedInUse: 'Этот proxy используется и не может быть удалён', + accountsTitle: 'Аккаунты, использующие этот IP', + accountsEmpty: 'Ни один аккаунт не использует этот proxy', + accountsFailed: 'Не удалось загрузить список аккаунтов', + accountName: 'Аккаунт', + accountPlatform: 'Платформа', + accountNotes: 'Заметки', + name: 'Имя', + protocol: 'Протокол', + host: 'Хост', + port: 'Порт', + username: 'Имя пользователя (необязательно)', + password: 'Пароль (необязательно)', + status: 'Статус', + enterProxyName: 'Введите имя proxy', + leaveEmptyToKeep: 'Оставьте пустым, чтобы сохранить текущее значение', + optionalAuth: 'Необязательная аутентификация', + form: { + hostPlaceholder: 'proxy.example.com', + portPlaceholder: '8080' + }, + noProxiesYet: 'proxy пока нет', + createFirstProxy: 'Создайте первый proxy, чтобы маршрутизировать через него трафик.', + // Batch import + standardAdd: 'Стандартное добавление', + batchAdd: 'Быстрое добавление', + batchInput: 'Список proxy', + batchInputPlaceholder: + "Введите по одному proxy на строку в следующих форматах:\nsocks5://user:pass{'@'}192.168.1.1:1080\nhttp://192.168.1.1:8080\nhttps://user:pass{'@'}proxy.example.com:443", + batchInputHint: + "Поддерживаются протоколы 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: 'Создание...', + updating: 'Обновление...', + proxyCreated: 'proxy создан', + proxyUpdated: 'proxy обновлён', + proxyDeleted: 'proxy удалён', + proxyWorking: 'proxy работает!', + proxyWorkingWithLatency: 'proxy работает! Задержка: {latency}ms', + proxyTestFailed: 'Проверка proxy не удалась', + qualityCheckDone: 'Проверка качества завершена: оценка {score} ({grade})', + qualityCheckFailed: 'Не удалось запустить проверку качества proxy', + batchQualityDone: + 'Массовая проверка качества завершена для 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: 'Задержка', + qualityTableMessage: 'Сообщение', + qualityInline: 'Качество {grade}/{score}', + qualityStatusHealthy: 'Здоровый', + qualityStatusPass: 'Пройдено', + qualityStatusWarn: 'Предупреждение', + qualityStatusFail: 'Ошибка', + qualityStatusChallenge: 'Challenge', + qualityTargetBase: 'Базовое подключение', + failedToLoad: 'Не удалось загрузить proxy', + failedToCreate: 'Не удалось создать proxy', + failedToUpdate: 'Не удалось обновить proxy', + failedToDelete: 'Не удалось удалить proxy', + failedToTest: 'Не удалось проверить proxy', + nameRequired: 'Введите имя proxy', + hostRequired: 'Введите адрес хоста', + portInvalid: 'Порт должен быть в диапазоне 1-65535', + deleteConfirm: + "Удалить '{name}'? У аккаунтов, использующих этот proxy, proxy будет удалён." + }, + + // Redeem Codes + redeem: { + title: 'Управление кодами активации', + description: 'Создавайте и управляйте кодами активации', + generateCodes: 'Создать коды', + searchCodes: 'Поиск по кодам или email...', + allTypes: 'Все типы', + allStatus: 'Все статусы', + balance: 'Баланс', + concurrency: 'Параллелизм', + subscription: 'Подписка', + invitation: 'Приглашение', + invitationHint: 'Коды приглашения используются для ограничения регистрации пользователей. После использования они автоматически помечаются как использованные.', + unused: 'Не использовано', + used: 'Использовано', + columns: { + code: 'Код', + type: 'Тип', + value: 'Значение', + status: 'Статус', + usedBy: 'Использовано', + usedAt: 'Дата использования', + expiresAt: 'Истекает', + actions: 'Действия' + }, + userPrefix: 'Пользователь #{id}', + exportCsv: 'Экспорт CSV', + batchUpdate: 'Массовое обновление', + batchUpdateTitle: 'Массовое обновление кодов активации', + selectedCount: 'Выбрано кодов активации: {count}', + clearSelection: 'Снять выбор', + selectCodesFirst: 'Сначала выберите коды активации', + noBatchFieldsSelected: 'Выберите хотя бы одно поле для обновления', + batchUpdateSuccess: 'Обновлено кодов активации: {count}', + failedToBatchUpdate: 'Не удалось массово обновить коды активации', + batchFields: { + status: 'Статус', + expiresAt: 'Истекает', + notes: 'Заметки', + group: 'Группа' + }, + batchNotesPlaceholder: 'Введите новую заметку или оставьте пустым, чтобы очистить её', + clearGroup: 'Очистить группу', + deleteAllUnused: 'Удалить все неиспользованные коды', + deleteCode: 'Удалить код активации', + deleteCodeConfirm: + 'Удалить этот код активации? Это действие нельзя отменить.', + deleteAllUnusedConfirm: + 'Удалить все неиспользованные (активные) коды активации? Это действие нельзя отменить.', + deleteAll: 'Удалить все', + generateCodesTitle: 'Создать коды активации', + generatedSuccessfully: 'Успешно создано', + codesCreated: 'Создано кодов активации: {count}', + codeType: 'Тип кода', + amount: 'Сумма ($)', + value: 'Значение', + count: 'Количество', + generating: 'Создание...', + generate: 'Создать', + copyAll: 'Скопировать все', + copied: 'Скопировано!', + download: 'Скачать', + codesExported: 'Коды экспортированы', + codeDeleted: 'Код активации удалён', + codesDeleted: 'Удалено неиспользованных кодов: {count}', + noUnusedCodes: 'Нет неиспользованных кодов для удаления', + failedToLoad: 'Не удалось загрузить коды активации', + failedToGenerate: 'Не удалось создать коды', + failedToExport: 'Не удалось экспортировать коды', + failedToDelete: 'Не удалось удалить код', + failedToDeleteUnused: 'Не удалось удалить неиспользованные коды', + failedToCopy: 'Не удалось скопировать коды', + types: { + balance: 'Баланс', + concurrency: 'Параллелизм', + subscription: 'Подписка', + invitation: 'Приглашение', + // Admin adjustment types (created when admin modifies user balance/concurrency) + admin_balance: 'Баланс (админ)', + admin_concurrency: 'Параллелизм (админ)' + }, + selectGroup: 'Выбрать группу', + selectGroupPlaceholder: 'Выберите группу подписки', + validityDays: 'Срок действия в днях', + codeExpiry: 'Срок действия кода', + neverExpires: 'Без срока действия', + expiryPresetDays: '{days} дн.', + customExpiry: 'Свой', + customExpiryDays: 'Свои дни', + expiryDaysRequired: 'Введите корректное количество дней срока действия', + groupRequired: 'Выберите группу подписки', + days: ' дн.', + status: { + unused: 'Не использовано', + used: 'Использовано', + expired: 'Истекла', + disabled: 'Отключено' + } + }, + + // Announcements + announcements: { + title: 'Объявления', + description: 'Создавайте объявления и настраивайте таргетинг по условиям', + createAnnouncement: 'Создать объявление', + editAnnouncement: 'Изменить объявление', + deleteAnnouncement: 'Удалить объявление', + searchAnnouncements: 'Поиск объявлений...', + status: 'Статус', + allStatus: 'Все статусы', + columns: { + title: 'Заголовок', + status: 'Статус', + notifyMode: 'Режим уведомления', + targeting: 'Таргетинг', + timeRange: 'Расписание', + createdAt: 'Создано', + actions: 'Действия' + }, + statusLabels: { + draft: 'Черновик', + active: 'Активен', + archived: 'В архиве' + }, + notifyModeLabels: { + silent: 'Тихий', + popup: 'Popup' + }, + form: { + title: 'Заголовок', + content: 'Содержимое (поддерживается Markdown)', + status: 'Статус', + notifyMode: 'Режим уведомления', + notifyModeHint: 'Режим Popup покажет пользователям всплывающее уведомление', + startsAt: 'Начало', + endsAt: 'Окончание', + startsAtHint: 'Оставьте пустым, чтобы начать сразу', + endsAtHint: 'Оставьте пустым, чтобы не ограничивать срок', + targetingMode: 'Таргетинг', + targetingAll: 'Все пользователи', + targetingCustom: 'Пользовательские правила', + addOrGroup: 'Добавить OR-группу', + addAndCondition: 'Добавить AND-условие', + conditionType: 'Тип условия', + conditionSubscription: 'Подписка', + conditionBalance: 'Баланс', + operator: 'Оператор', + balanceValue: 'Порог баланса', + selectPackages: 'Выберите пакеты' + }, + operators: { + gt: '>', + gte: '≥', + lt: '<', + lte: '≤', + eq: '=' + }, + targetingSummaryAll: 'Все пользователи', + targetingSummaryCustom: 'Пользовательские правила (групп: {groups})', + timeImmediate: 'Сразу', + timeNever: 'Никогда', + readStatus: 'Статус прочтения', + eligible: 'Подходит', + readAt: 'Прочитано', + unread: 'Непрочитано', + searchUsers: 'Поиск пользователей...', + failedToLoad: 'Не удалось загрузить объявления', + failedToCreate: 'Не удалось создать объявление', + failedToUpdate: 'Не удалось обновить объявление', + failedToDelete: 'Не удалось удалить объявление', + failedToLoadReadStatus: 'Не удалось загрузить статус прочтения', + deleteConfirm: 'Удалить это объявление? Это действие нельзя отменить.' + }, + + // Promo Codes + promo: { + title: 'Управление промокодами', + description: 'Создавайте и управляйте промокодами для регистрации', + createCode: 'Создать промокод', + editCode: 'Изменить промокод', + deleteCode: 'Удалить промокод', + searchCodes: 'Поиск промокодов...', + allStatus: 'Все статусы', + columns: { + code: 'Код', + bonusAmount: 'Бонусная сумма', + maxUses: 'Макс. использований', + usedCount: 'Использовано', + usage: 'Расход', + status: 'Статус', + expiresAt: 'Истекает', + createdAt: 'Создано', + actions: 'Действия' + }, + // Form labels (flat structure for template usage) + code: 'Промокод', + autoGenerate: 'автогенерация, если пусто', + codePlaceholder: 'Введите промокод или оставьте пустым', + bonusAmount: 'Бонусная сумма ($)', + maxUses: 'Макс. использований', + zeroUnlimited: '0 = безлимитно', + expiresAt: 'Истекает', + notes: 'Заметки', + notesPlaceholder: 'Необязательные заметки для этого кода', + status: 'Статус', + neverExpires: 'Без срока действия', + // Status labels + statusActive: 'Активен', + statusDisabled: 'Отключено', + statusExpired: 'Истекла', + statusMaxUsed: 'Использован полностью', + // Usage records + usageRecords: 'История расхода', + viewUsages: 'Посмотреть использования', + noUsages: 'Записей расхода пока нет', + userPrefix: 'Пользователь #{id}', + copied: 'Скопировано!', + // Messages + noCodesYet: 'Промокодов пока нет', + createFirstCode: 'Создайте первый промокод, чтобы выдавать бонусы за регистрацию.', + codeCreated: 'Промокод создан', + codeUpdated: 'Промокод обновлён', + codeDeleted: 'Промокод удалён', + deleteCodeConfirm: 'Удалить этот промокод? Это действие нельзя отменить.', + copyRegisterLink: 'Скопировать ссылку регистрации', + registerLinkCopied: 'Ссылка регистрации скопирована', + failedToLoad: 'Не удалось загрузить промокоды', + failedToCreate: 'Не удалось создать промокод', + failedToUpdate: 'Не удалось обновить промокод', + failedToDelete: 'Не удалось удалить промокод', + failedToLoadUsages: 'Не удалось загрузить записи расхода' + }, + + // Usage Records + usage: { + title: 'История расхода', + description: 'Просматривайте и управляйте всеми пользовательскими записями расхода', + userFilter: 'Пользователь', + searchUserPlaceholder: 'Поиск пользователя по email...', + searchApiKeyPlaceholder: 'Поиск API-ключа по имени...', + searchAccountPlaceholder: 'Поиск аккаунта по имени...', + selectedUser: 'Выбрано', + user: 'Пользователь', + account: 'Аккаунт', + group: 'Группа', + requestId: 'Request ID', + requestIdCopied: 'Request ID скопирован', + allModels: 'Все модели', + allAccounts: 'Все аккаунты', + allGroups: 'Все группы', + allTypes: 'Все типы', + inputCost: 'Стоимость входа', + outputCost: 'Стоимость выхода', + cacheCreationCost: 'Стоимость создания cache', + cacheReadCost: 'Стоимость чтения cache', + inputTokens: 'Входные токены', + outputTokens: 'Выходные токены', + cacheCreationTokens: 'Токены создания cache', + cacheCreation5mTokens: 'Запись кэша', + cacheCreation1hTokens: 'Запись кэша', + cacheReadTokens: 'Токены чтения cache', + failedToLoad: 'Не удалось загрузить записи расхода', + billingType: 'Тип списания', + allBillingTypes: 'Все типы списания', + billingTypeBalance: 'Баланс', + billingTypeSubscription: 'Подписка', + billingMode: 'Режим списания', + billingModeToken: 'Token', + billingModePerRequest: 'За запрос', + billingModeImage: 'Изображение', + allBillingModes: 'Все режимы списания', + ipAddress: 'IP', + clickToViewBalance: 'Нажмите, чтобы посмотреть историю баланса', + failedToLoadUser: 'Не удалось загрузить информацию пользователя', + cleanup: { + button: 'Очистка', + title: 'Очистка записей расхода', + warning: 'Очистка необратима и повлияет на историческую статистику.', + submit: 'Запустить очистку', + submitting: 'Отправка...', + confirmTitle: 'Подтвердите очистку', + confirmMessage: 'Отправить эту задачу очистки? Это действие нельзя отменить.', + confirmSubmit: 'Подтвердить очистку', + cancel: 'Отмена', + cancelConfirmTitle: 'Подтвердите отмену', + cancelConfirmMessage: 'Отменить эту задачу очистки?', + cancelConfirm: 'Подтвердить отмену', + cancelSuccess: 'Задача очистки отменена', + cancelFailed: 'Не удалось отменить задачу очистки', + recentTasks: 'Последние задачи очистки', + loadingTasks: 'Загрузка задач...', + noTasks: 'Задач очистки пока нет', + range: 'Диапазон', + deletedRows: 'Удалено', + missingRange: 'Выберите диапазон дат', + submitSuccess: 'Задача очистки создана', + submitFailed: 'Не удалось создать задачу очистки', + loadFailed: 'Не удалось загрузить задачи очистки', + status: { + pending: 'Ожидает', + running: 'Выполняется', + succeeded: 'Успешно', + failed: 'Ошибка', + canceled: 'Отменено' + } + } + }, + + // Ops Monitoring + ops: { + title: 'Операционный мониторинг', + description: 'Операционный мониторинг и диагностика', + // Dashboard + systemHealth: 'Состояние системы', + overview: 'Обзор', + noSystemMetrics: 'Системные метрики пока не собраны.', + collectedAt: 'Собрано:', + window: 'окно', + memory: 'Память', + db: 'DB', + goroutines: 'Goroutines', + jobs: 'Jobs', + jobsHelp: 'Нажмите «Details», чтобы посмотреть heartbeat задач и последние ошибки', + active: 'активно', + idle: 'idle', + waiting: 'waiting', + conns: 'conns', + queue: 'queue', + accountSwitches: 'Переключения аккаунтов', + ok: 'ok', + lastRun: 'last_run:', + lastSuccess: 'last_success:', + lastError: 'last_error:', + noData: 'Нет данных.', + loadingText: 'загрузка', + ready: 'готово', + requestsTotal: 'Запросы (всего)', + slaScope: 'Область SLA:', + tokens: 'Токены', + tps: 'TPS:', + current: 'текущее', + peak: 'пик', + average: 'среднее', + totalRequests: 'Всего запросов', + avgQps: 'Средний QPS', + avgTps: 'Средний TPS', + avgLatency: 'Средняя длительность запроса', + avgTtft: 'Средний TTFT', + exceptions: 'Исключения', + requestErrors: 'Ошибки запросов', + errorCount: 'Количество ошибок', + upstreamErrors: 'Ошибки upstream', + errorCountExcl429529: 'Количество ошибок (без 429/529)', + sla: 'SLA (без бизнес-лимитов)', + businessLimited: 'business_limited:', + errors: 'Ошибки', + errorRate: 'error_rate:', + upstreamRate: 'upstream_rate:', + latencyDuration: 'Длительность запроса', + ttftLabel: 'TTFT (first_token_ms)', + p50: 'p50:', + p90: 'p90:', + p95: 'p95:', + p99: 'p99:', + avg: 'avg:', + max: 'max:', + requests: 'Запросы', + requestsTitle: 'Запросы', + upstream: 'upstream', + client: 'Клиент', + system: 'Система', + other: 'Другое', + errorsSla: 'Ошибки (область SLA)', + upstreamExcl429529: 'upstream (без 429/529)', + failedToLoadData: 'Не удалось загрузить данные ops.', + failedToLoadOverview: 'Не удалось загрузить обзор', + failedToLoadThroughputTrend: 'Не удалось загрузить динамику пропускной способности', + failedToLoadSwitchTrend: 'Не удалось загрузить динамику средних переключений аккаунтов', + failedToLoadLatencyHistogram: 'Не удалось загрузить гистограмму длительности запросов', + failedToLoadErrorTrend: 'Не удалось загрузить динамику ошибок', + failedToLoadErrorDistribution: 'Не удалось загрузить распределение ошибок', + failedToLoadErrorDetail: 'Не удалось загрузить детали ошибки', + retryFailed: 'Повтор не удался', + tpsK: 'TPS (K)', + top: 'Top:', + throughputTrend: 'Динамика пропускной способности', + switchRateTrend: 'Средние переключения аккаунтов', + latencyHistogram: 'Гистограмма длительности запросов', + errorTrend: 'Динамика ошибок', + errorDistribution: 'Распределение ошибок', + switchRate: 'Средние переключения', + // Health Score & Diagnosis + health: 'Состояние', + healthCondition: 'Состояние здоровья', + healthHelp: 'Общая оценка состояния системы на основе SLA, доли ошибок и использования ресурсов', + healthyStatus: 'Здорово', + riskyStatus: 'В зоне риска', + idleStatus: 'Простой', + timeRange: { + '5m': 'Последние 5 минут', + '30m': 'Последние 30 минут', + '1h': 'Последний 1 час', + '1d': 'Последний 1 день', + '15d': 'Последние 15 дней', + '6h': 'Последние 6 часов', + '24h': 'Последние 24 часа', + '7d': 'Последние 7 дней', + '30d': 'Последние 30 дней' + }, + openaiTokenStats: { + title: 'Статистика запросов Token OpenAI', + viewModeTopN: 'TopN', + viewModePagination: 'Пагинация', + prevPage: 'Назад', + nextPage: 'Далее', + pageInfo: 'Страница {page}/{total}', + totalModels: 'Всего моделей: {total}', + failedToLoad: 'Не удалось загрузить статистику Token OpenAI', + empty: 'Нет статистики Token OpenAI для текущих фильтров', + table: { + model: 'Модель', + requestCount: 'Запросы', + avgTokensPerSec: 'Среднее Tokens/sec', + avgFirstTokenMs: 'Средняя задержка первого Token (ms)', + totalOutputTokens: 'Всего выходных токенов', + avgDurationMs: 'Средняя длительность (ms)', + requestsWithFirstToken: 'Запросы с первым Token' + } + }, + fullscreen: { + enter: 'Во весь экран' + }, + diagnosis: { + title: 'Умная диагностика', + footer: 'Автоматические диагностические рекомендации на основе текущих метрик', + idle: 'Система сейчас простаивает', + idleImpact: 'Нет активного трафика', + // Resource diagnostics + 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 критически высокая ({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 критически ниже целевого значения ({sla}%)', + slaCriticalImpact: 'Пользовательский опыт сильно ухудшен', + slaCriticalAction: 'Срочно проверьте ошибки и задержки, рассмотрите ограничение частоты', + slaLow: 'SLA ниже целевого значения ({sla}%)', + slaLowImpact: 'Качество сервиса требует внимания', + slaLowAction: 'Проанализируйте причины снижения SLA, оптимизируйте производительность системы', + // Health score diagnostics + healthCritical: 'Общая оценка состояния критически низкая ({score})', + healthCriticalImpact: 'Несколько метрик могут быть ухудшены; сначала проверьте долю ошибок и задержки', + healthCriticalAction: 'Проведите комплексную проверку системы, приоритизируйте критические проблемы', + healthLow: 'Общая оценка состояния низкая ({score})', + healthLowImpact: 'Может указывать на небольшую нестабильность; отслеживайте SLA и долю ошибок', + healthLowAction: 'Отслеживайте динамику метрик, не допускайте эскалации проблемы', + healthy: 'Все системные метрики в норме', + healthyImpact: 'Сервис работает стабильно' + }, + // Error Log + errorLog: { + timeId: 'Время / ID', + commonErrors: { + contextDeadlineExceeded: 'context deadline exceeded', + connectionRefused: 'connection refused', + rateLimit: 'rate limit' + }, + time: 'Время', + type: 'Тип', + context: 'Контекст', + platform: 'Платформа', + model: 'Модель', + group: 'Группа', + user: 'Пользователь', + userId: 'ID пользователя', + account: 'Аккаунт', + accountId: 'Account ID', + status: 'Статус', + message: 'Сообщение', + latency: 'Длительность запроса', + action: 'Действие', + noErrors: 'В этом окне ошибок нет.', + grp: 'GRP:', + acc: 'ACC:', + details: 'Детали', + phase: 'Фаза', + id: 'ID:', + typeUpstream: 'Апстрим', + typeRequest: 'Запрос', + typeAuth: 'Auth', + typeRouting: 'Маршрутизация', + typeInternal: 'Внутренняя', + endpoint: 'Endpoint', + requestType: 'Тип', + requestTypeSync: 'Sync', + requestTypeStream: 'Stream', + requestTypeWs: 'WS' + }, + // Error Details Modal + errorDetails: { + upstreamErrors: 'Ошибки upstream', + requestErrors: 'Ошибки запросов', + unresolved: 'Не решено', + resolved: 'Решено', + viewErrors: 'Ошибки', + viewExcluded: 'Исключено', + statusCodeOther: 'Другое', + owner: { + provider: 'Провайдер', + client: 'Клиент', + platform: 'Платформа' + }, + phase: { + request: 'Запрос', + auth: 'Auth', + routing: 'Маршрутизация', + upstream: 'upstream', + network: 'Сеть', + internal: 'Внутренняя' + }, + total: 'Всего:', + searchPlaceholder: 'Поиск request_id / client_request_id / message', + }, + // Error Detail Modal + errorDetail: { + title: 'Детали ошибки', + titleWithId: 'Ошибка #{id}', + noErrorSelected: 'Ошибка не выбрана.', + resolution: 'Решено:', + failedToUpdateResolvedStatus: 'Не удалось обновить статус решения', + classificationKeys: { + phase: 'Фаза', + owner: 'Владелец', + source: 'Источник', + resolvedAt: 'Решено в', + resolvedBy: 'Решил' + }, + source: { + upstream_http: 'Upstream HTTP' + }, + upstreamKeys: { + status: 'Статус', + message: 'Сообщение', + detail: 'Детали', + upstreamErrors: 'Ошибки upstream' + }, + upstreamEvent: { + account: 'Аккаунт', + status: 'Статус', + requestId: 'Request ID' + }, + responsePreview: { + expand: 'Response (нажмите, чтобы раскрыть)', + collapse: 'Response (нажмите, чтобы свернуть)' + }, + loading: 'Загрузка…', + requestId: 'Request ID', + time: 'Время', + phase: 'Фаза', + status: 'Статус', + message: 'Сообщение', + basicInfo: 'Основная информация', + platform: 'Платформа', + model: 'Модель', + group: 'Группа', + user: 'Пользователь', + account: 'Аккаунт', + latency: 'Длительность запроса', + businessLimited: 'Бизнес-лимит', + requestPath: 'Путь запроса', + inboundEndpoint: 'Входящий endpoint', + upstreamEndpoint: 'Апстрим endpoint', + requestedModel: 'Запрошенная модель', + upstreamModel: 'Модель upstream', + requestType: 'Тип запроса', + requestTypeUnknown: 'Неизвестно', + requestTypeSync: 'Sync', + requestTypeStream: 'Stream', + requestTypeWs: 'WebSocket', + modelMapping: 'Model Mapping', + timings: 'Тайминги', + auth: 'Auth', + routing: 'Маршрутизация', + upstream: 'upstream', + response: 'Response', + classification: 'Классификация', + errorBody: 'Тело ошибки', + trimmed: 'обрезано', + markResolved: 'Пометить решённой', + markUnresolved: 'Пометить нерешённой', + tabOverview: 'Обзор', + tabRequest: 'Request', + tabResponse: 'Response', + responseBody: 'Response', + compareA: 'Compare A', + compareB: 'Compare B', + suggestion: 'Рекомендация', + suggestUpstream: 'Нестабильность upstream: проверьте статус аккаунта или рассмотрите переключение аккаунтов', + suggestRequest: 'Ошибка клиентского запроса: попросите клиента исправить параметры запроса', + suggestAuth: 'Ошибка Auth: проверьте API key/credentials', + suggestPlatform: 'Ошибка платформы: приоритизируйте расследование и исправление', + suggestGeneric: 'Смотрите детали для дополнительного контекста' + }, + requestDetails: { + title: 'Детали запроса', + details: 'Детали', + rangeLabel: 'Окно: {range}', + rangeMinutes: '{n} минут', + rangeHours: '{n} часов', + empty: 'В этом окне запросов нет.', + emptyHint: 'Попробуйте другой диапазон времени или уберите фильтры.', + failedToLoad: 'Не удалось загрузить детали запроса', + requestIdCopied: 'Request ID скопирован', + copyFailed: 'Не удалось скопировать', + copy: 'Копировать', + viewError: 'Посмотреть ошибку', + kind: { + success: 'SUCCESS', + error: 'ERROR' + }, + table: { + time: 'Время', + kind: 'Тип', + platform: 'Платформа', + model: 'Модель', + duration: 'Длительность', + status: 'Статус', + requestId: 'Request ID', + actions: 'Действия' + } + }, + alertEvents: { + title: 'События оповещений', + description: 'Последние записи срабатывания/решения оповещений (только email)', + loading: 'Загрузка...', + empty: 'Событий оповещений нет', + loadFailed: 'Не удалось загрузить события оповещений', + status: { + firing: 'FIRING', + resolved: 'RESOLVED', + manualResolved: 'MANUAL RESOLVED' + }, + detail: { + title: 'Детали оповещения', + loading: 'Загрузка деталей...', + empty: 'Деталей нет', + loadFailed: 'Не удалось загрузить детали оповещения', + manualResolve: 'Пометить как решённое', + manualResolvedSuccess: 'Помечено как решённое вручную', + manualResolvedFailed: 'Не удалось пометить как решённое вручную', + silence: 'Заглушить оповещение', + silenceSuccess: 'Оповещение заглушено', + silenceFailed: 'Не удалось заглушить оповещение', + viewRule: 'Посмотреть правило', + viewLogs: 'Посмотреть логи', + firedAt: 'Сработало в', + resolvedAt: 'Решено в', + ruleId: 'Rule ID', + dimensions: 'Измерения', + historyTitle: 'История', + historyHint: 'Последние события с тем же правилом + измерениями', + historyLoading: 'Загрузка истории...', + historyEmpty: 'Истории нет' + }, + table: { + time: 'Время', + status: 'Статус', + severity: 'Важность', + platform: 'Платформа', + ruleId: 'Rule ID', + title: 'Заголовок', + duration: 'Длительность', + metric: 'Метрика / порог', + dimensions: 'Измерения', + email: 'Email отправлен', + emailSent: 'Отправлено', + emailIgnored: 'Игнорировано' + } + }, + alertRules: { + title: 'Правила оповещений', + description: 'Создавайте и управляйте системными оповещениями по порогам (только email)', + loading: 'Загрузка...', + empty: 'Правил оповещений нет', + loadFailed: 'Не удалось загрузить правила оповещений', + saveFailed: 'Не удалось сохранить правило оповещения', + saveSuccess: 'Правило оповещения сохранено', + deleteFailed: 'Не удалось удалить правило оповещения', + deleteSuccess: 'Правило оповещения удалено', + manage: 'Управлять правилами оповещений', + create: 'Создать правило', + createTitle: 'Создать правило оповещения', + editTitle: 'Изменить правило оповещения', + deleteConfirmTitle: 'Удалить это правило?', + deleteConfirmMessage: 'Это удалит правило и связанные события. Продолжить?', + metricGroups: { + system: 'Системные метрики', + group: 'Метрики уровня группы (требуется group_id)', + account: 'Метрики уровня аккаунта' + }, + metrics: { + 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: 'Доля успешных запросов в окне (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: 'Рекомендуется: оператор {operator}, порог {threshold}{unit}', + groupRequired: 'Это метрика уровня группы; выбор группы (group_id) обязателен.', + groupOptional: 'Необязательно: ограничить правило конкретной группой через group_id.' + }, + table: { + name: 'Имя', + metric: 'Метрика', + severity: 'Важность', + enabled: 'Включено', + actions: 'Действия' + }, + form: { + name: 'Имя', + description: 'Описание', + metric: 'Метрика', + operator: 'Оператор', + groupId: 'Group (group_id)', + groupPlaceholder: 'Выберите группу', + allGroups: 'Все группы', + threshold: 'Порог', + severity: 'Важность', + window: 'Окно (минуты)', + sustained: 'Устойчиво (samples)', + cooldown: 'Cooldown (минуты)', + enabled: 'Включено', + notifyEmail: 'Отправлять email-уведомления' + }, + validation: { + 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-настройки', + description: 'Хранится в базе данных; изменения вступают в силу без редактирования config-файлов.', + loading: 'Загрузка...', + noData: 'runtime-настройки недоступны', + loadFailed: 'Не удалось загрузить runtime-настройки', + saveSuccess: 'runtime-настройки сохранены', + saveFailed: 'Не удалось сохранить runtime-настройки', + alertTitle: 'Оценщик оповещений', + groupAvailabilityTitle: 'Монитор доступности групп', + evalIntervalSeconds: 'Интервал оценки (секунды)', + silencing: { + title: 'Заглушение оповещений (режим обслуживания)', + enabled: 'Включить заглушение', + globalUntil: 'Заглушить до (RFC3339)', + untilHint: 'Оставьте пустым, чтобы только переключить заглушение без срока действия (не рекомендуется).', + reason: 'Причина', + reasonPlaceholder: 'например, плановое обслуживание', + entries: { + title: 'Дополнительно: точечное заглушение', + hint: 'Необязательно: заглушить только отдельные правила или уровни важности. Оставьте поля пустыми, чтобы применить ко всем.', + add: 'Добавить запись', + empty: 'Точечных записей нет', + entryTitle: 'Запись #{n}', + ruleId: 'Rule ID (необязательно)', + ruleIdPlaceholder: 'e.g., 1', + severities: 'Уровни важности (необязательно)', + severitiesPlaceholder: 'например, P0,P1 (пусто = все)', + until: 'До (RFC3339)', + reason: 'Причина', + validation: { + untilRequired: 'Время окончания записи обязательно', + untilFormat: 'Время окончания записи должно быть корректным timestamp RFC3339', + ruleIdPositive: 'rule_id записи должен быть положительным целым числом', + severitiesFormat: 'Уровни важности записи должны быть списком P0..P3 через запятую' + } + }, + validation: { + timeFormat: 'Время заглушения должно быть корректным timestamp RFC3339' + } + }, + lockEnabled: 'Распределённая блокировка включена', + lockKey: 'Ключ распределённой блокировки', + lockTTLSeconds: 'TTL распределённой блокировки (секунды)', + showAdvancedDeveloperSettings: 'Показать расширенные настройки разработчика (распределённая блокировка)', + advancedSettingsSummary: 'Расширенные настройки (распределённая блокировка)', + evalIntervalHint: 'Как часто запускается оценщик. Рекомендуется оставить значение по умолчанию.', + validation: { + 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-уведомления', + description: 'Настройте email-оповещения и email-отчёты (хранятся в базе данных).', + loading: 'Загрузка...', + 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: 'Исправьте следующие проблемы', + 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-мониторинга', + loadFailed: 'Не удалось загрузить настройки', + saveSuccess: 'Настройки ops-мониторинга сохранены', + saveFailed: 'Не удалось сохранить настройки', + dataCollection: 'Сбор данных', + evaluationInterval: 'Интервал оценки (секунды)', + evaluationIntervalHint: 'Частота задач обнаружения; рекомендуется оставить значение по умолчанию', + alertConfig: 'Настройка оповещений', + enableAlert: 'Включить оповещения', + alertRecipients: 'Email получателей оповещений', + emailPlaceholder: 'Введите email', + 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: 'Автоматически обновлять данные 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: 'Исправьте следующие проблемы', + retentionDaysRange: 'Дни хранения должны быть от 0 до 365 (0 = удалять всё при каждой очистке)', + slaMinPercentRange: 'Минимальный процент SLA должен быть от 0 до 100', + ttftP99MaxRange: 'Максимум TTFT P99 должен быть числом ≥ 0', + requestErrorRateMaxRange: 'Максимальная доля ошибок запросов должна быть от 0 до 100', + upstreamErrorRateMaxRange: 'Максимальная доля ошибок upstream должна быть от 0 до 100' + } + }, + concurrency: { + title: 'Параллелизм / очередь', + byPlatform: 'По платформе', + byGroup: 'По группе', + byAccount: 'По аккаунту', + byUser: 'По пользователю', + showByUserTooltip: 'Переключитесь на вид по пользователям, чтобы увидеть использование параллелизма по каждому пользователю', + switchToUser: 'Переключиться на вид по пользователям', + switchToPlatform: 'Переключиться на вид по платформам', + totalRows: 'Строк: {count}', + disabledHint: 'Realtime-мониторинг отключён в настройках.', + empty: 'Нет данных', + queued: 'Очередь {count}', + rateLimited: 'Ограничено по лимиту {count}', + errorAccounts: 'Ошибки {count}', + loadFailed: 'Не удалось загрузить данные параллелизма' + }, + realtime: { + title: 'Realtime', + connected: 'Realtime подключён', + connecting: 'Realtime подключается', + reconnecting: 'Realtime переподключается', + offline: 'Realtime offline', + closed: 'Realtime закрыт', + reconnectIn: 'повтор через {seconds}с' + }, + queryMode: { + auto: 'Auto', + raw: 'Raw', + preagg: 'Preagg' + }, + accountAvailability: { + available: 'Доступно', + unavailable: 'Недоступно', + accountError: 'Ошибка' + }, + tooltips: { + totalRequests: 'Общее количество запросов (успешных и неуспешных) в выбранном временном окне.', + throughputTrend: 'Requests/QPS + Tokens/TPS в выбранном окне.', + switchRateTrend: 'Динамика переключений аккаунтов / всего запросов за последние 5 часов (средние переключения).', + latencyHistogram: 'Распределение длительности запросов (ms) для успешных запросов.', + errorTrend: 'Количество ошибок во времени (область SLA исключает бизнес-лимиты; upstream исключает 429/529).', + errorDistribution: 'Распределение ошибок по status code (область SLA, без бизнес-лимитов).', + goroutines: + 'Количество 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: 'В этом окне запросов нет.', + emptyError: 'В этом окне ошибок нет.', + resetZoom: 'Сбросить', + resetZoomHint: 'Сбросить масштаб (если включён)', + downloadChart: 'Скачать', + downloadChartHint: 'Скачать график как изображение' + } + }, + + // Settings + settings: { + title: 'Настройки системы', + description: 'Управляйте регистрацией, подтверждением email, значениями по умолчанию и настройками SMTP', + tabs: { + general: 'Общие', + agreement: 'Соглашение', + features: 'Переключатели функций', + security: 'Безопасность', + users: 'Пользователи', + gateway: 'Шлюз', + email: 'Email', + backup: 'Бэкап', + payment: 'Оплата', + }, + features: { + channelMonitor: { + title: 'Монитор каналов', + description: 'Периодически проверяет настроенные каналы и показывает пользователям доступность / задержку. Отключение остановит планировщик и вернёт пустой список на пользовательской странице.', + configureLink: 'Настройте мониторы в Управление каналами > Channel Monitor', + enabled: 'Включить монитор каналов', + enabledHint: 'Отключение остановит фоновые проверки; существующая история сохранится.', + defaultInterval: 'Интервал проверки по умолчанию (секунды)', + defaultIntervalHint: 'Подставляется при создании нового монитора; каждый монитор может переопределить значение. Диапазон 15–3600.', + }, + availableChannels: { + title: 'Доступные каналы', + description: 'Показывает вошедшим пользователям агрегированный вид доступных им каналов, моделей и цен. По умолчанию отключено.', + configureLink: 'Настройте цены моделей в Управление каналами > Channel Pricing', + enabled: 'Включить Доступные каналы', + enabledHint: 'Когда отключено, пункт бокового меню скрыт, а endpoint возвращает пустой список.', + }, + riskControl: { + title: 'Риск-контроль', + description: 'Включает меню модерации контента и точку входа аудита шлюза. По умолчанию отключено.', + configureLink: 'Настройте модерацию контента в Контроль рисков', + enabled: 'Включить Контроль рисков', + enabledHint: 'Когда отключено, пункт в админском боковом меню скрыт, а модерация шлюза пропускается.', + }, + affiliate: { + title: 'Партнёрская программа (бонус за приглашение)', + description: 'Существующие пользователи приглашают новых; пригласивший получает процентное вознаграждение с пополнений приглашённого. По умолчанию отключено.', + enabled: 'Включить партнёрскую программу', + enabledHint: 'Когда отключено, меню партнёрской программы скрыто, параметр aff при регистрации игнорируется, а новые пополнения не создают вознаграждение. Уже накопленные вознаграждения всё ещё можно переводить.', + rebateRate: 'Глобальная ставка вознаграждения', + rebateRateHint: 'Процент по умолчанию, возвращаемый пригласившему с пополнений (0-100, например 10 = 10%).', + freezeHours: 'Период заморозки вознаграждения (часы)', + freezeHoursDesc: 'Новые вознаграждения будут заморожены на этот период перед доступом к выводу. 0 = без заморозки.', + durationDays: 'Срок действия вознаграждения (дни)', + durationDaysDesc: 'Связь вознаграждения истекает через это число дней после регистрации приглашённого. 0 = навсегда.', + perInviteeCap: 'Лимит вознаграждения на приглашённого', + perInviteeCapDesc: 'Максимальное суммарное вознаграждение от одного приглашённого. 0 = без лимита.', + customUsers: { + title: 'Индивидуальные настройки пользователей', + description: 'Задайте пользовательский код приглашения или эксклюзивную ставку вознаграждения для отдельных пользователей. В списке только пользователи с индивидуальными настройками.', + addButton: 'Добавить пользователя', + searchPlaceholder: 'Поиск по email или имени пользователя', + batchButton: 'Массово задать ставку (выбрано {count})', + empty: 'Пользователей с индивидуальными настройками партнёрской программы пока нет', + customBadge: 'индивидуально', + useGlobal: 'использовать глобальную', + resetTitle: 'Сбросить индивидуальные настройки', + resetMessage: 'Сбросить все индивидуальные настройки для {email}?\n• Эксклюзивная ставка вознаграждения будет очищена (возврат к глобальной ставке)\n• Код приглашения будет заново создан как новый системный код (ранее отправленные ссылки перестанут работать)', + totalLabel: 'Всего: {total}', + col: { + email: 'Email', + username: 'Имя пользователя', + code: 'Код приглашения', + rate: 'Индивидуальная ставка', + actions: 'Действия', + }, + }, + modal: { + addTitle: 'Добавить пользователя', + editTitle: 'Изменить индивидуальные настройки', + userLabel: 'Пользователь', + userPlaceholder: 'Search by email or username', + changeUser: 'Сменить пользователя', + codeLabel: 'Пользовательский код приглашения (необязательно)', + codePlaceholder: 'e.g. VIP2026', + codeHint: '4–32 символа; A-Z, 0-9, подчёркивание, дефис. Оставьте пустым, чтобы сохранить текущее значение. Ввод приводится к верхнему регистру.', + rateLabel: 'Эксклюзивная ставка вознаграждения (необязательно)', + ratePlaceholder: 'e.g. 30', + rateHint: '0-100. Оставьте пустым (в режиме редактирования), чтобы очистить и вернуться к глобальной ставке.', + errorBadRate: 'Введите число от 0 до 100', + errorEmpty: 'Заполните хотя бы одно поле: пользовательский код приглашения или эксклюзивную ставку вознаграждения', + }, + batchModal: { + title: 'Массово задать ставку (выбрано пользователей: {count})', + hint: 'Применить одинаковую эксклюзивную ставку вознаграждения ко всем выбранным пользователям.', + placeholder: 'e.g. 30', + clearHint: 'Отправка пустого значения очистит эксклюзивную ставку для выбранных пользователей.', + }, + }, + }, + emailTabDisabledTitle: 'Подтверждение email не включено', + emailTabDisabledHint: 'Включите подтверждение email на вкладке Безопасность, чтобы настроить SMTP.', + registration: { + title: 'Настройки регистрации', + description: 'Управляйте регистрацией и проверкой пользователей', + enableRegistration: 'Включить регистрацию', + enableRegistrationHint: 'Разрешить новым пользователям регистрироваться', + emailVerification: 'Подтверждение email', + emailVerificationHint: 'Требовать подтверждение email для новых регистраций', + emailSuffixWhitelist: 'Whitelist доменов email', + emailSuffixWhitelistHint: + "Регистрироваться могут только email-адреса из указанных доменов (например, {'@'}qq.com, {'@'}gmail.com, *.edu.cn)", + emailSuffixWhitelistPlaceholder: "{'@'}example.com, *.edu.cn", + emailSuffixWhitelistInputHint: 'Оставьте пустым без ограничений. Используйте *.edu.cn, чтобы сопоставлять edu.cn и его поддомены.', + promoCode: 'Промокод', + promoCodeHint: 'Разрешить пользователям применять промокоды при регистрации', + invitationCode: 'Регистрация по коду приглашения', + invitationCodeHint: 'Когда включено, пользователи должны ввести действительный код приглашения для регистрации', + passwordReset: 'Сброс пароля', + passwordResetHint: 'Разрешить пользователям сбрасывать пароль через email', + frontendUrl: 'Frontend URL', + frontendUrlPlaceholder: 'https://example.com', + frontendUrlHint: 'Используется для генерации ссылок сброса пароля в email. Пример: https://example.com', + totp: 'Двухфакторная аутентификация (2FA)', + totpHint: 'Разрешить пользователям использовать приложения-аутентификаторы, например Google Authenticator', + totpKeyNotConfigured: + 'Сначала настройте TOTP_ENCRYPTION_KEY в переменных окружения. Сгенерируйте ключ командой: openssl rand -hex 32' + }, + turnstile: { + title: 'Cloudflare Turnstile', + description: 'Защита от ботов для входа и регистрации', + enableTurnstile: 'Включить Turnstile', + enableTurnstileHint: 'Требовать проверку Cloudflare Turnstile', + siteKey: 'Site Key', + secretKey: 'Secret Key', + siteKeyHint: 'Получите это в Cloudflare Dashboard', + cloudflareDashboard: 'Cloudflare Dashboard', + secretKeyHint: 'Ключ server-side проверки (храните в секрете)', + secretKeyConfiguredHint: 'Secret key настроен. Оставьте пустым, чтобы сохранить текущее значение.' + }, + apiKeyAcl: { + title: 'Контроль доступа API-ключей по IP', + description: 'Выберите, какой IP клиента используется в allowlist/denylist API Key', + trustForwardedIp: 'Доверять forwarded client IP', + trustForwardedIpHint: + 'По умолчанию отключено. Включайте только когда origin доступен только через Cloudflare или reverse proxy Nginx. Когда включено, allowlist и denylist API Key используют CF-Connecting-IP, X-Real-IP или X-Forwarded-For, совпадая с IP запроса в записях расхода.' + }, + linuxdo: { + title: 'Вход через LinuxDo Connect', + description: 'Настройте LinuxDo Connect OAuth для входа конечных пользователей Sub2API', + enable: 'Включить вход через LinuxDo', + enableHint: 'Показывать вход через LinuxDo на страницах входа/регистрации', + clientId: 'Client ID', + clientIdPlaceholder: 'e.g., hprJ5pC3...', + clientIdHint: 'Получите это в Connect.Linux.Do', + clientSecret: 'Client Secret', + clientSecretPlaceholder: '********', + clientSecretHint: 'Используется backend для обмена токенов (храните в секрете)', + clientSecretConfiguredPlaceholder: '********', + clientSecretConfiguredHint: 'Secret настроен. Оставьте пустым, чтобы сохранить текущее значение.', + redirectUrl: 'Redirect URL', + redirectUrlPlaceholder: 'https://your-domain.com/api/v1/auth/oauth/linuxdo/callback', + redirectUrlHint: + 'Должен совпадать с redirect URL, настроенным в Connect.Linux.Do (должен быть абсолютным http(s) URL)', + quickSetCopy: 'Сгенерировать и скопировать (текущий сайт)', + redirectUrlSetAndCopied: 'Redirect URL сгенерирован и скопирован в буфер обмена' + }, + dingtalk: { + title: 'Вход через DingTalk', + description: 'Настройте DingTalk OAuth для входа конечных пользователей Sub2API', + enable: 'Включить вход через DingTalk (внутреннее корпоративное приложение)', + enableHint: 'Показывать вход через DingTalk на страницах входа/регистрации', + clientId: 'Client ID (AppKey)', + clientIdPlaceholder: 'e.g., dingxxxxxxxxxxxxxxxx', + clientIdHint: 'Получите это в деталях приложения DingTalk Open Platform', + clientSecret: 'Client Secret (AppSecret)', + clientSecretPlaceholder: '********', + clientSecretHint: 'Используется backend для обмена токенов (храните в секрете)', + clientSecretConfiguredPlaceholder: '********', + clientSecretConfiguredHint: 'Secret настроен. Оставьте пустым, чтобы сохранить текущее значение.', + redirectUrl: 'Redirect URL', + redirectUrlPlaceholder: 'https://your-domain.com/api/v1/auth/oauth/dingtalk/callback', + redirectUrlHint: + 'Должен совпадать с redirect URL, настроенным в DingTalk Open Platform (должен быть абсолютным http(s) URL)', + corpPolicy: { + label: 'Политика ограничения организаций', + hint: 'Управляет тем, какие аккаунты DingTalk (организации) могут входить', + none: 'Без ограничений (разрешены все аккаунты DingTalk)', + internalOnly: 'Только внутренняя организация (одна corp)' + }, + 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', + description: 'Настройте стандартного провайдера OIDC (например Keycloak)', + enable: 'Включить вход через OIDC', + enableHint: 'Показывать вход через OIDC на страницах входа/регистрации', + providerName: 'Имя провайдера', + providerNamePlaceholder: 'например Keycloak', + clientId: 'Client ID', + clientIdPlaceholder: 'OIDC client id', + clientSecret: 'Client Secret', + clientSecretPlaceholder: '********', + clientSecretHint: 'Используется backend для обмена токенов (храните в секрете)', + clientSecretConfiguredPlaceholder: '********', + clientSecretConfiguredHint: 'Secret настроен. Оставьте пустым, чтобы сохранить текущее значение.', + issuerUrl: 'Issuer URL', + issuerUrlPlaceholder: 'https://id.example.com/realms/main', + discoveryUrl: 'Discovery URL', + discoveryUrlPlaceholder: 'Необязательно, оставьте пустым для автоопределения из issuer', + authorizeUrl: 'Authorize URL', + authorizeUrlPlaceholder: 'Необязательно, может быть найдено автоматически', + tokenUrl: 'Token URL', + tokenUrlPlaceholder: 'Необязательно, может быть найдено автоматически', + userinfoUrl: 'UserInfo URL', + userinfoUrlPlaceholder: 'Необязательно, может быть найдено автоматически', + jwksUrl: 'JWKS URL', + jwksUrlPlaceholder: 'Необязательно, требуется при включённой строгой проверке ID token', + scopes: 'Scopes', + scopesPlaceholder: 'openid email profile', + scopesHint: 'Должно включать openid', + redirectUrl: 'Backend Redirect URL', + redirectUrlPlaceholder: 'https://your-domain.com/api/v1/auth/oauth/oidc/callback', + redirectUrlHint: 'Должен совпадать с callback URL, настроенным у OIDC-провайдера', + quickSetCopy: 'Сгенерировать и скопировать (текущий сайт)', + redirectUrlSetAndCopied: 'Redirect URL сгенерирован и скопирован в буфер обмена', + frontendRedirectUrl: 'Frontend Callback Path', + frontendRedirectUrlPlaceholder: '/auth/oidc/callback', + frontendRedirectUrlHint: 'Frontend-маршрут, используемый после backend callback', + tokenAuthMethod: 'Token Auth Method', + clockSkewSeconds: 'Clock Skew (seconds)', + allowedSigningAlgs: 'Allowed Signing Algs', + allowedSigningAlgsPlaceholder: 'RS256,ES256,PS256', + usePkce: 'Использовать PKCE', + validateIdToken: 'Проверять ID Token', + requireEmailVerified: 'Требовать подтверждённый email', + 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: 'Настройки пользователя по умолчанию', + description: 'Значения по умолчанию для новых пользователей', + defaultBalance: 'Баланс по умолчанию', + defaultBalanceHint: 'Начальный баланс для новых пользователей', + affiliateRebateRate: 'Ставка партнёрского вознаграждения', + affiliateRebateRateHint: + 'Процент вознаграждения, начисляемый пригласившему после пополнения (0-100%, например 10 означает 10%)', + defaultConcurrency: 'Параллелизм по умолчанию', + defaultConcurrencyHint: 'Максимум одновременных запросов для новых пользователей', + defaultUserRpmLimit: 'Лимит RPM пользователя по умолчанию', + defaultUserRpmLimitHint: 'Максимум запросов в минуту по умолчанию для новых пользователей; 0 = безлимитно. Применяется только при создании нового пользователя.', + defaultSubscriptions: 'Подписки по умолчанию', + defaultSubscriptionsHint: 'Автоматически назначать эти подписки при создании или регистрации нового пользователя', + addDefaultSubscription: 'Добавить подписку по умолчанию', + defaultSubscriptionsEmpty: 'Подписки по умолчанию не настроены.', + defaultSubscriptionsDuplicate: + 'Дублирующаяся группа подписки: {groupId}. Каждая группа может быть указана только один раз.', + subscriptionGroup: 'Группа подписки', + subscriptionValidityDays: 'Срок действия (дни)', + defaultPlatformQuotas: 'Квоты платформ по умолчанию (при регистрации)', + defaultPlatformQuotasHint: 'Автоматически назначаются новым пользователям при регистрации; существующих пользователей не затрагивает. Пусто = безлимитно.', + platformQuotaNotice: 'Месячная квота использует 30-дневное скользящее окно, а не календарный месяц.', + }, + platformQuota: { + platform: 'Платформа', + daily: 'Дневная (USD)', + weekly: 'Недельная (USD)', + monthly: 'Месячная (USD, 30d rolling)', + placeholder: 'Безлимитно', + }, + claudeCode: { + title: 'Настройки Claude Code', + description: 'Управляйте требованиями доступа клиента Claude Code', + minVersion: 'Минимальная версия', + minVersionPlaceholder: 'e.g. 2.1.63', + minVersionHint: + 'Отклонять клиентов Claude Code ниже этой версии (формат semver). Оставьте пустым, чтобы отключить проверку версии.', + maxVersion: 'Максимальная версия', + maxVersionPlaceholder: 'e.g. 2.5.0', + maxVersionHint: + 'Отклонять клиентов Claude Code выше этой версии (формат semver). Оставьте пустым, чтобы разрешить любую версию.' + }, + scheduling: { + title: 'Настройки маршрутизации шлюза', + description: 'Управляйте поведением маршрутизации API Key', + allowUngroupedKey: 'Разрешить маршрутизацию ключей без группы', + allowUngroupedKeyHint: 'Когда отключено, API Keys без назначенной группы не могут выполнять запросы (403 Forbidden). Оставьте отключённым, чтобы все Keys принадлежали конкретной группе.' + }, + gatewayForwarding: { + title: 'Пересылка запросов', + description: 'Управляйте тем, как запросы пересылаются в upstream OAuth-аккаунты', + fingerprintUnification: 'Унификация fingerprint', + fingerprintUnificationHint: 'Унифицировать заголовки X-Stainless-* для пользователей, использующих один OAuth-аккаунт. При отключении исходные заголовки каждого клиента передаются как есть.', + metadataPassthrough: 'Passthrough metadata', + metadataPassthroughHint: 'Передавать исходный metadata.user_id клиента без перезаписи. Может улучшить долю попаданий в upstream cache.', + cchSigning: 'CCH Signing', + 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: 'Оставьте пустым, чтобы использовать 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: 'Используется для обхода Cloudflare browser-UA challenges на OpenAI upstream. Применяется только когда клиентский User-Agent определяется как браузер (Mozilla/...). Оставьте пустым, чтобы использовать встроенное значение по умолчанию.', + }, + webSearchEmulation: { + title: 'Эмуляция Web Search', + description: 'Внедряет возможность Web Search для аккаунтов Anthropic API Key, которые не поддерживают её нативно', + enabled: 'Включить эмуляцию Web Search', + enabledHint: 'Глобальный переключатель. Когда отключено, эмуляция Web Search неактивна для всех каналов и аккаунтов.', + providers: 'Search-провайдеры', + addProvider: 'Добавить провайдера', + providerType: 'Тип провайдера', + apiKey: 'API-ключ', + apiKeyPlaceholder: 'Введите API Key', + apiKeyConfigured: 'Настроено', + showApiKey: 'Показать', + hideApiKey: 'Скрыть', + copyApiKey: 'Копировать', + copied: 'Скопировано', + quotaLimit: 'Лимит квоты', + quotaLimitHint: 'Оставьте пустым для безлимита; если задано, должно быть > 0', + quotaLimitMustBePositive: 'Лимит квоты должен быть больше 0', + subscribedAt: 'Дата подписки', + subscribedAtHint: 'Квота сбрасывается ежемесячно от этой даты; оставьте пустым, чтобы отключить автосброс', + quotaUsage: 'Расход', + resetUsage: 'Сбросить', + resetUsageConfirm: 'Сбросить счётчик расхода для этого провайдера?', + resetUsageSuccess: 'Счётчик расхода сброшен', + proxy: 'Proxy', + removeProvider: 'Удалить', + noProviders: 'Search-провайдеры не настроены', + test: 'Проверить', + testDefaultQuery: 'Главные мировые события этого года', + testing: 'Поиск...', + testResultTitle: 'Результаты поиска', + testResultProvider: 'Провайдер', + testNoResults: 'Результаты не найдены', + }, + site: { + title: 'Настройки сайта', + description: 'Настройте брендинг сайта', + backendMode: 'Backend Mode', + backendModeDescription: + 'Отключает регистрацию пользователей, публичный сайт и функции самообслуживания. Только администратор может входить и управлять платформой.', + siteName: 'Название сайта', + siteNamePlaceholder: 'Sub2API', + siteNameHint: 'Отображается в email и заголовках страниц', + siteSubtitle: 'Подзаголовок сайта', + siteSubtitlePlaceholder: 'Платформа преобразования подписок в API', + siteSubtitleHint: 'Отображается на страницах входа и регистрации', + apiBaseUrl: 'API Base URL', + apiBaseUrlPlaceholder: 'https://api.example.com', + apiBaseUrlHint: + 'Используется для функций "Использовать ключ" и "Import to CC Switch". Оставьте пустым, чтобы использовать URL текущего сайта.', + tablePreferencesTitle: 'Глобальные настройки таблиц', + tablePreferencesDescription: 'Настройте поведение пагинации по умолчанию для общих компонентов таблиц', + tableDefaultPageSize: 'Строк на странице по умолчанию', + tableDefaultPageSizeHint: 'Должно быть целым числом от 5 до 1000', + tablePageSizeOptions: 'Варианты строк на странице', + tablePageSizeOptionsPlaceholder: '10, 20, 50, 100', + tablePageSizeOptionsHint: 'Разделяйте запятыми целые числа от 5 до 1000; при сохранении значения дедуплицируются и сортируются', + tableDefaultPageSizeRangeError: 'Количество строк на странице по умолчанию должно быть от {min} до {max}', + tablePageSizeOptionsFormatError: 'Неверный формат вариантов. Введите целые числа от {min} до {max}, разделённые запятыми', + customEndpoints: { + title: 'Дополнительные API endpoint-ы', + description: 'Добавьте дополнительные URL API endpoint-ов, чтобы пользователи могли быстро копировать их на странице API Keys', + itemLabel: 'Endpoint #{n}', + name: 'Имя', + namePlaceholder: 'например OpenAI Compatible', + endpointUrl: 'Endpoint URL', + endpointUrlPlaceholder: 'https://api2.example.com', + descriptionLabel: 'Описание', + descriptionPlaceholder: 'например поддерживает запросы в формате OpenAI', + add: 'Добавить endpoint', + }, + contactInfo: 'Контактная информация', + contactInfoPlaceholder: 'e.g., QQ: 123456789', + contactInfoHint: 'Контактная информация поддержки, отображается на странице активации кодов, в профиле и т. д.', + docUrl: 'URL документации', + docUrlPlaceholder: 'https://docs.example.com', + docUrlHint: 'Ссылка на сайт документации. Оставьте пустым, чтобы скрыть ссылку на документацию.', + siteLogo: 'Логотип сайта', + uploadImage: 'Загрузить изображение', + remove: 'Удалить', + 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: 'Страница пополнения / подписки', + description: 'Показывает пункт "Пополнение / подписка" в боковом меню и открывает настроенный URL в iframe', + enabled: 'Показывать пункт пополнения / подписки', + enabledHint: 'Показывается только в стандартном режиме (не в простом режиме)', + url: 'URL пополнения / подписки', + urlPlaceholder: 'https://example.com/purchase', + urlHint: 'Должен быть абсолютным http(s) URL', + iframeWarning: + '⚠️ Примечание об iframe: некоторые сайты блокируют встраивание через X-Frame-Options или CSP (frame-ancestors). Если страница пустая, предоставьте альтернативу "Открыть в новой вкладке".', + integrationDoc: 'Документация по интеграции оплаты', + integrationDocHint: 'Описывает спецификации endpoint-ов, семантику идемпотентности и примеры кода' + }, + soraClient: { + title: 'Клиент Sora', + description: 'Управляет отображением пункта клиента Sora в боковом меню', + enabled: 'Включить клиент Sora', + enabledHint: 'Когда включено, пункт Sora будет показан в боковом меню, чтобы пользователи могли получить доступ к функциям Sora' + }, + customMenu: { + title: 'Пользовательские страницы меню', + description: 'Добавьте пользовательские iframe-страницы в навигацию бокового меню. Каждая страница может быть видна обычным пользователям или администраторам.', + itemLabel: 'Пункт меню #{n}', + name: 'Название меню', + namePlaceholder: 'например Help Center', + url: 'URL страницы', + urlPlaceholder: 'https://example.com/page', + iconSvg: 'SVG Icon', + iconSvgPlaceholder: '...', + iconPreview: 'Предпросмотр иконки', + uploadSvg: 'Загрузить SVG', + removeSvg: 'Удалить', + visibility: 'Видно для', + visibilityUser: 'Обычные пользователи', + visibilityAdmin: 'Администраторы', + add: 'Добавить пункт меню', + remove: 'Удалить', + moveUp: 'Переместить вверх', + moveDown: 'Переместить вниз', + }, + payment: { + title: 'Настройки оплаты', + description: 'Настройте параметры системы оплаты', + configGuide: 'Руководство по настройке', + enabled: 'Включить оплату', + enabledHint: 'Включить или отключить систему оплаты', + enabledPaymentTypes: 'Включённые провайдеры', + enabledPaymentTypesHint: 'Отключение провайдера также отключит его экземпляры.', + findProvider: 'Ищете подходящего провайдера EasyPay?', + minAmount: 'Минимальная сумма', + maxAmount: 'Максимальная сумма', + dailyLimit: 'Дневной лимит', + 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: 'тип отключён', + enableTypesFirst: 'Сначала включите хотя бы один тип оплаты выше', + easypayRedirect: 'Redirect', + paymentMode: 'Режим оплаты', + modeRedirect: 'Redirect', + modeQRCode: 'QR Code', + modePopup: 'Popup', + validationNameRequired: 'Имя провайдера обязательно', + validationTypesRequired: 'Выберите хотя бы один поддерживаемый тип оплаты', + validationFieldRequired: '{field} обязательно', + 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: 'Серийный номер сертификата', + field_h5AppName: 'H5 App Name', + field_h5AppUrl: 'H5 App URL', + 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: 'Код страны/региона', + field_currency: 'Валюта оплаты', + field_accountId: 'Airwallex Account ID', + 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: 'Настройте следующий 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: 'Наименьшая дневная сумма', + providerManagement: 'Управление провайдерами', + providerManagementDesc: 'Управляйте экземплярами платёжных провайдеров', + createProvider: 'Добавить провайдера', + editProvider: 'Редактировать провайдера', + deleteProvider: 'Удалить провайдера', + deleteProviderConfirm: 'Удалить этого провайдера?', + providerName: 'Имя провайдера', + providerKey: 'Тип провайдера', + selectProviderKey: 'Выберите тип провайдера', + providerConfig: 'Учётные данные', + paymentGuideTrigger: 'Посмотреть руководство по оплате', + guideOpenLabel: 'Включить: ', + guideCallLabel: 'Вызов: ', + guideFallbackLabel: 'Fallback: ', + 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: 'Включите оплату через 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: 'Отправлять email-уведомление, когда баланс пользователя опускается ниже порога', + enabled: 'Включить уведомление о низком балансе', + threshold: 'Порог по умолчанию', + thresholdHint: 'Используется, когда пользователь не задал индивидуальное значение', + thresholdPlaceholder: 'Введите сумму', + rechargeUrl: 'URL страницы пополнения', + rechargeUrlPlaceholder: 'https://example.com/payment', + rechargeUrlHint: 'Если задано, в email появится кнопка пополнения', + }, + quotaNotify: { + title: 'Уведомление о квоте аккаунта', + description: 'Уведомлять администраторов, когда расход квоты аккаунта достигает порога оповещения', + enabled: 'Включить уведомление о квоте аккаунта', + emails: 'Email для уведомлений', + emailsHint: 'Оставьте пустым, чтобы отключить уведомления', + addEmail: 'Добавить email', + emailPlaceholder: 'Введите email', + }, + subscriptionExpiryNotify: { + title: 'Напоминание об истечении подписки', + description: 'Управляет тем, получают ли пользователи email-напоминания об истечении подписки.', + enabled: 'Включить напоминание об истечении подписки', + enabledHint: 'Когда включено, система отправляет напоминания за 7, 3 и 1 день до истечения.' + }, + smtp: { + title: 'Настройки SMTP', + description: 'Настройте отправку email для кодов подтверждения', + testConnection: 'Проверить подключение', + 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: 'Оставьте пустым, чтобы сохранить существующий пароль', + passwordConfiguredPlaceholder: '********', + passwordConfiguredHint: 'Пароль настроен. Оставьте пустым, чтобы сохранить текущее значение.', + fromEmail: 'Email отправителя', + fromEmailPlaceholder: "noreply{'@'}example.com", + fromName: 'Имя отправителя', + fromNamePlaceholder: 'Sub2API', + useTls: 'Использовать TLS', + useTlsHint: 'Включить TLS-шифрование для SMTP-соединения' + }, + testEmail: { + title: 'Отправить тестовый email', + description: 'Отправьте тестовый email, чтобы проверить конфигурацию SMTP', + recipientEmail: 'Email получателя', + recipientEmailPlaceholder: "test{'@'}example.com", + sendTestEmail: 'Отправить тестовый email', + sending: 'Отправка...', + enterRecipientHint: 'Введите email-адрес получателя' + }, + emailTemplates: { + title: 'Email-шаблоны', + description: 'Настройте темы email-уведомлений и HTML-содержимое для каждого события и локали.', + event: 'Событие', + locale: 'Локаль', + localeEn: 'English', + localeZh: 'Chinese', + subject: 'Тема', + subjectPlaceholder: 'Введите тему email', + html: 'HTML Template', + htmlPlaceholder: 'Измените HTML-шаблон email', + placeholders: 'Доступные placeholders', + placeholdersHelp: 'Нажмите placeholder, чтобы скопировать его. Backend заменяет эти значения при отправке email.', + livePreview: 'Live Preview', + previewSecurityHint: 'Preview HTML генерируется backend preview endpoint и отображается в sandboxed iframe с отключёнными скриптами.', + preview: 'Preview / обновить', + previewing: 'Формируется preview...', + save: 'Сохранить шаблон', + saving: 'Сохранение...', + restoreOfficial: 'Восстановить официальный', + restoring: 'Восстановление...', + restoreConfirm: 'Восстановить официальный шаблон для этого события и локали? Пользовательская версия будет заменена.', + restoreSuccess: 'Официальный шаблон восстановлен', + saveSuccess: 'Email-шаблон сохранён', + placeholderCopied: 'Placeholder скопирован', + validationRequired: 'Тема и HTML-шаблон обязательны', + empty: 'События или локали email-шаблонов пока недоступны.', + noPreview: 'Обновите preview, чтобы увидеть отрендеренную тему email.', + customized: 'Настроено' + }, + opsMonitoring: { + title: 'Ops Monitoring', + 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: 'Админский API-ключ', + description: 'Глобальный API-ключ для интеграции внешних систем с полным админским доступом', + notConfigured: 'Админский API-ключ не настроен', + configured: 'Админский API-ключ активен', + currentKey: 'Текущий ключ', + regenerate: 'Перегенерировать', + regenerating: 'Перегенерация...', + delete: 'Удалить', + deleting: 'Удаление...', + create: 'Создать ключ', + creating: 'Создание...', + regenerateConfirm: 'Вы уверены? Текущий ключ будет немедленно аннулирован.', + deleteConfirm: + 'Вы уверены, что хотите удалить админский API-ключ? Внешние интеграции перестанут работать.', + keyGenerated: 'Новый админский API-ключ сгенерирован', + keyDeleted: 'Админский API-ключ удалён', + copyKey: 'Копировать ключ', + keyCopied: 'Ключ скопирован в буфер обмена', + keyWarning: 'Этот ключ будет показан только один раз. Скопируйте его сейчас.', + securityWarning: 'Внимание: этот ключ предоставляет полный админский доступ. Храните его безопасно.', + usage: 'Использование: добавьте в header запроса - x-api-key: ' + }, + soraS3: { + title: 'Хранилище Sora', + description: 'Управляйте профилями хранилища медиа Sora с поддержкой S3 и Google Drive', + newProfile: 'Новый профиль', + reloadProfiles: 'Перезагрузить профили', + empty: 'Профилей хранилища пока нет, сначала создайте профиль', + createTitle: 'Создать профиль хранилища', + editTitle: 'Редактировать профиль хранилища', + selectProvider: 'Выберите тип хранилища', + providerS3Desc: 'S3-совместимое объектное хранилище', + providerGDriveDesc: 'Облачное хранилище Google Drive', + profileID: 'Profile ID', + 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', + name: 'Имя', + provider: 'Тип', + active: 'Активен', + endpoint: 'Endpoint', + bucket: 'Bucket', + storagePath: 'Путь хранилища', + capacityUsage: 'Ёмкость / использовано', + capacityUnlimited: 'Безлимитно', + videoCount: 'Видео', + videoCompleted: 'завершено', + videoInProgress: 'в процессе', + quota: 'Квота по умолчанию', + updatedAt: 'Обновлено', + actions: 'Действия', + rootFolder: 'Корневая папка', + testInTable: 'Тест', + testingInTable: 'Проверка...', + testTimeout: 'Таймаут теста (15s)' + }, + enabled: 'Включить хранилище', + enabledHint: 'Когда включено, сгенерированные Sora медиафайлы будут автоматически загружаться', + endpoint: 'S3 Endpoint', + region: 'Регион', + bucket: 'Bucket', + prefix: 'Префикс объектов', + accessKeyId: 'Access Key ID', + secretAccessKey: 'Secret Access Key', + secretConfigured: '(Настроено, оставьте пустым, чтобы сохранить)', + cdnUrl: 'CDN URL', + cdnUrlHint: 'Необязательно. Когда настроено, доступ к файлам выполняется через CDN URL', + forcePathStyle: 'Принудительный Path Style', + defaultQuota: 'Квота хранилища по умолчанию', + defaultQuotaHint: 'Квота по умолчанию, если не задана на уровне пользователя или группы. 0 означает безлимитно', + testConnection: 'Проверить подключение', + testing: 'Проверка...', + testSuccess: 'Проверка соединения успешна', + testFailed: 'Проверка соединения не удалась', + saved: 'Настройки хранилища успешно сохранены', + saveFailed: 'Не удалось сохранить настройки хранилища', + gdrive: { + authType: 'Метод аутентификации', + serviceAccount: 'Service Account', + clientId: 'Client ID', + clientSecret: 'Client Secret', + clientSecretConfigured: '(Настроено, оставьте пустым, чтобы сохранить)', + refreshToken: 'Refresh Token', + refreshTokenConfigured: '(Настроено, оставьте пустым, чтобы сохранить)', + serviceAccountJson: 'Service Account JSON', + 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: 'Настройте стратегию паузы маршрутизации аккаунта, когда upstream возвращает 529 (overloaded)', + enabled: 'Включить overload cooldown', + enabledHint: 'Приостанавливать маршрутизацию аккаунта при ошибках 529, автоматически восстанавливать после cooldown', + cooldownMinutes: 'Длительность cooldown (минуты)', + cooldownMinutesHint: 'Длительность паузы маршрутизации аккаунта (1-120 минут)', + saved: 'Настройки overload cooldown сохранены', + saveFailed: 'Не удалось сохранить настройки overload cooldown' + }, + rateLimit429Cooldown: { + 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', + 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: 'Исправитель запросов', + 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: + 'Автоматически удаляет signatures и повторяет запрос, когда аккаунты API Key получают ошибки, связанные с signature (встроенные patterns применяются всегда)', + apikeyPatterns: 'Пользовательские patterns сопоставления', + apikeyPatternsHint: + 'Дополнительные ключевые слова для сопоставления с телом ответа (без учёта регистра). Встроенные patterns применяются всегда; используйте эти для дополнительного сопоставления.', + apikeyPatternPlaceholder: 'e.g., thinking_error', + addPattern: 'Добавить pattern', + saved: 'Настройки исправителя сохранены', + saveFailed: 'Не удалось сохранить настройки исправителя' + }, + betaPolicy: { + 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: 'Добавить pattern модели', + removePattern: 'Удалить', + fallbackAction: 'Fallback-действие', + fallbackActionHint: 'Действие для моделей, не совпадающих с белым списком', + fallbackErrorMessagePlaceholder: 'Пользовательское сообщение об ошибке при блокировке моделей вне белого списка', + quickPresets: 'Быстрые пресеты', + presetOpusOnly: 'Только Opus для 1M', + presetOpusOnlyDesc: 'Пропускать для Opus, фильтровать остальные', + commonPatterns: 'Частые patterns' + }, + openaiFastPolicy: { + 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: 'Действие', + 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: 'Добавить pattern модели', + fallbackAction: 'Fallback-действие', + fallbackActionHint: 'Действие для моделей, не совпадающих с белым списком.', + fallbackErrorMessagePlaceholder: 'Пользовательское сообщение об ошибке при блокировке моделей вне белого списка' + }, + wechatConnect: { + title: 'WeChat Connect', + 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 настроен. Оставьте пустым, чтобы сохранить текущее значение.', + appSecretPlaceholder: 'WeChat App Secret', + 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: 'Сгенерировать и скопировать (текущий сайт)', + redirectUrlSetAndCopied: 'Redirect URL сгенерирован и скопирован в буфер обмена', + frontendRedirectUrlLabel: 'Frontend redirect URL', + frontendRedirectUrlPlaceholder: '/auth/wechat/callback', + frontendRedirectUrlHint: 'Обычно это callback path frontend-маршрута; держите его согласованным с backend.' + }, + authSourceDefaults: { + title: 'Значения по умолчанию для источника auth', + description: 'Настройте баланс, параллелизм, подписки и правила выдачи по умолчанию для каждого источника.', + requireEmailLabel: 'Требовать email при сторонней регистрации', + requireEmailHint: 'Когда включено, регистрации через Linux DO, OIDC и WeChat должны предоставить email перед созданием аккаунта.', + enabledHint: 'Эти значения по умолчанию применяются, когда новый пользователь регистрируется через этот источник. Grant on first bind применяется только когда существующий пользователь привязывает этот источник.', + sources: { + email: { + title: 'Регистрация через email', + description: 'Выдачи квот по умолчанию для регистраций по email-паролю.' + }, + linuxdo: { + title: 'Регистрация через Linux DO', + description: 'Выдачи квот по умолчанию для регистраций через Linux DO.' + }, + oidc: { + title: 'Регистрация через OIDC', + description: 'Выдачи квот по умолчанию для регистраций через OIDC.' + }, + wechat: { + title: 'Регистрация через WeChat', + description: 'Выдачи квот по умолчанию для регистраций через WeChat.' + } + }, + grantOnFirstBindLabel: 'Выдать при первой привязке', + grantOnFirstBindHint: 'Выдавать права по умолчанию, когда существующий пользователь впервые привязывает этот источник.', + defaultSubscriptionsLabel: 'Подписки по умолчанию', + defaultSubscriptionsHint: 'Применяется только к этому auth source. Оставьте пустым, чтобы пропустить подписки для конкретного источника.', + noSourceSubscriptions: 'Подписки по умолчанию для конкретного источника не настроены.', + platformQuotasOverride: 'Переопределения квот платформ', + platformQuotasOverrideHint: 'Пустые поля наследуют системное значение по умолчанию. Установите 0, чтобы полностью заблокировать это окно для данного auth source.', + }, + paymentVisibleMethods: { + methodLabel: 'Видимый метод {title}', + methodHint: 'Управляет тем, показывает ли checkout этот метод и какой source key он раскрывает.', + sourceLabel: 'Источник оплаты', + sourceHint: 'Выберите явный источник перед включением метода. Ненастроенные методы не раскрываются.', + sourceRequiredError: 'Выберите источник оплаты перед включением {title}.' + }, + openaiExperimentalScheduler: { + title: 'Экспериментальная политика OpenAI scheduler', + description: "По умолчанию отключено. Когда включено, меняет только экспериментальную политику выбора аккаунта шлюза для OpenAI traffic; это не означает наличие такой возможности у upstream OpenAI." + }, + saveSettings: 'Сохранить настройки', + saving: 'Сохранение...', + settingsSaved: 'Настройки успешно сохранены', + smtpConnectionSuccess: 'SMTP-соединение успешно', + testEmailSent: 'Тестовый email успешно отправлен', + failedToLoad: 'Не удалось загрузить настройки', + failedToSave: 'Не удалось сохранить настройки', + failedToTestSmtp: 'Проверка SMTP-соединения не удалась', + failedToSendTestEmail: 'Не удалось отправить тестовый email' + }, + + // Error Passthrough Rules + errorPassthrough: { + title: 'Правила проброса ошибок', + description: 'Настройте, как upstream-ошибки возвращаются клиентам', + createRule: 'Создать правило', + editRule: 'Редактировать правило', + deleteRule: 'Удалить правило', + noRules: 'Правила не настроены', + createFirstRule: 'Создайте первое правило проброса ошибок', + allPlatforms: 'Все платформы', + passthrough: 'Проброс', + custom: 'Свой', + code: 'Код', + body: 'Body', + skipMonitoring: 'Пропустить мониторинг', + + // Columns + columns: { + priority: 'Приоритет', + name: 'Имя', + conditions: 'Условия', + platforms: 'Платформы', + behavior: 'Поведение', + status: 'Статус', + actions: 'Действия' + }, + + // Match Mode + matchMode: { + any: 'Код ИЛИ ключевое слово', + all: 'Код И ключевое слово', + anyHint: 'Status code совпадает с любым кодом ошибки ИЛИ сообщение содержит любое ключевое слово', + allHint: 'Status code совпадает с любым кодом ошибки И сообщение содержит любое ключевое слово' + }, + + // Form + form: { + name: 'Название правила', + namePlaceholder: 'например Context Limit Passthrough', + priority: 'Приоритет', + priorityHint: 'Меньшие значения имеют более высокий приоритет', + description: 'Описание', + descriptionPlaceholder: 'Опишите назначение этого правила...', + matchConditions: 'Условия сопоставления', + errorCodes: 'Коды ошибок', + errorCodesPlaceholder: '422, 400, 429', + errorCodesHint: 'Разделяйте несколько кодов запятыми', + keywords: 'Ключевые слова', + keywordsPlaceholder: 'One keyword per line\ncontext limit\nmodel not supported', + keywordsHint: 'Одно ключевое слово на строку, без учёта регистра', + matchMode: 'Режим сопоставления', + platforms: 'Платформы', + platformsHint: 'Оставьте пустым, чтобы применить ко всем платформам', + responseBehavior: 'Поведение ответа', + passthroughCode: 'Проброс upstream status code', + responseCode: 'Пользовательский status code', + passthroughBody: 'Проброс upstream-сообщения об ошибке', + customMessage: 'Пользовательское сообщение об ошибке', + customMessagePlaceholder: 'Сообщение об ошибке для возврата клиенту...', + skipMonitoring: 'Skip monitoring', + skipMonitoringHint: 'Когда включено, ошибки, соответствующие этому правилу, не будут записываться в ops monitoring', + enabled: 'Включить это правило' + }, + + // Messages + nameRequired: 'Введите название правила', + conditionsRequired: 'Настройте хотя бы один код ошибки или ключевое слово', + ruleCreated: 'Правило успешно создано', + ruleUpdated: 'Правило успешно обновлено', + ruleDeleted: 'Правило успешно удалено', + deleteConfirm: 'Удалить правило "{name}"?', + failedToLoad: 'Не удалось загрузить правила', + failedToSave: 'Не удалось сохранить правило', + failedToDelete: 'Не удалось удалить правило', + failedToToggle: 'Не удалось переключить статус' + }, + + // TLS Fingerprint Profiles + tlsFingerprintProfiles: { + title: 'Профили TLS fingerprint', + description: 'Управляйте профилями TLS fingerprint для имитации характеристик TLS handshake конкретного клиента', + createProfile: 'Создать профиль', + editProfile: 'Изменить профиль', + deleteProfile: 'Удалить профиль', + noProfiles: 'Профили не настроены', + createFirstProfile: 'Создайте первый профиль TLS fingerprint', + + columns: { + name: 'Имя', + description: 'Описание', + grease: 'GREASE', + alpn: 'ALPN', + actions: 'Действия' + }, + + form: { + 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: 'Необязательное описание для этого профиля', + enableGrease: 'Включить GREASE', + enableGreaseHint: 'Вставлять GREASE values в extensions TLS ClientHello', + cipherSuites: 'Cipher Suites', + cipherSuitesHint: 'Hex-значения через запятую, например 0x1301, 0x1302, 0xc02c', + curves: 'Elliptic Curves', + curvesHint: 'Curve IDs через запятую', + pointFormats: 'Point Formats', + signatureAlgorithms: 'Signature Algorithms', + alpnProtocols: 'ALPN Protocols', + alpnProtocolsHint: 'Через запятую, например h2, http/1.1', + supportedVersions: 'Поддерживаемые TLS Versions', + keyShareGroups: 'Key Share Groups', + pskModes: 'PSK Modes', + extensions: 'Extensions' + }, + + deleteConfirm: 'Удалить профиль', + deleteConfirmMessage: 'Удалить профиль "{name}"? Аккаунты, использующие этот профиль, вернутся к встроенному значению по умолчанию.', + createSuccess: 'Профиль успешно создан', + updateSuccess: 'Профиль обновлён', + deleteSuccess: 'Профиль успешно удалён', + loadFailed: 'Не удалось загрузить профили', + saveFailed: 'Не удалось сохранить профиль', + deleteFailed: 'Не удалось удалить профиль' + } + }, + + // Subscription Progress (Header component) + subscriptionProgress: { + title: 'Мои подписки', + viewDetails: 'Посмотреть детали подписки', + activeCount: 'Активных подписок: {count}', + daily: 'Дневная', + weekly: 'Недельная', + monthly: 'Месячная', + daysRemaining: 'Осталось дней: {days}', + expired: 'Истекла', + expiresToday: 'Истекает сегодня', + expiresTomorrow: 'Истекает завтра', + viewAll: 'Все подписки', + noSubscriptions: 'Нет активных подписок', + unlimited: 'Безлимитно' + }, + + // Version Badge + version: { + currentVersion: 'Текущая версия', + latestVersion: 'Последняя версия', + upToDate: 'У вас установлена последняя версия.', + updateAvailable: 'Доступна новая версия!', + releaseNotes: 'Примечания к релизу', + noReleaseNotes: 'Нет примечаний к релизу', + viewUpdate: 'Посмотреть обновление', + viewRelease: 'Посмотреть релиз', + viewChangelog: 'История изменений', + refresh: 'Обновить', + sourceMode: 'Сборка из исходников', + sourceModeHint: 'Сборка из исходников, обновляйте через git pull', + updateNow: 'Обновить сейчас', + updating: 'Обновление...', + updateComplete: 'Обновление завершено', + updateFailed: 'Обновление не удалось', + restartRequired: 'Перезапустите сервис, чтобы применить обновление', + restartNow: 'Перезапустить сейчас', + restarting: 'Перезапуск...', + retry: 'Повторить' + }, + + // Recharge / Subscription Page + purchase: { + title: 'Пополнение / подписка', + description: 'Пополните баланс или купите подписку через встроенную страницу', + openInNewTab: 'Открыть в новой вкладке', + notEnabledTitle: 'Функция не включена', + notEnabledDesc: 'Администратор не включил раздел пополнения/подписки. Обратитесь к администратору.', + notConfiguredTitle: 'URL пополнения / подписки не настроен', + notConfiguredDesc: + 'Раздел включён, но URL пополнения/подписки не настроен. Обратитесь к администратору.' + }, + + // Custom Page (iframe embed) + customPage: { + title: 'Пользовательская страница', + openInNewTab: 'Открыть в новой вкладке', + notFoundTitle: 'Страница не найдена', + notFoundDesc: 'Эта пользовательская страница не существует или была удалена.', + notConfiguredTitle: 'URL страницы не настроен', + notConfiguredDesc: 'URL этой пользовательской страницы настроен некорректно.', + }, + + // Announcements Page + announcements: { + title: 'Объявления', + description: 'Просмотр системных объявлений', + unreadOnly: 'Только непрочитанные', + markRead: 'Отметить прочитанным', + markAllRead: 'Отметить все прочитанными', + viewAll: 'Все объявления', + markedAsRead: 'Отмечено прочитанным', + allMarkedAsRead: 'Все объявления отмечены прочитанными', + newCount: '{count} новое объявление | {count} новых объявлений', + readAt: 'Прочитано', + read: 'Прочитано', + unread: 'Непрочитано', + startsAt: 'Начинается', + endsAt: 'Заканчивается', + empty: 'Нет объявлений', + emptyUnread: 'Нет непрочитанных объявлений', + total: 'объявлений', + emptyDescription: 'Сейчас системных объявлений нет', + readStatus: 'Вы прочитали это объявление', + markReadHint: 'Нажмите "Отметить прочитанным", чтобы отметить объявление' + }, + + // User Subscriptions Page + userSubscriptions: { + title: 'Мои подписки', + description: 'Просмотр ваших подписок и расхода', + noActiveSubscriptions: 'Нет активных подписок', + noActiveSubscriptionsDesc: + "У вас нет активных подписок. Обратитесь к администратору.", + failedToLoad: 'Не удалось загрузить подписки', + status: { + active: 'Активен', + expired: 'Истекла', + revoked: 'Отозвана' + }, + usage: 'Расход', + expires: 'Истекает', + noExpiration: 'Без срока', + unlimited: 'Безлимитно', + unlimitedDesc: 'У этой подписки нет лимитов расхода', + daily: 'Дневная', + weekly: 'Недельная', + monthly: 'Месячная', + daysRemaining: 'Осталось дней: {days}', + expiresOn: 'Истекает {date}', + resetIn: 'Сброс через {time}', + quotaEndsIn: 'Квота закончится через {time}', + windowNotActive: 'Ожидает первого использования', + usageOf: '{used} из {limit}' + }, + + // Onboarding Tour + onboarding: { + restartTour: 'Перезапустить обучение', + dontShowAgain: 'Больше не показывать', + dontShowAgainTitle: 'Закрыть обучение навсегда', + confirmDontShow: "Больше не показывать мастер настройки?\n\nВы сможете перезапустить его в любое время из меню пользователя в правом верхнем углу.", + confirmExit: 'Выйти из мастера настройки? Вы сможете перезапустить его в любое время из меню в правом верхнем углу.', + interactiveHint: 'Нажмите Enter или кликните, чтобы продолжить', + navigation: { + flipPage: 'Перелистнуть', + exit: 'Выйти' + }, + // Admin tour steps + admin: { + welcome: { + title: '👋 Добро пожаловать в Sub2API', + description: '

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

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

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

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

', + nextBtn: 'Начать 🚀', + prevBtn: 'Пропустить' + }, + groupManage: { + title: '📦 Шаг 1: Управление группами', + description: '

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

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

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

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

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

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

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

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

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

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

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

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

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

', + nextBtn: 'Далее' + }, + groupPlatform: { + title: '🤖 2. Выбор платформы', + description: '

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

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

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

', + nextBtn: 'Далее' + }, + groupMultiplier: { + title: '💰 3. Тарифный коэффициент', + description: '

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

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

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

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

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

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

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

', + nextBtn: 'Далее' + }, + groupSubmit: { + title: '✅ Сохранить группу', + description: '

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

', + nextBtn: 'Далее' + }, + accountPlatform: { + title: '🤖 2. Выбор платформы', + description: '

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

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

', + nextBtn: 'Далее' + }, + accountType: { + title: '🔐 3. Способ авторизации', + description: '

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

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

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

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

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

', + nextBtn: 'Далее' + }, + accountGroups: { + title: '🎯 5. Назначить группы', + description: '

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

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

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

', + nextBtn: 'Далее' + }, + accountSubmit: { + title: '✅ Сохранить аккаунт', + description: '

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

', + nextBtn: 'Далее' + }, + keyGroup: { + title: '🎯 2. Выбор группы', + description: '

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

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

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

', + nextBtn: 'Далее' + }, + keySubmit: { + title: '🎉 Создать и скопировать', + description: '

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

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

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

' + } + }, + // User tour steps + user: { + welcome: { + title: '👋 Добро пожаловать в Sub2API', + description: '

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

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

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

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

', + nextBtn: 'Начать 🚀', + prevBtn: 'Пропустить' + }, + keyManage: { + title: '🔑 Управление API Key', + description: '

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

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

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

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

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

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

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

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

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

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

', + nextBtn: 'Далее' + }, + keyGroup: { + title: '🎯 Выбор группы', + description: '

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

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

', + nextBtn: 'Далее' + }, + keySubmit: { + title: '🎉 Завершить создание', + description: '

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

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

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

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

' + } + } + }, + + // 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: 'Карта', + link: 'Ссылка', + alipay_direct: 'Alipay (Direct)', + wxpay_direct: 'WeChat Pay (Direct)', + }, + status: { + pending: 'Ожидает', + paid: 'Оплачено', + recharging: 'Пополнение', + completed: 'Завершено', + expired: 'Истекла', + cancelled: 'Отменено', + failed: 'Ошибка', + refund_requested: 'Запрошен возврат', + refunding: 'Возврат', + refunded: 'Возвращено', + partially_refunded: 'Частично возвращено', + refund_failed: 'Возврат не удался', + }, + qr: { + scanToPay: 'Сканируйте для оплаты', + scanAlipay: 'Оплата Alipay по QR', + scanWxpay: 'Оплата WeChat по QR', + scanAlipayHint: 'Откройте Alipay на телефоне и отсканируйте QR-код', + scanWxpayHint: 'Откройте WeChat на телефоне и отсканируйте QR-код', + payInNewWindow: 'Завершить оплату в новом окне', + payInNewWindowHint: 'Страница оплаты открыта в новом окне. Завершите оплату там и вернитесь сюда.', + openPayWindow: 'Открыть страницу оплаты снова', + expiresIn: 'Истекает через', + expired: 'Заказ истёк', + expiredDesc: 'Этот заказ истёк. Создайте новый.', + cancelled: 'Заказ отменён', + cancelledDesc: 'Вы отменили этот платёж.', + waitingPayment: 'Ожидание оплаты...', + cancelOrder: 'Отменить заказ', + }, + orders: { + title: 'Мои заказы', + empty: 'Заказов пока нет', + orderId: 'ID заказа', + orderNo: 'Номер заказа', + amount: 'Сумма', + payAmount: 'Оплачено', + creditedAmount: 'Зачислено', + fee: 'Комиссия', + baseAmount: 'Базовая сумма', + includedInPayAmount: 'включено в сумму оплаты', + status: 'Статус', + paymentMethod: 'Способ оплаты', + createdAt: 'Создан', + cancel: 'Отменить заказ', + userId: 'ID пользователя', + orderType: 'Тип заказа', + actions: 'Действия', + requestRefund: 'Запросить возврат', + }, + result: { + success: 'Оплата успешна', + subscriptionSuccess: 'Подписка оформлена', + processing: 'Платёж обрабатывается', + processingHint: 'Подтверждение платежа ещё ожидается. Страница обновится автоматически.', + failed: 'Оплата не удалась', + backToRecharge: 'Назад к пополнению', + viewOrders: 'Посмотреть заказы', + }, + currentBalance: 'Текущий баланс', + groupFallback: 'Группа #{id}', + rechargeAccount: 'Пополнить аккаунт', + activeSubscription: 'Активная подписка', + noActiveSubscription: 'Нет активной подписки', + tabTopUp: 'Пополнить', + tabSubscribe: 'Подписаться', + noPlans: 'Нет доступных планов подписки', + notAvailable: 'Пополнение сейчас недоступно', + confirmSubscription: 'Подтвердить подписку', + 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: 'Слишком много ожидающих заказов (макс. {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: 'Система оплаты отключена.', + 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: 'Оплатить', + stripeSuccessProcessing: 'Платёж успешен, обрабатываем заказ...', + stripePopup: { + redirecting: 'Переход на страницу оплаты...', + loadingQr: 'Загрузка QR-кода WeChat Pay...', + timeout: 'Истекло время ожидания платёжных данных, повторите попытку', + qrFailed: 'Не удалось получить QR-код WeChat Pay', + }, + subscribeNow: 'Подписаться', + renewNow: 'Продлить', + selectPlan: 'Выбрать план', + planFeatures: 'Возможности', + planCard: { + rate: 'Тариф', + dailyLimit: 'Дневной', + weeklyLimit: 'Недельный', + monthlyLimit: 'Месячный', + quota: 'Квота', + unlimited: 'Безлимитно', + models: 'Модели', + }, + days: 'дней', + months: 'месяцев', + years: 'лет', + oneMonth: '1 месяц', + oneYear: '1 год', + perMonth: 'месяц', + perYear: 'год', + admin: { + tabs: { + overview: 'Обзор', + orders: 'Заказы', + channels: 'Каналы', + plans: 'Планы', + }, + todayRevenue: 'Выручка сегодня', + totalRevenue: 'Общая выручка', + todayOrders: 'Заказы сегодня', + orderCount: 'Количество заказов', + avgAmount: 'Средняя сумма', + revenue: 'Выручка', + dailyRevenue: 'Дневная выручка', + paymentDistribution: 'Распределение оплат', + colUser: 'Пользователь', + topUsers: 'Топ пользователей', + noData: 'Нет данных', + days: 'дней', + weeks: 'недель', + months: 'месяцев', + searchOrders: 'Поиск заказов...', + allStatuses: 'Все статусы', + allPaymentTypes: 'Все типы оплаты', + allOrderTypes: 'Все типы заказов', + orderDetail: 'Детали заказа', + orderType: 'Тип заказа', + orders: 'Заказы', + balanceOrder: 'Пополнение баланса', + subscriptionOrder: 'Подписка', + paidAt: 'Оплачен', + completedAt: 'Завершён', + expiresAt: 'Истекает', + feeRate: 'Ставка комиссии', + refund: 'Возврат', + refundOrder: 'Возврат заказа', + refundAmount: 'Сумма возврата', + maxRefundable: 'Максимум к возврату', + refundReason: 'Причина возврата', + refundReasonPlaceholder: 'Введите причину возврата', + confirmRefund: 'Подтвердить возврат', + refundSuccess: 'Возврат выполнен', + refundInfo: 'Информация о возврате', + refundEnabled: 'Возвраты включены', + allowUserRefund: 'Разрешить возврат пользователю', + alreadyRefunded: 'Уже возвращено', + deductBalance: 'Списать с баланса', + deductBalanceHint: 'Списать сумму пополнения с баланса пользователя', + userBalance: 'Баланс пользователя', + orderAmount: 'Сумма заказа', + insufficientBalance: 'Недостаточно баланса — будет списано до $0', + noDeduction: 'Баланс пользователя НЕ будет списан', + forceRefund: 'Принудительный возврат (игнорировать проверку баланса)', + orderCancelled: 'Заказ отменён', + retry: 'Повторить', + 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: 'ID группы', + features: 'Возможности', + 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: 'Аналитика и сводка заказов пополнения', + daySuffix: 'д', + paymentConfigTitle: 'Настройки платежей', + paymentConfigDesc: 'Настройка провайдеров оплаты и параметров', + plansPageTitle: 'Тарифы подписки', + plansPageDesc: 'Управление конфигурацией тарифов подписки', + tabPlanConfig: 'Конфигурация тарифа', + tabUserSubs: 'Подписки пользователей', + selectGroup: 'Выберите группу', + groupRequired: 'Выберите группу подписки', + priceRequired: 'Цена должна быть больше 0', + validityDaysRequired: 'Срок действия в днях должен быть больше 0', + groupMissing: 'Отсутствует', + groupInfo: 'Информация о группе', + platform: 'Платформа', + rateMultiplierLabel: 'Тариф', + dailyLimit: 'Дневной лимит', + weeklyLimit: 'Недельный лимит', + monthlyLimit: 'Месячный лимит', + unlimited: 'Безлимитно', + searchUserSubs: 'Поиск подписок пользователей...', + daily: 'D', + weekly: 'W', + monthly: 'M', + subsStatus: { + active: 'Активен', + expired: 'Истекла', + revoked: 'Отозвана', + }, + }, + }, + +}