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
21 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_ko.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 StringsKo extends AppText {
const StringsKo();
@override
AppLang get lang => AppLang.ko;
@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 => '이번 주 (GB)';
@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 => 'PRO는 최대 3대';
@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 => 'Team';
@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 => 'LAN / 사설망 직접 연결';
@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(으)로 코드를 보냈습니다';
}