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
20 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_es.dart — Español文案(自动生成 · 勿手改)
//
// 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 StringsEs extends AppText {
const StringsEs();
@override
AppLang get lang => AppLang.es;
@override
String get brand => 'Pangolin';
@override
String get online => '● En línea';
@override
String get offline => '○ Desconectado';
@override
String get capOff => 'Toca para conectar';
@override
String get capConnecting => 'Conectando…';
@override
String get capOn => 'Conectado · Cifrado';
@override
String get connectNow => 'CONECTAR';
@override
String get secure => 'SEGURO';
@override
String get tabConnect => 'Conectar';
@override
String get tabServers => 'Servidores';
@override
String get tabStats => 'Estadísticas';
@override
String get tabMe => 'Cuenta';
@override
String get currentNode => 'Nodo actual';
@override
String get download => 'Bajada';
@override
String get upload => 'Subida';
@override
String get latency => 'Ping';
@override
String get quotaToday => 'Restante hoy';
@override
String get minutes => 'min';
@override
String get quotaFree => 'Gratis · 10 min/día';
@override
String get planFreeTag => 'Gratis';
@override
String get watchAd => 'Mira un anuncio para empezar';
@override
String get adUnlocked => 'Desbloqueado por hoy';
@override
String get quotaLeftLabel => 'Restante';
@override
String get quotaUsedUp => 'Tiempo gratis diario agotado';
@override
String get watchAdMore => 'Mira un anuncio para más tiempo';
@override
String get quotaExhaustedNotice => 'Tiempo gratis diario agotado. Mejora tu plan para seguir conectado.';
@override
String get adPlaying => 'Reproduciendo anuncio…';
@override
String get adFailed => 'No se pudo añadir tiempo, inténtalo de nuevo';
@override
String get quotaDesktopTitle => 'Tiempo gratis diario agotado';
@override
String get quotaDesktopBody => 'Mira anuncios en la app móvil para añadir tiempo, o mejora tu plan para acceso ilimitado.';
@override
String get gotIt => 'Entendido';
@override
String get chooseNode => 'Elegir servidor';
@override
String get searchPh => 'Buscar país / ciudad';
@override
String get smartSelect => 'Selección inteligente';
@override
String get nodeSwitchBody => 'Esto desconecta el enlace actual y reconecta a este nodo.';
@override
String get nodeSwitchConfirm => 'Cambiar';
@override
String get nodeUnhealthySwitched => 'Nodo actual inaccesible: cambiado automáticamente';
@override
String get nodeUnhealthyError => 'El nodo actual está inaccesible: reconecta o cambia de nodo';
@override
String get nodeReconnecting => 'Red inestable, reconectando…';
@override
String get routingRulesReconnecting => 'Reglas actualizadas, reconectando…';
@override
String get smartSub => 'Elige el mejor nodo para tu red';
@override
String get recommended => 'Recomendado';
@override
String get statsTitle => 'Estadísticas';
@override
String get trafficMonth => 'Tráfico';
@override
String get avgPing => 'Ping medio';
@override
String get durMonth => 'Tiempo';
@override
String get weekTraffic => 'Esta semana (GB)';
@override
String get byDevice => 'Por dispositivo';
@override
String get noDeviceUsage => 'Aún no hay uso por dispositivo';
@override
String get periodMonth => 'Este mes';
@override
String get periodWeek => 'Esta semana';
@override
String get periodToday => 'Hoy';
@override
String get statDown => 'Descarga';
@override
String get statUp => 'Subida';
@override
String get statDuration => 'Duración';
@override
String get chartTwoWeeks => 'Últimas 2 semanas';
@override
String get allDevices => 'Todos los dispositivos';
@override
String get meTitle => 'Cuenta';
@override
String get proMember => 'Miembro PRO';
@override
String get freePlanName => 'Gratis';
@override
String get expires => 'Vence';
@override
String get upgradeBtn => 'Renovar / Mejorar';
@override
String get accInfoTitle => 'Información de la cuenta';
@override
String get accEmail => 'Correo';
@override
String get accPassword => 'Contraseña';
@override
String get accChange => 'Cambiar';
@override
String get deviceLimitTitle => 'Límite de dispositivos alcanzado';
@override
String get deviceLimitDesc => 'Tu plan permite hasta %s dispositivos. Elimina uno para continuar.';
@override
String get deviceLimitRemoveOldest => 'Eliminar el menos usado recientemente';
@override
String get myDevices => 'Mis dispositivos';
@override
String get devicesSub => 'Hasta 3 dispositivos en PRO';
@override
String get thisDevice => 'Este dispositivo';
@override
String get remove => 'Eliminar';
@override
String get devOnline => 'En línea';
@override
String get devOffline => 'Desconectado';
@override
String get devLastOnline => 'Última conexión';
@override
String get devNeverLogin => 'Nunca ha iniciado sesión';
@override
String get devForceLogout => 'Forzar cierre de sesión';
@override
String get devClearLogin => 'Borrar datos de sesión';
@override
String get devRename => 'Renombrar';
@override
String get devRenameHint => 'Nombre del dispositivo';
@override
String get devSave => 'Guardar';
@override
String get devForceLogoutConfirm => 'Esto revoca la sesión del dispositivo: será desconectado y deberá iniciar sesión de nuevo. El dispositivo permanece en la lista.';
@override
String get devClearLoginConfirm => 'Esto elimina el dispositivo de la lista por completo y revoca su sesión y su credencial del plano de datos. El dispositivo deberá iniciar sesión de nuevo para poder usarse.';
@override
String get devCancel => 'Cancelar';
@override
String get sessionRevokedTitle => 'Se cerró la sesión de este dispositivo';
@override
String get sessionRevokedBody => 'Tu cuenta cerró la sesión de este dispositivo desde otro dispositivo. Inicia sesión de nuevo.';
@override
String get sessionRevokedOk => 'Iniciar sesión de nuevo';
@override
String get redeemEntry => 'Canjear';
@override
String get contactEntry => 'Contáctanos';
@override
String get signOut => 'Cerrar sesión';
@override
String get language => 'Idioma';
@override
String get darkAppearance => 'Apariencia oscura';
@override
String get stateOn => 'Activado';
@override
String get followLight => 'Desactivado';
@override
String get protocol => 'Protocolo';
@override
String get settingsTitle => 'Ajustes';
@override
String get autostart => 'Iniciar al arrancar';
@override
String get autostartSub => 'Ejecutar automáticamente al iniciar el sistema';
@override
String get autoConnect => 'Conexión automática';
@override
String get autoConnectSub => 'Conectar automáticamente al abrir';
@override
String get smartRoute => 'Enrutamiento inteligente';
@override
String get smartRouteSub => 'Acelera en el extranjero, directo en casa';
@override
String get killSwitch => 'Kill Switch';
@override
String get killSwitchSub => 'Bloquea el tráfico si se cae el enlace';
@override
String get checkUpdate => 'Buscar actualizaciones';
@override
String get webUserCenter => 'Centro de usuario (Web)';
@override
String get updateNotesFallback => 'Esta actualización incluye correcciones y mejoras de estabilidad.';
@override
String get updateLater => 'Más tarde';
@override
String get updateDownload => 'Descargar actualización';
@override
String get updateUpToDate => 'Ya tienes la última versión';
@override
String get updateCheckFailed => 'Falló la búsqueda de actualizaciones, inténtalo más tarde';
@override
String get freePlan => 'Gratis';
@override
String get proPlan => 'Pro';
@override
String get teamPlan => 'Equipo';
@override
String get perMonth => '/mes';
@override
String get current => 'Actual';
@override
String get upgrade => 'Mejorar';
@override
String get choose => 'Elegir';
@override
String get mostPopular => 'Popular';
@override
String get redeemTitle => 'Canjear y comprar';
@override
String get redeemCodeTitle => 'Canjear un código';
@override
String get redeemPh => 'Introduce el código de activación';
@override
String get redeemBtn => 'Canjear';
@override
String get redeemOk => 'Activado · PRO desbloqueado';
@override
String get buyTitle => 'Dónde comprar';
@override
String get buySub => 'El pago dentro de la app no está disponible. Consigue un código mediante:';
@override
String get chStore => 'Tienda autoservicio';
@override
String get chEmail => 'Soporte por correo';
@override
String get contactTitle => 'Contáctanos';
@override
String get contactIntro => '¿Necesitas ayuda? Escríbenos por cualquiera de los canales de abajo: solemos responder en minutos.';
@override
String get contactEmail => 'Soporte por correo';
@override
String get contactStore => 'Tienda autoservicio';
@override
String get contactWebsite => 'Sitio web';
@override
String get contactHoursTitle => 'Horario de soporte';
@override
String get contactHours => 'Todos los días 9:00 24:00 (GMT+8)';
@override
String get authTagline => 'Rápido · Estable · Sin complicaciones';
@override
String get tabLogin => 'Iniciar sesión';
@override
String get tabRegister => 'Registrarse';
@override
String get emailLabel => 'Correo';
@override
String get emailPh => 'tu@correo.com';
@override
String get pwLabel => 'Contraseña';
@override
String get pwPh => 'Introduce la contraseña';
@override
String get setPwPh => 'Crea una contraseña (para acceso multidispositivo)';
@override
String get codeLabel => 'Código de verificación';
@override
String get sendCode => 'Enviar código';
@override
String get resend => 'Reenviar';
@override
String get doLogin => 'Iniciar sesión';
@override
String get doNext => 'Siguiente';
@override
String get doCreate => 'Crear cuenta';
@override
String get forgotPw => '¿Olvidaste la contraseña?';
@override
String get tos => 'Al continuar aceptas nuestros Términos y Política de privacidad';
@override
String get obSkip => 'Omitir';
@override
String get obNext => 'Siguiente';
@override
String get obAllow => 'Permitir y continuar';
@override
String get obStart => 'Empezar';
@override
String get ob1Title => 'Conéctate en todo el mundo';
@override
String get ob1Sub => 'Más de 80 ubicaciones, enrutamiento al más rápido automático, sólido como una roca.';
@override
String get ob2Title => 'Permite la configuración de red';
@override
String get ob2Sub => 'El sistema te pedirá añadir un perfil de red para crear el túnel cifrado.';
@override
String get ob3Title => 'Privado por diseño';
@override
String get ob3Sub => 'Cifrado de extremo a extremo. No guardamos ningún registro de navegación.';
@override
String get purchaseTitle => 'Comprar plan';
@override
String get paymentTitle => 'Pago';
@override
String get buyNow => 'Comprar ahora';
@override
String get payMethodAlipay => 'Alipay';
@override
String get payMethodCrypto => 'USDT (TRC20)';
@override
String get choosePayMethod => 'Elige un método de pago';
@override
String get proMonthly => 'Pro · Mensual';
@override
String get proQuarterly => 'Pro · Trimestral';
@override
String get proYearly => 'Pro · Anual';
@override
String get perQuarter => '/trimestre';
@override
String get perYear => '/año';
@override
String get payAmountLabel => 'Importe a transferir';
@override
String get payAddressLabel => 'Dirección de pago';
@override
String get payNetworkLabel => 'Red';
@override
String get payExactAmountHint => 'Envía el importe exacto; se activa automáticamente';
@override
String get copied => 'Copiado';
@override
String get openAlipay => 'Pagar con Alipay';
@override
String get openAlipayHint => 'Vuelve tras pagar; esta página se actualiza automáticamente';
@override
String get openAlipayFailed => 'No se pudo abrir Alipay; comprueba que esté instalado o inténtalo de nuevo';
@override
String get awaitingPayment => 'Esperando el pago';
@override
String get paySucceeded => 'Activado';
@override
String get payExpiresAt => 'Válido hasta';
@override
String get payDone => 'Listo';
@override
String get payFailed => 'El pago falló';
@override
String get payRetry => 'Reintentar';
@override
String get switchPayMethod => 'Cambiar método de pago';
@override
String get cancelOrder => 'Cancelar pedido';
@override
String get qrNotSupported => 'Copia el contenido y ábrelo en Alipay';
@override
String get orderCreateFailed => 'Error al crear el pedido, inténtalo de nuevo más tarde';
@override
String get payUnsupportedRenderType => 'No compatible, cambia el método de pago';
@override
String get payNoActiveOrder => 'No hay pedidos activos';
@override
String get payChannel => 'Canal de pago';
@override
String get payChannelUsdt => 'USDT · Cripto';
@override
String get payChannelCny => 'CNY · Alipay';
@override
String get perMonthSuffix => ' /mes';
@override
String get ordersTitle => 'Mis pedidos';
@override
String get orderDetailTitle => 'Detalles del pedido';
@override
String get ordersEmpty => 'Aún no hay pedidos';
@override
String get orderNoLabel => 'N.º de pedido';
@override
String get orderPlanLabel => 'Plan';
@override
String get orderMethodLabel => 'Método de pago';
@override
String get orderAmountLabel => 'Importe';
@override
String get orderCreatedLabel => 'Creado';
@override
String get orderPaidLabel => 'Pagado';
@override
String get orderStatusPaid => 'Activado';
@override
String get orderStatusCanceled => 'Cancelado';
@override
String get orderContinuePay => 'Continuar pago';
@override
String get orderRefresh => 'Actualizar';
@override
String get orderRebuy => 'Comprar de nuevo';
@override
String get payMethodNezha => 'Nazhapay';
@override
String get inviteEntry => 'Invita a amigos y gana membresía';
@override
String get inviteEntrySub => 'El amigo se registra · ambos ganan días';
@override
String get inviteTitle => 'Invitar amigos';
@override
String get inviteCodeLabel => 'Código de invitación';
@override
String get inviteLinkLabel => 'Enlace de invitación';
@override
String get inviteCopyCode => 'Copiar código';
@override
String get inviteCopyLink => 'Copiar enlace';
@override
String get inviteRewardsTitle => 'Reglas de recompensas';
@override
String get inviteRewardRegister => 'Registro de amigo · +3 días cada uno';
@override
String get inviteRewardFirstBuy => 'Primera compra del amigo · +7 días cada uno';
@override
String get inviteRewardJoinTg => 'Unirse al canal de TG · +3 días';
@override
String get inviteProgressTitle => 'Mis recompensas';
@override
String get inviteTasksTitle => 'Más formas de ganar';
@override
String get inviteJoinTgTitle => 'Unirse al canal de Telegram';
@override
String get inviteJoinTgBtn => 'Unirse al canal';
@override
String get inviteVerifyBtn => 'Verificar y reclamar';
@override
String get inviteTaskDone => 'Reclamado +3 días';
@override
String get inviteStatInvited => 'Invitados';
@override
String get inviteStatConverted => 'Convertidos';
@override
String get inviteStatDays => 'Días acumulados';
@override
String get inviteCodeFieldLabel => 'Código de invitación (opcional)';
@override
String get notifTitle => 'Notificaciones';
@override
String get notifEmpty => 'Aún no hay notificaciones';
@override
String get notifTypeNews => 'Noticias';
@override
String get notifTypeFeature => 'Función';
@override
String get notifTypeImportant => 'Importante';
@override
String get notifTypeReward => 'Acreditado';
@override
String get notifTypeVersion => 'Actualización';
@override
String get notifTypePromo => 'Promoción';
@override
String get settingsGroupConn => 'Conexión';
@override
String get settingsGroupAppearance => 'Apariencia';
@override
String get settingsGroupAbout => 'Acerca de';
@override
String get orderStatusExpired => 'Caducado';
@override
String get promoBadge => 'Oferta especial';
@override
String get promoOriginalPrice => 'Antes';
@override
String get routingRulesTitle => 'Reglas de enrutamiento';
@override
String get routingRulesSub => 'Personalizar: directo / túnel / rechazar';
@override
String get routingLanDirect => 'LAN y red privada directas';
@override
String get routingLanForced => 'Siempre activo';
@override
String get routingForcedPill => '🔒 Forzado';
@override
String get routingProxyMode => 'Modo de proxy';
@override
String get routingModeGlobal => 'Global';
@override
String get routingModeSmart => 'Inteligente';
@override
String get routingModeDirect => 'Directo';
@override
String get routingBuiltin => 'Reglas integradas';
@override
String get routingCnDirect => 'Directo nacional';
@override
String get routingMyRulesOrdered => 'Mis reglas · en orden, gana la primera coincidencia';
@override
String get routingMyRulesFirst => 'Mis reglas · gana la primera coincidencia';
@override
String get routingFinalLong => 'Final (ninguna regla coincide)';
@override
String get routingFinalShort => 'Final (sin coincidencia)';
@override
String get routingFinalFallback => 'FINAL — reserva';
@override
String get routingTypeDomainSuffix => 'Sufijo de dominio';
@override
String get routingTypeDomainSuffixCi => 'Sufijo de dominio · fuente CI';
@override
String get routingTypeDomainSuffixMirror => 'Sufijo de dominio · espejo';
@override
String get routingTypeGeosite => 'Conjunto de reglas GeoSite';
@override
String get routingTypeDomain => 'Dominio';
@override
String get routingTypeDomainKeyword => 'Palabra clave de dominio';
@override
String get routingTypeIpCidr => 'Rango de IP (CIDR)';
@override
String get routingTypeGeoip => 'Conjunto de reglas GeoIP';
@override
String get routingActDirect => 'Directo';
@override
String get routingActProxy => 'Túnel';
@override
String get routingActReject => 'Rechazar';
@override
String get routingForcedRow => '⚠ El sistema fuerza el túnel para este dominio; esta regla no se aplica';
@override
String get routingRuleShadowed => 'Sustituida por una regla anterior';
@override
String get routingModeNote => '"Mis reglas" solo se aplican en modo Inteligente. El modo Global / Directo ignora las reglas personalizadas (las reglas forzadas por el sistema siguen aplicándose).';
@override
String get routingRuleValueHint => 'p. ej. example.com o 192.168.1.0/24';
@override
String get routingNoRules => 'Aún no hay reglas personalizadas';
@override
String get routingAddRule => 'Añadir regla';
@override
String get routingSaveFailed => 'Error al guardar, inténtalo de nuevo';
@override
String get routingRuleValueInvalid => 'Formato no válido';
@override
String get routingImport => 'Importar desde texto';
@override
String get routingReset => 'Restablecer';
@override
String get routingResetConfirmTitle => '¿Restablecer las reglas predeterminadas?';
@override
String get routingResetConfirmBody => 'Esto borra todas tus reglas personalizadas y restaura la tabla de enrutamiento de fábrica.';
@override
String get routingResetConfirmAction => 'Restablecer';
@override
String get routingBack => 'Volver a ajustes';
@override
List<String> get days7 => const ['L', 'M', 'X', 'J', 'V', 'S', 'D'];
@override
List<String> get featsFree => const ['Solo 1 nodo básico', '10 min al día', 'Mira un anuncio para empezar', 'Prueba gratis de 7 días al registrarte'];
@override
List<String> get featsPro => const ['Más de 80 ubicaciones', 'Ilimitado · rápido', '5 dispositivos', 'Optimizado para streaming'];
@override
List<String> get featsTeam => const ['Todo lo de Pro', '10 plazas', 'Facturación centralizada', 'Soporte prioritario'];
@override
String adRewarded(int minutes) => '+$minutes min añadidos';
@override
String nodeSwitchTitle(String name) => '¿Cambiar a $name?';
@override
String autoConnectingTo(String name) => 'Conectando automáticamente a $name';
@override
String updateAvailableTitle(String version) => 'Nueva versión v$version disponible';
@override
String codeSentTo(String email) => 'Código enviado a $email';
}