Files
wangjia 6f232d4043
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 25s
ci-pangolin / Cleartext Scan — Android 禁明文 (push) Successful in 19s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 18s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (push) Failing after 13m6s
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Failing after 13m16s
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Failing after 13m25s
ci-pangolin / Go — build + test (push) Failing after 13m35s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (push) Failing after 13m45s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Failing after 13m54s
ci-pangolin / Flutter — analyze + test (push) Failing after 14m5s
ci-pangolin / OpenAPI Sync Check (push) Failing after 14m16s
ci-pangolin / Lint — shellcheck (push) Failing after 14m27s
fix(routing): 堵住 ip_cidr direct 旁路系统层 + 客户端保存失败可见提示
分支审核发现两处 Important,合并前修复。

① [安全] direct 的 ip_cidr 用户规则可自伤式旁路整条隧道:
   Validate 原先只校 CIDR 语法。用户提交 ip_cidr=0.0.0.0/0 action=direct
   (或 172.16.0.0/12,含隧道 DNS 172.19.0.2)会并入 TUN 入站
   route_exclude_address(OS/auto_route 层,位于系统强制层之下),被排除的
   流量根本不进 sing-box → hijack-dns 与整条隧道被静默旁路,违反「系统层
   用户不可越」铁律。
   - Validate: direct 的 ip_cidr 拒绝 catch-all(/0)及与保留段 172.16.0.0/12
     重叠(写入闸)。
   - clientconfig 渲染层:新增 routing.SafeToExclude 守卫,只有安全的 direct
     ip_cidr 才并入 route_exclude_address(纵深防护,兜底写入闸之前的历史坏行)。
   - 测试 TestValidateDirectIPCIDRReservedGuard 钉死:拒 catch-all/隧道段重叠、
     放行 proxy catch-all 与不重叠 direct。

② [健壮性] 客户端保存失败静默回滚 + 抛未捕获异步异常 + 对话框无字段校验:
   _persist 失败会 rethrow(约定调用方 catch),但屏幕层所有回调
   (setMode/setBuiltin/addRule/removeRule/reorder/resetToDefault)均未 catch,
   规则闪现即消失、无提示,且 rethrow 变 zone 未处理异常。
   - 新增 _guardSave 守卫:await + 失败弹 SnackBar(AuthApiException 显服务端
     双语文案含校验错,其余回退通用「保存失败」),包裹全部变更类回调。
   - 添加规则对话框:_valueError 字段级预校验(ip_cidr 用 InternetAddress
     校验、geo 白名单仅 cn),非法即禁用保存并内联红字提示;语义级(保留段)
     仍由服务端权威判定经 SnackBar 呈现。
   - l10n 单源新增 routingSaveFailed / routingRuleValueInvalid(6 语),regen。

go test ./... 全绿;flutter analyze 无 error;flutter test 265 全过无 golden 回归;
codegen 幂等、原型 i18n 无漂移。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
2026-08-01 00:32:52 +08:00

