Files
pangolin/client/lib/l10n/strings_en.dart
T
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
19 KiB
Dart
Raw 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_en.dart — English文案(自动生成 · 勿手改)
//
// 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 StringsEn extends AppText {
const StringsEn();
@override
AppLang get lang => AppLang.en;
@override
String get brand => 'Pangolin';
@override
String get online => '● Online';
@override
String get offline => '○ Offline';
@override
String get capOff => 'Tap to connect';
@override
String get capConnecting => 'Connecting…';
@override
String get capOn => 'Connected · Encrypted';
@override
String get connectNow => 'CONNECT';
@override
String get secure => 'SECURE';
@override
String get tabConnect => 'Connect';
@override
String get tabServers => 'Servers';
@override
String get tabStats => 'Stats';
@override
String get tabMe => 'Account';
@override
String get currentNode => 'Current node';
@override
String get download => 'Down';
@override
String get upload => 'Up';
@override
String get latency => 'Ping';
@override
String get quotaToday => 'Left today';
@override
String get minutes => 'min';
@override
String get quotaFree => 'Free · 10 min/day';
@override
String get planFreeTag => 'Free';
@override
String get watchAd => 'Watch ad to start';
@override
String get adUnlocked => 'Unlocked for today';
@override
String get quotaLeftLabel => 'Left';
@override
String get quotaUsedUp => 'Daily free time used up';
@override
String get watchAdMore => 'Watch ad for more time';
@override
String get quotaExhaustedNotice => 'You\'ve used today\'s free time. Upgrade to keep connecting.';
@override
String get adPlaying => 'Ad playing…';
@override
String get adFailed => 'Failed to add time, please retry';
@override
String get quotaDesktopTitle => 'Daily free time used up';
@override
String get quotaDesktopBody => 'Watch ads in the mobile app to add time, or upgrade for unlimited access.';
@override
String get gotIt => 'Got it';
@override
String get chooseNode => 'Choose server';
@override
String get searchPh => 'Search country / city';
@override
String get smartSelect => 'Smart select';
@override
String get nodeSwitchBody => 'This disconnects the current link and reconnects to this node.';
@override
String get nodeSwitchConfirm => 'Switch';
@override
String get nodeUnhealthySwitched => 'Current node unreachable — switched automatically';
@override
String get nodeUnhealthyError => 'Current node is unreachable — reconnect or switch node';
@override
String get nodeReconnecting => 'Network unstable, reconnecting…';
@override
String get routingRulesReconnecting => 'Rules updated, reconnecting…';
@override
String get smartSub => 'Picks the best node for your network';
@override
String get recommended => 'Recommended';
@override
String get statsTitle => 'Statistics';
@override
String get trafficMonth => 'Traffic';
@override
String get avgPing => 'Avg ping';
@override
String get durMonth => 'Time';
@override
String get weekTraffic => 'This week (GB)';
@override
String get byDevice => 'By device';
@override
String get noDeviceUsage => 'No device usage yet';
@override
String get periodMonth => 'This month';
@override
String get periodWeek => 'This week';
@override
String get periodToday => 'Today';
@override
String get statDown => 'Download';
@override
String get statUp => 'Upload';
@override
String get statDuration => 'Duration';
@override
String get chartTwoWeeks => 'Last 2 weeks';
@override
String get allDevices => 'All devices';
@override
String get meTitle => 'Account';
@override
String get proMember => 'PRO member';
@override
String get freePlanName => 'Free';
@override
String get expires => 'Expires';
@override
String get upgradeBtn => 'Renew / Upgrade';
@override
String get accInfoTitle => 'Account info';
@override
String get accEmail => 'Email';
@override
String get accPassword => 'Password';
@override
String get accChange => 'Change';
@override
String get deviceLimitTitle => 'Device limit reached';
@override
String get deviceLimitDesc => 'Your plan allows up to %s devices. Remove one to continue.';
@override
String get deviceLimitRemoveOldest => 'Remove least recently used';
@override
String get myDevices => 'My devices';
@override
String get devicesSub => 'Up to 3 devices on PRO';
@override
String get thisDevice => 'This device';
@override
String get remove => 'Remove';
@override
String get devOnline => 'Online';
@override
String get devOffline => 'Offline';
@override
String get devLastOnline => 'Last online';
@override
String get devNeverLogin => 'Never logged in';
@override
String get devForceLogout => 'Force logout';
@override
String get devClearLogin => 'Clear login info';
@override
String get devRename => 'Rename';
@override
String get devRenameHint => 'Device name';
@override
String get devSave => 'Save';
@override
String get devForceLogoutConfirm => 'This revokes the device\'s login session — it will be kicked offline and must sign in again. The device stays in the list.';
@override
String get devClearLoginConfirm => 'This removes the device from the list entirely and revokes its login session and data-plane credential. The device must sign in again to be used.';
@override
String get devCancel => 'Cancel';
@override
String get sessionRevokedTitle => 'This device was signed out';
@override
String get sessionRevokedBody => 'Your account signed this device out from another device. Please sign in again.';
@override
String get sessionRevokedOk => 'Sign in again';
@override
String get redeemEntry => 'Redeem';
@override
String get contactEntry => 'Contact us';
@override
String get signOut => 'Sign out';
@override
String get language => 'Language';
@override
String get darkAppearance => 'Dark appearance';
@override
String get stateOn => 'On';
@override
String get followLight => 'Off';
@override
String get protocol => 'Protocol';
@override
String get settingsTitle => 'Settings';
@override
String get autostart => 'Launch at startup';
@override
String get autostartSub => 'Run automatically on system start';
@override
String get autoConnect => 'Auto-connect';
@override
String get autoConnectSub => 'Connect automatically on launch';
@override
String get smartRoute => 'Smart routing';
@override
String get smartRouteSub => 'Accelerate abroad, direct at home';
@override
String get killSwitch => 'Kill Switch';
@override
String get killSwitchSub => 'Block traffic if the link drops';
@override
String get checkUpdate => 'Check for updates';
@override
String get webUserCenter => 'User Center (Web)';
@override
String get updateNotesFallback => 'This update includes fixes and stability improvements.';
@override
String get updateLater => 'Later';
@override
String get updateDownload => 'Download update';
@override
String get updateUpToDate => 'You are up to date';
@override
String get updateCheckFailed => 'Update check failed, try again later';
@override
String get freePlan => 'Free';
@override
String get proPlan => 'Pro';
@override
String get teamPlan => 'Team';
@override
String get perMonth => '/mo';
@override
String get current => 'Current';
@override
String get upgrade => 'Upgrade';
@override
String get choose => 'Choose';
@override
String get mostPopular => 'Popular';
@override
String get redeemTitle => 'Redeem & buy';
@override
String get redeemCodeTitle => 'Redeem a code';
@override
String get redeemPh => 'Enter activation code';
@override
String get redeemBtn => 'Redeem';
@override
String get redeemOk => 'Activated · PRO unlocked';
@override
String get buyTitle => 'Where to buy';
@override
String get buySub => 'In-app payment is unavailable. Get a code via:';
@override
String get chStore => 'Self-serve store';
@override
String get chEmail => 'Email support';
@override
String get contactTitle => 'Contact us';
@override
String get contactIntro => 'Need help? Reach us via any channel below — usually replies in minutes.';
@override
String get contactEmail => 'Email support';
@override
String get contactStore => 'Self-serve store';
@override
String get contactWebsite => 'Website';
@override
String get contactHoursTitle => 'Support hours';
@override
String get contactHours => 'Daily 9:00 24:00 (GMT+8)';
@override
String get authTagline => 'Fast · Stable · Effortless';
@override
String get tabLogin => 'Log in';
@override
String get tabRegister => 'Sign up';
@override
String get emailLabel => 'Email';
@override
String get emailPh => 'your@email.com';
@override
String get pwLabel => 'Password';
@override
String get pwPh => 'Enter password';
@override
String get setPwPh => 'Set a password (for multi-device login)';
@override
String get codeLabel => 'Verification code';
@override
String get sendCode => 'Send code';
@override
String get resend => 'Resend';
@override
String get doLogin => 'Log in';
@override
String get doNext => 'Next';
@override
String get doCreate => 'Create account';
@override
String get forgotPw => 'Forgot password?';
@override
String get tos => 'By continuing you agree to our Terms & Privacy Policy';
@override
String get obSkip => 'Skip';
@override
String get obNext => 'Next';
@override
String get obAllow => 'Allow & continue';
@override
String get obStart => 'Get started';
@override
String get ob1Title => 'Connect worldwide';
@override
String get ob1Sub => '80+ locations, auto-fastest routing, rock-solid.';
@override
String get ob2Title => 'Allow network setup';
@override
String get ob2Sub => 'The system will ask to add a network profile to build the encrypted tunnel.';
@override
String get ob3Title => 'Private by design';
@override
String get ob3Sub => 'End-to-end encrypted. We keep zero browsing logs.';
@override
String get purchaseTitle => 'Purchase';
@override
String get paymentTitle => 'Payment';
@override
String get buyNow => 'Buy now';
@override
String get payMethodAlipay => 'Alipay';
@override
String get payMethodCrypto => 'USDT (TRC20)';
@override
String get choosePayMethod => 'Choose payment method';
@override
String get proMonthly => 'Pro · Monthly';
@override
String get proQuarterly => 'Pro · Quarterly';
@override
String get proYearly => 'Pro · Yearly';
@override
String get perQuarter => '/quarter';
@override
String get perYear => '/year';
@override
String get payAmountLabel => 'Amount';
@override
String get payAddressLabel => 'Address';
@override
String get payNetworkLabel => 'Network';
@override
String get payExactAmountHint => 'Send the exact amount; activates automatically';
@override
String get copied => 'Copied';
@override
String get openAlipay => 'Pay with Alipay';
@override
String get openAlipayHint => 'Return after paying; this page refreshes automatically';
@override
String get openAlipayFailed => 'Couldn\'t open Alipay; check it\'s installed or try again';
@override
String get awaitingPayment => 'Awaiting payment';
@override
String get paySucceeded => 'Activated';
@override
String get payExpiresAt => 'Valid until';
@override
String get payDone => 'Done';
@override
String get payFailed => 'Payment failed';
@override
String get payRetry => 'Retry';
@override
String get switchPayMethod => 'Switch payment method';
@override
String get cancelOrder => 'Cancel order';
@override
String get qrNotSupported => 'Copy and open in Alipay';
@override
String get orderCreateFailed => 'Failed to create order, please retry';
@override
String get payUnsupportedRenderType => 'Unsupported, switch payment method';
@override
String get payNoActiveOrder => 'No active order';
@override
String get payChannel => 'Payment channel';
@override
String get payChannelUsdt => 'USDT · Crypto';
@override
String get payChannelCny => 'CNY · Alipay';
@override
String get perMonthSuffix => ' /mo';
@override
String get ordersTitle => 'My orders';
@override
String get orderDetailTitle => 'Order details';
@override
String get ordersEmpty => 'No orders yet';
@override
String get orderNoLabel => 'Order no.';
@override
String get orderPlanLabel => 'Plan';
@override
String get orderMethodLabel => 'Payment method';
@override
String get orderAmountLabel => 'Amount';
@override
String get orderCreatedLabel => 'Created';
@override
String get orderPaidLabel => 'Paid at';
@override
String get orderStatusPaid => 'Activated';
@override
String get orderStatusCanceled => 'Canceled';
@override
String get orderContinuePay => 'Continue payment';
@override
String get orderRefresh => 'Refresh';
@override
String get orderRebuy => 'Buy again';
@override
String get payMethodNezha => 'Nazhapay';
@override
String get inviteEntry => 'Invite friends, earn membership';
@override
String get inviteEntrySub => 'Friend signs up · both earn days';
@override
String get inviteTitle => 'Invite friends';
@override
String get inviteCodeLabel => 'Invite code';
@override
String get inviteLinkLabel => 'Invite link';
@override
String get inviteCopyCode => 'Copy code';
@override
String get inviteCopyLink => 'Copy link';
@override
String get inviteRewardsTitle => 'Rewards';
@override
String get inviteRewardRegister => 'Sign up · +3 days each';
@override
String get inviteRewardFirstBuy => 'First purchase · +7 days each';
@override
String get inviteRewardJoinTg => 'Join TG · +3 days';
@override
String get inviteProgressTitle => 'My rewards';
@override
String get inviteTasksTitle => 'More ways to earn';
@override
String get inviteJoinTgTitle => 'Join Telegram channel';
@override
String get inviteJoinTgBtn => 'Join channel';
@override
String get inviteVerifyBtn => 'Verify & claim';
@override
String get inviteTaskDone => 'Claimed +3 days';
@override
String get inviteStatInvited => 'Invited';
@override
String get inviteStatConverted => 'Converted';
@override
String get inviteStatDays => 'Days earned';
@override
String get inviteCodeFieldLabel => 'Invite code (optional)';
@override
String get notifTitle => 'Notifications';
@override
String get notifEmpty => 'No notifications';
@override
String get notifTypeNews => 'News';
@override
String get notifTypeFeature => 'Feature';
@override
String get notifTypeImportant => 'Important';
@override
String get notifTypeReward => 'Credited';
@override
String get notifTypeVersion => 'Update';
@override
String get notifTypePromo => 'Promo';
@override
String get settingsGroupConn => 'Connection';
@override
String get settingsGroupAppearance => 'Appearance';
@override
String get settingsGroupAbout => 'About';
@override
String get orderStatusExpired => 'Expired';
@override
String get promoBadge => 'Limited-time offer';
@override
String get promoOriginalPrice => 'Was';
@override
String get routingRulesTitle => 'Routing rules';
@override
String get routingRulesSub => 'Custom direct / tunnel / reject';
@override
String get routingLanDirect => 'LAN & private direct';
@override
String get routingLanForced => 'Always on';
@override
String get routingForcedPill => '🔒 Forced';
@override
String get routingProxyMode => 'Proxy mode';
@override
String get routingModeGlobal => 'Global';
@override
String get routingModeSmart => 'Smart';
@override
String get routingModeDirect => 'Direct';
@override
String get routingBuiltin => 'Built-in rules';
@override
String get routingCnDirect => 'China direct';
@override
String get routingMyRulesOrdered => 'My rules · ordered, first match wins';
@override
String get routingMyRulesFirst => 'My rules · first match wins';
@override
String get routingFinalLong => 'Final (no rule matched)';
@override
String get routingFinalShort => 'Final (no match)';
@override
String get routingFinalFallback => 'FINAL fallback';
@override
String get routingTypeDomainSuffix => 'Domain suffix';
@override
String get routingTypeDomainSuffixCi => 'Domain suffix · CI source';
@override
String get routingTypeDomainSuffixMirror => 'Domain suffix · Mirror';
@override
String get routingTypeGeosite => 'GeoSite rule-set';
@override
String get routingTypeDomain => 'Domain';
@override
String get routingTypeDomainKeyword => 'Domain keyword';
@override
String get routingTypeIpCidr => 'IP range (CIDR)';
@override
String get routingTypeGeoip => 'GeoIP rule-set';
@override
String get routingActDirect => 'Direct';
@override
String get routingActProxy => 'Tunnel';
@override
String get routingActReject => 'Reject';
@override
String get routingForcedRow => '⚠ Forced to tunnel by the system; this rule is inactive';
@override
String get routingRuleShadowed => 'Overridden by a rule above';
@override
String get routingModeNote => '"My rules" only take effect in Smart mode. Global / Direct mode ignores custom rules entirely (system-forced rules still apply).';
@override
String get routingRuleValueHint => 'e.g. example.com or 192.168.1.0/24';
@override
String get routingNoRules => 'No custom rules yet';
@override
String get routingAddRule => 'Add rule';
@override
String get routingSaveFailed => 'Save failed, please try again';
@override
String get routingRuleValueInvalid => 'Invalid format';
@override
String get routingImport => 'Import from text';
@override
String get routingReset => 'Reset defaults';
@override
String get routingResetConfirmTitle => 'Reset to defaults?';
@override
String get routingResetConfirmBody => 'This clears all your custom rules and restores the factory routing table.';
@override
String get routingResetConfirmAction => 'Reset';
@override
String get routingBack => 'Back to settings';
@override
List<String> get days7 => const ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
@override
List<String> get featsFree => const ['1 basic node only', '10 min per day', 'Watch an ad to start', '7-day free trial on sign-up'];
@override
List<String> get featsPro => const ['80+ locations', 'Unlimited · fast', '5 devices', 'Streaming optimized'];
@override
List<String> get featsTeam => const ['Everything in Pro', '10 seats', 'Central billing', 'Priority support'];
@override
String adRewarded(int minutes) => '+$minutes min added';
@override
String nodeSwitchTitle(String name) => 'Switch to $name?';
@override
String autoConnectingTo(String name) => 'Auto-connecting to $name';
@override
String updateAvailableTitle(String version) => 'New version v$version available';
@override
String codeSentTo(String email) => 'Code sent to $email';
}