608 lines
26 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// strings_ru.dart — Русский文案(自动生成 · 勿手改)
//
// GENERATED by design/codegen/gen_l10n_dart.mjs from design/i18n/strings.json.
// 改文案:改 strings.json 后重跑 codegen;新增 key 需同步 app_text.dart 抽象声明。
import 'app_text.dart';
class StringsRu extends AppText {
const StringsRu();
@override
AppLang get lang => AppLang.ru;
@override
String get brand => 'Pangolin';
@override
String get online => 'В сети';
@override
String get offline => '○ Не в сети';
@override
String get capOff => 'Нажмите для подключения';
@override
String get capConnecting => 'Подключение…';
@override
String get capOn => 'Подключено · Шифрование';
@override
String get connectNow => 'ПОДКЛЮЧИТЬ';
@override
String get secure => 'ЗАЩИЩЕНО';
@override
String get tabConnect => 'Подключение';
@override
String get tabServers => 'Серверы';
@override
String get tabStats => 'Статистика';
@override
String get tabMe => 'Аккаунт';
@override
String get currentNode => 'Текущий узел';
@override
String get download => 'Приём';
@override
String get upload => 'Отдача';
@override
String get latency => 'Пинг';
@override
String get quotaToday => 'Осталось сегодня';
@override
String get minutes => 'мин';
@override
String get quotaFree => 'Бесплатно · 10 мин/день';
@override
String get planFreeTag => 'Бесплатно';
@override
String get watchAd => 'Смотрите рекламу, чтобы начать';
@override
String get adUnlocked => 'Открыто на сегодня';
@override
String get quotaLeftLabel => 'Осталось';
@override
String get quotaUsedUp => 'Дневной бесплатный лимит исчерпан';
@override
String get watchAdMore => 'Смотрите рекламу, чтобы добавить время';
@override
String get quotaExhaustedNotice => 'Дневной бесплатный лимит исчерпан. Оформите подписку, чтобы продолжить подключение.';
@override
String get adPlaying => 'Реклама воспроизводится…';
@override
String get adFailed => 'Не удалось добавить время, попробуйте ещё раз';
@override
String get quotaDesktopTitle => 'Дневной бесплатный лимит исчерпан';
@override
String get quotaDesktopBody => 'Смотрите рекламу в мобильном приложении, чтобы добавить время, или оформите подписку для безлимитного доступа.';
@override
String get gotIt => 'Понятно';
@override
String get chooseNode => 'Выбор сервера';
@override
String get searchPh => 'Поиск страны / города';
@override
String get smartSelect => 'Умный выбор';
@override
String get nodeSwitchBody => 'Текущее соединение будет разорвано и восстановлено через этот узел.';
@override
String get nodeSwitchConfirm => 'Переключить';
@override
String get nodeUnhealthySwitched => 'Текущий узел недоступен — переключение выполнено автоматически';
@override
String get nodeUnhealthyError => 'Текущий узел недоступен — переподключитесь или смените узел';
@override
String get nodeReconnecting => 'Сеть нестабильна, переподключение…';
@override
String get routingRulesReconnecting => 'Правила обновлены, переподключение…';
@override
String get smartSub => 'Подбирает лучший узел для вашей сети';
@override
String get recommended => 'Рекомендуется';
@override
String get statsTitle => 'Статистика';
@override
String get trafficMonth => 'Трафик';
@override
String get avgPing => 'Средний пинг';
@override
String get durMonth => 'Время';
@override
String get weekTraffic => 'На этой неделе (ГБ)';
@override
String get byDevice => 'По устройствам';
@override
String get noDeviceUsage => 'Пока нет данных по устройствам';
@override
String get periodMonth => 'Этот месяц';
@override
String get periodWeek => 'Эта неделя';
@override
String get periodToday => 'Сегодня';
@override
String get statDown => 'Приём';
@override
String get statUp => 'Отдача';
@override
String get statDuration => 'Длительность';
@override
String get chartTwoWeeks => 'Последние 2 недели';
@override
String get allDevices => 'Все устройства';
@override
String get meTitle => 'Аккаунт';
@override
String get proMember => 'PRO-подписка';
@override
String get freePlanName => 'Бесплатный';
@override
String get expires => 'Действует до';
@override
String get upgradeBtn => 'Продлить / Улучшить';
@override
String get accInfoTitle => 'Данные аккаунта';
@override
String get accEmail => 'Эл. почта';
@override
String get accPassword => 'Пароль';
@override
String get accChange => 'Изменить';
@override
String get deviceLimitTitle => 'Достигнут лимит устройств';
@override
String get deviceLimitDesc => 'Ваш тариф допускает до %s устройств. Удалите одно, чтобы продолжить.';
@override
String get deviceLimitRemoveOldest => 'Удалить давно неиспользуемое';
@override
String get myDevices => 'Мои устройства';
@override
String get devicesSub => 'До 3 устройств на PRO';
@override
String get thisDevice => 'Это устройство';
@override
String get remove => 'Удалить';
@override
String get devOnline => 'В сети';
@override
String get devOffline => 'Не в сети';
@override
String get devLastOnline => 'Был(а) в сети';
@override
String get devNeverLogin => 'Ни разу не входил(а)';
@override
String get devForceLogout => 'Принудительный выход';
@override
String get devClearLogin => 'Очистить данные входа';
@override
String get devRename => 'Переименовать';
@override
String get devRenameHint => 'Имя устройства';
@override
String get devSave => 'Сохранить';
@override
String get devForceLogoutConfirm => 'Это завершит сеанс входа устройства — оно будет отключено и потребует повторного входа. Устройство останется в списке.';
@override
String get devClearLoginConfirm => 'Это полностью удалит устройство из списка и отзовёт его сеанс входа и учётные данные передачи данных. Чтобы снова использовать устройство, нужно войти заново.';
@override
String get devCancel => 'Отмена';
@override
String get sessionRevokedTitle => 'Это устройство было отключено';
@override
String get sessionRevokedBody => 'Вход с этого устройства завершён с другого устройства вашего аккаунта. Пожалуйста, войдите снова.';
@override
String get sessionRevokedOk => 'Войти снова';
@override
String get redeemEntry => 'Активация';
@override
String get contactEntry => 'Связаться с нами';
@override
String get signOut => 'Выйти';
@override
String get language => 'Язык';
@override
String get darkAppearance => 'Тёмная тема';
@override
String get stateOn => 'Вкл.';
@override
String get followLight => 'Выкл.';
@override
String get protocol => 'Протокол';
@override
String get settingsTitle => 'Настройки';
@override
String get autostart => 'Запуск при старте системы';
@override
String get autostartSub => 'Запускать автоматически при включении системы';
@override
String get autoConnect => 'Автоподключение';
@override
String get autoConnectSub => 'Подключаться автоматически при запуске';
@override
String get smartRoute => 'Умная маршрутизация';
@override
String get smartRouteSub => 'Ускорение для зарубежных ресурсов, прямой доступ для локальных';
@override
String get killSwitch => 'Kill Switch';
@override
String get killSwitchSub => 'Блокировать трафик при разрыве соединения';
@override
String get checkUpdate => 'Проверить обновления';
@override
String get webUserCenter => 'Личный кабинет (веб)';
@override
String get updateNotesFallback => 'Это обновление включает исправления и улучшения стабильности.';
@override
String get updateLater => 'Позже';
@override
String get updateDownload => 'Скачать обновление';
@override
String get updateUpToDate => 'У вас установлена последняя версия';
@override
String get updateCheckFailed => 'Не удалось проверить обновления, попробуйте позже';
@override
String get freePlan => 'Бесплатный';
@override
String get proPlan => 'Pro';
@override
String get teamPlan => 'Команда';
@override
String get perMonth => '/мес';
@override
String get current => 'Текущий';
@override
String get upgrade => 'Улучшить';
@override
String get choose => 'Выбрать';
@override
String get mostPopular => 'Популярный';
@override
String get redeemTitle => 'Активация и покупка';
@override
String get redeemCodeTitle => 'Активировать код';
@override
String get redeemPh => 'Введите код активации';
@override
String get redeemBtn => 'Активировать';
@override
String get redeemOk => 'Активировано · PRO открыт';
@override
String get buyTitle => 'Где купить';
@override
String get buySub => 'Оплата внутри приложения недоступна. Получить код можно через:';
@override
String get chStore => 'Магазин самообслуживания';
@override
String get chEmail => 'Поддержка по эл. почте';
@override
String get contactTitle => 'Связаться с нами';
@override
String get contactIntro => 'Нужна помощь? Свяжитесь с нами любым способом ниже — обычно отвечаем за считанные минуты.';
@override
String get contactEmail => 'Поддержка по эл. почте';
@override
String get contactStore => 'Магазин самообслуживания';
@override
String get contactWebsite => 'Веб-сайт';
@override
String get contactHoursTitle => 'Часы поддержки';
@override
String get contactHours => 'Ежедневно 9:00 24:00 (GMT+8)';
@override
String get authTagline => 'Быстро · Стабильно · Без усилий';
@override
String get tabLogin => 'Вход';
@override
String get tabRegister => 'Регистрация';
@override
String get emailLabel => 'Эл. почта';
@override
String get emailPh => 'your@email.com';
@override
String get pwLabel => 'Пароль';
@override
String get pwPh => 'Введите пароль';
@override
String get setPwPh => 'Задайте пароль (для входа с нескольких устройств)';
@override
String get codeLabel => 'Код подтверждения';
@override
String get sendCode => 'Отправить код';
@override
String get resend => 'Отправить снова';
@override
String get doLogin => 'Войти';
@override
String get doNext => 'Далее';
@override
String get doCreate => 'Создать аккаунт';
@override
String get forgotPw => 'Забыли пароль?';
@override
String get tos => 'Продолжая, вы принимаете наши Условия и Политику конфиденциальности';
@override
String get obSkip => 'Пропустить';
@override
String get obNext => 'Далее';
@override
String get obAllow => 'Разрешить и продолжить';
@override
String get obStart => 'Начать';
@override
String get ob1Title => 'Подключение по всему миру';
@override
String get ob1Sub => '80+ локаций, автоматический выбор самого быстрого маршрута, надёжная стабильность.';
@override
String get ob2Title => 'Разрешите настройку сети';
@override
String get ob2Sub => 'Система запросит добавление сетевого профиля для создания зашифрованного туннеля.';
@override
String get ob3Title => 'Приватность по умолчанию';
@override
String get ob3Sub => 'Сквозное шифрование. Мы не храним журналы просмотров.';
@override
String get purchaseTitle => 'Купить тариф';
@override
String get paymentTitle => 'Оплата';
@override
String get buyNow => 'Купить';
@override
String get payMethodAlipay => 'Alipay';
@override
String get payMethodCrypto => 'USDT (TRC20)';
@override
String get choosePayMethod => 'Выберите способ оплаты';
@override
String get proMonthly => 'Pro · Помесячно';
@override
String get proQuarterly => 'Pro · Квартал';
@override
String get proYearly => 'Pro · Год';
@override
String get perQuarter => '/квартал';
@override
String get perYear => '/год';
@override
String get payAmountLabel => 'Сумма перевода';
@override
String get payAddressLabel => 'Адрес для оплаты';
@override
String get payNetworkLabel => 'Сеть';
@override
String get payExactAmountHint => 'Отправьте точную сумму — активация произойдёт автоматически';
@override
String get copied => 'Скопировано';
@override
String get openAlipay => 'Оплатить через Alipay';
@override
String get openAlipayHint => 'Вернитесь после оплаты — страница обновится автоматически';
@override
String get openAlipayFailed => 'Не удалось открыть Alipay. Проверьте, установлен ли он, или повторите';
@override
String get awaitingPayment => 'Ожидание оплаты';
@override
String get paySucceeded => 'Активировано';
@override
String get payExpiresAt => 'Действует до';
@override
String get payDone => 'Готово';
@override
String get payFailed => 'Оплата не удалась';
@override
String get payRetry => 'Повторить';
@override
String get switchPayMethod => 'Сменить способ оплаты';
@override
String get cancelOrder => 'Отменить заказ';
@override
String get qrNotSupported => 'Скопируйте и откройте в Alipay';
@override
String get orderCreateFailed => 'Не удалось создать заказ, повторите попытку позже';
@override
String get payUnsupportedRenderType => 'Не поддерживается, смените способ оплаты';
@override
String get payNoActiveOrder => 'Нет активных заказов';
@override
String get payChannel => 'Способ оплаты';
@override
String get payChannelUsdt => 'USDT · Крипто';
@override
String get payChannelCny => 'Юань · Alipay';
@override
String get perMonthSuffix => ' /мес';
@override
String get ordersTitle => 'Мои заказы';
@override
String get orderDetailTitle => 'Детали заказа';
@override
String get ordersEmpty => 'Заказов пока нет';
@override
String get orderNoLabel => 'Номер заказа';
@override
String get orderPlanLabel => 'Тариф';
@override
String get orderMethodLabel => 'Способ оплаты';
@override
String get orderAmountLabel => 'Сумма';
@override
String get orderCreatedLabel => 'Создан';
@override
String get orderPaidLabel => 'Оплачен';
@override
String get orderStatusPaid => 'Активирован';
@override
String get orderStatusCanceled => 'Отменён';
@override
String get orderContinuePay => 'Продолжить оплату';
@override
String get orderRefresh => 'Обновить';
@override
String get orderRebuy => 'Купить снова';
@override
String get payMethodNezha => 'Nazhapay';
@override
String get inviteEntry => 'Приглашайте друзей и получайте членство';
@override
String get inviteEntrySub => 'Друг регистрируется · оба получают дни подписки';
@override
String get inviteTitle => 'Пригласить друзей';
@override
String get inviteCodeLabel => 'Код приглашения';
@override
String get inviteLinkLabel => 'Ссылка приглашения';
@override
String get inviteCopyCode => 'Скопировать код';
@override
String get inviteCopyLink => 'Скопировать ссылку';
@override
String get inviteRewardsTitle => 'Правила наград';
@override
String get inviteRewardRegister => 'Регистрация друга · +3 дня каждому';
@override
String get inviteRewardFirstBuy => 'Первая покупка друга · +7 дней каждому';
@override
String get inviteRewardJoinTg => 'Вступление в TG-канал · +3 дня';
@override
String get inviteProgressTitle => 'Мои награды';
@override
String get inviteTasksTitle => 'Больше способов заработать';
@override
String get inviteJoinTgTitle => 'Вступить в Telegram-канал';
@override
String get inviteJoinTgBtn => 'Вступить в канал';
@override
String get inviteVerifyBtn => 'Проверить и получить';
@override
String get inviteTaskDone => 'Получено +3 дня';
@override
String get inviteStatInvited => 'Приглашено';
@override
String get inviteStatConverted => 'Конвертировано';
@override
String get inviteStatDays => 'Всего получено (дней)';
@override
String get inviteCodeFieldLabel => 'Код приглашения (необязательно)';
@override
String get notifTitle => 'Уведомления';
@override
String get notifEmpty => 'Пока нет уведомлений';
@override
String get notifTypeNews => 'Новости';
@override
String get notifTypeFeature => 'Функции';
@override
String get notifTypeImportant => 'Важное';
@override
String get notifTypeReward => 'Начислено';
@override
String get notifTypeVersion => 'Обновление';
@override
String get notifTypePromo => 'Акция';
@override
String get settingsGroupConn => 'Подключение';
@override
String get settingsGroupAppearance => 'Внешний вид';
@override
String get settingsGroupAbout => 'О приложении';
@override
String get orderStatusExpired => 'Истёк';
@override
String get promoBadge => 'Спецпредложение';
@override
String get promoOriginalPrice => 'Было';
@override
String get routingRulesTitle => 'Правила маршрутизации';
@override
String get routingRulesSub => 'Настройка: напрямую / туннель / отклонить';
@override
String get routingLanDirect => 'Локальная и частная сеть напрямую';
@override
String get routingLanForced => 'Всегда включено';
@override
String get routingForcedPill => '🔒 Принудительно';
@override
String get routingProxyMode => 'Режим прокси';
@override
String get routingModeGlobal => 'Глобально';
@override
String get routingModeSmart => 'Умный';
@override
String get routingModeDirect => 'Напрямую';
@override
String get routingBuiltin => 'Встроенные правила';
@override
String get routingCnDirect => 'Прямой доступ внутри страны';
@override
String get routingMyRulesOrdered => 'Мои правила · по порядку, срабатывает первое совпадение';
@override
String get routingMyRulesFirst => 'Мои правила · срабатывает первое совпадение';
@override
String get routingFinalLong => 'Итог (ни одно правило не совпало)';
@override
String get routingFinalShort => 'Итог (нет совпадений)';
@override
String get routingFinalFallback => 'FINAL — резерв';
@override
String get routingTypeDomainSuffix => 'Суффикс домена';
@override
String get routingTypeDomainSuffixCi => 'Суффикс домена · CI-источник';
@override
String get routingTypeDomainSuffixMirror => 'Суффикс домена · зеркало';
@override
String get routingTypeGeosite => 'Набор правил GeoSite';
@override
String get routingTypeDomain => 'Домен';
@override
String get routingTypeDomainKeyword => 'Ключевое слово домена';
@override
String get routingTypeIpCidr => 'Диапазон IP (CIDR)';
@override
String get routingTypeGeoip => 'Набор правил GeoIP';
@override
String get routingActDirect => 'Напрямую';
@override
String get routingActProxy => 'Туннель';
@override
String get routingActReject => 'Отклонить';
@override
String get routingForcedRow => '⚠ Домен принудительно направлен через туннель системой; правило неактивно';
@override
String get routingRuleShadowed => 'Перекрыто правилом выше';
@override
String get routingModeNote => '«Мои правила» действуют только в режиме умной маршрутизации. Глобальный прокси / прямое соединение игнорируют пользовательские правила (принудительные системные правила по-прежнему применяются).';
@override
String get routingRuleValueHint => 'например, example.com или 192.168.1.0/24';
@override
String get routingNoRules => 'Пользовательских правил пока нет';
@override
String get routingAddRule => 'Добавить правило';
@override
String get routingSaveFailed => 'Не удалось сохранить, попробуйте ещё раз';
@override
String get routingRuleValueInvalid => 'Неверный формат';
@override
String get routingImport => 'Импорт из текста';
@override
String get routingReset => 'Сбросить настройки';
@override
String get routingResetConfirmTitle => 'Сбросить к правилам по умолчанию?';
@override
String get routingResetConfirmBody => 'Это удалит все ваши пользовательские правила и восстановит заводскую таблицу маршрутизации.';
@override
String get routingResetConfirmAction => 'Сбросить';
@override
String get routingBack => 'Назад к настройкам';
@override
List<String> get days7 => const ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
@override
List<String> get featsFree => const ['Только 1 базовый узел', '10 минут в день', 'Смотрите рекламу, чтобы начать', '7 дней бесплатно при регистрации'];
@override
List<String> get featsPro => const ['80+ локаций', 'Безлимитно · быстро', '5 устройств', 'Оптимизация для стриминга'];
@override
List<String> get featsTeam => const ['Всё из Pro', '10 мест', 'Единый счёт', 'Приоритетная поддержка'];
@override
String adRewarded(int minutes) => '+$minutes мин добавлено';
@override
String nodeSwitchTitle(String name) => 'Переключиться на $name?';
@override
String autoConnectingTo(String name) => 'Автоподключение к $name';
@override
String updateAvailableTitle(String version) => 'Доступна новая версия v$version';
@override
String codeSentTo(String email) => 'Код отправлен на $email';
}