// app_text.dart — 文案契约(l10n 资源层) // // 产品任一时刻只显示中文【或】英文(设计铁律 §6 单语言显示),由 // `localeProvider` 段控切换。所有界面文案必须经此处取用,禁止在组件内 // 写死中/英字面量。zh / en 两份资源分别在 strings_zh.dart / strings_en.dart。 // // 红线词约束(设计铁律 §13):资源文件不得出现 VPN / 翻墙 / 科学上网 / // 突破封锁 / 自由穿越 / Go anywhere。CI `ci/scan-redline.sh` 对本目录做扫描。 // // 注意:这是手写资源(非 flutter gen-l10n 代码生成),保证离线可编译; // 接口形态与 .arb 一致,后续可平滑迁移到官方 l10n 工具链。 /// 受支持语言。单显——任一时刻只渲染其一。 /// zh 中文 · en English · ja 日本語 · ko 한국어 · ru Русский · es Español。 enum AppLang { zh, en, ja, ko, ru, es } /// 语言的本地名(用于语言切换 UI,各语言用自身文字标注)。 extension AppLangLabel on AppLang { String get nativeLabel { switch (this) { case AppLang.zh: return '中文'; case AppLang.en: return 'English'; case AppLang.ja: return '日本語'; case AppLang.ko: return '한국어'; case AppLang.ru: return 'Русский'; case AppLang.es: return 'Español'; } } } /// 少量「拿不到 AppText 实例的场景」(model / 非 Consumer 的 tile,只有 AppLang) /// 用到的零散文案。exhaustive switch 保证 6 语全覆盖,不会漏译回退英文。 /// 仍归属 l10n 层(scan-redline 覆盖本文件),不违反「文案集中」铁律。 extension AppLangMisc on AppLang { /// 节点不可用(agent 离线)。 String get unavailable { switch (this) { case AppLang.zh: return '不可用'; case AppLang.en: return 'Unavailable'; case AppLang.ja: return '利用不可'; case AppLang.ko: return '사용 불가'; case AppLang.ru: return 'Недоступно'; case AppLang.es: return 'No disponible'; } } /// 节点标签:流媒体优化。 String get tagStreaming { switch (this) { case AppLang.zh: return '流媒体优化'; case AppLang.en: return 'Streaming'; case AppLang.ja: return 'ストリーミング最適化'; case AppLang.ko: return '스트리밍 최적화'; case AppLang.ru: return 'Оптимизация стриминга'; case AppLang.es: return 'Optimización de streaming'; } } /// 节点尚未就绪(列表加载中/为空)。 String get nodesNotReady { switch (this) { case AppLang.zh: return '节点尚未就绪,请稍候重试'; case AppLang.en: return 'Nodes not ready, please retry'; case AppLang.ja: return 'ノードの準備ができていません。しばらくして再試行してください'; case AppLang.ko: return '노드가 아직 준비되지 않았습니다. 잠시 후 다시 시도하세요'; case AppLang.ru: return 'Узлы не готовы, повторите попытку'; case AppLang.es: return 'Nodos no listos, inténtalo de nuevo'; } } /// 连接失败(通用兜底)。 String get connectFailed { switch (this) { case AppLang.zh: return '连接失败,请重试'; case AppLang.en: return 'Connection failed, please retry'; case AppLang.ja: return '接続に失敗しました。再試行してください'; case AppLang.ko: return '연결에 실패했습니다. 다시 시도하세요'; case AppLang.ru: return 'Не удалось подключиться, повторите попытку'; case AppLang.es: return 'Error de conexión, inténtalo de nuevo'; } } /// 数据加载失败。 String get loadFailedRetry { switch (this) { case AppLang.zh: return '加载失败,请重试'; case AppLang.en: return 'Failed to load, retry'; case AppLang.ja: return '読み込みに失敗しました。再試行してください'; case AppLang.ko: return '불러오기에 실패했습니다. 다시 시도하세요'; case AppLang.ru: return 'Не удалось загрузить, повторите'; case AppLang.es: return 'Error al cargar, reintentar'; } } /// 无已登录设备。 String get noDevices { switch (this) { case AppLang.zh: return '暂无已登录设备'; case AppLang.en: return 'No devices yet'; case AppLang.ja: return 'ログイン済みのデバイスはありません'; case AppLang.ko: return '로그인된 기기가 없습니다'; case AppLang.ru: return 'Пока нет устройств'; case AppLang.es: return 'Aún no hay dispositivos'; } } /// 「改邮箱」按钮短标签。 String get changeEmail { switch (this) { case AppLang.zh: return '改邮箱'; case AppLang.en: return 'Change'; case AppLang.ja: return '変更'; case AppLang.ko: return '변경'; case AppLang.ru: return 'Изменить'; case AppLang.es: return 'Cambiar'; } } /// 相对时间(设备最后在线):刚刚 / X 分钟前 / X 小时前 / X 天前。 String relativeTime(Duration d) { final m = d.inMinutes, h = d.inHours, days = d.inDays; switch (this) { case AppLang.zh: if (m < 1) return '刚刚'; if (m < 60) return '$m 分钟前'; if (h < 24) return '$h 小时前'; return '$days 天前'; case AppLang.en: if (m < 1) return 'just now'; if (m < 60) return '$m min ago'; if (h < 24) return '${h}h ago'; return '${days}d ago'; case AppLang.ja: if (m < 1) return 'たった今'; if (m < 60) return '$m 分前'; if (h < 24) return '$h 時間前'; return '$days 日前'; case AppLang.ko: if (m < 1) return '방금'; if (m < 60) return '$m분 전'; if (h < 24) return '$h시간 전'; return '$days일 전'; case AppLang.ru: if (m < 1) return 'только что'; if (m < 60) return '$m мин назад'; if (h < 24) return '$h ч назад'; return '$days дн назад'; case AppLang.es: if (m < 1) return 'ahora mismo'; if (m < 60) return 'hace $m min'; if (h < 24) return 'hace $h h'; return 'hace $days d'; } } /// 更新下载进度:「正在下载 X%」。 String updateDownloadingPercent(int pct) { switch (this) { case AppLang.zh: return '正在下载 $pct%'; case AppLang.en: return 'Downloading $pct%'; case AppLang.ja: return 'ダウンロード中 $pct%'; case AppLang.ko: return '다운로드 중 $pct%'; case AppLang.ru: return 'Загрузка $pct%'; case AppLang.es: return 'Descargando $pct%'; } } /// 下载完成、正在安装。 String get updateInstalling { switch (this) { case AppLang.zh: return '下载完成,正在安装…'; case AppLang.en: return 'Downloaded, installing…'; case AppLang.ja: return 'ダウンロード完了、インストール中…'; case AppLang.ko: return '다운로드 완료, 설치 중…'; case AppLang.ru: return 'Загружено, установка…'; case AppLang.es: return 'Descargado, instalando…'; } } /// 下载失败。 String get updateDownloadFailed { switch (this) { case AppLang.zh: return '下载失败,请重试或改用浏览器下载。'; case AppLang.en: return 'Download failed. Retry or download in your browser.'; case AppLang.ja: return 'ダウンロードに失敗しました。再試行するかブラウザでダウンロードしてください。'; case AppLang.ko: return '다운로드에 실패했습니다. 다시 시도하거나 브라우저로 다운로드하세요.'; case AppLang.ru: return 'Не удалось загрузить. Повторите или скачайте в браузере.'; case AppLang.es: return 'Error de descarga. Reintenta o descarga en el navegador.'; } } /// macOS:已下载,提示手动拖入应用程序。 String get updateMacReveal { switch (this) { case AppLang.zh: return '已下载并解压。请在访达中将 Pangolin 拖入「应用程序」文件夹替换旧版,然后重新打开。'; case AppLang.en: return 'Downloaded and extracted. In Finder, drag Pangolin into your Applications folder to replace the old version, then reopen.'; case AppLang.ja: return 'ダウンロードと展開が完了しました。Finder で Pangolin を「アプリケーション」フォルダにドラッグして置き換え、再度開いてください。'; case AppLang.ko: return '다운로드 및 압축 해제 완료. Finder에서 Pangolin을 「응용 프로그램」 폴더로 드래그해 이전 버전을 교체한 뒤 다시 여세요.'; case AppLang.ru: return 'Загружено и распаковано. В Finder перетащите Pangolin в папку «Программы», заменив старую версию, затем откройте заново.'; case AppLang.es: return 'Descargado y extraído. En Finder, arrastra Pangolin a tu carpeta de Aplicaciones para reemplazar la versión anterior y vuelve a abrir.'; } } /// 失败对话框:改用浏览器下载。 String get updateOpenBrowser { switch (this) { case AppLang.zh: return '浏览器下载'; case AppLang.en: return 'Open in browser'; case AppLang.ja: return 'ブラウザで開く'; case AppLang.ko: return '브라우저에서 열기'; case AppLang.ru: return 'Открыть в браузере'; case AppLang.es: return 'Abrir en el navegador'; } } /// 通用「取消」。 String get updateCancelBtn { switch (this) { case AppLang.zh: return '取消'; case AppLang.en: return 'Cancel'; case AppLang.ja: return 'キャンセル'; case AppLang.ko: return '취소'; case AppLang.ru: return 'Отмена'; case AppLang.es: return 'Cancelar'; } } /// 更新 banner:「立即更新」。 String get updateNow { switch (this) { case AppLang.zh: return '立即更新'; case AppLang.en: return 'Update now'; case AppLang.ja: return '今すぐ更新'; case AppLang.ko: return '지금 업데이트'; case AppLang.ru: return 'Обновить'; case AppLang.es: return 'Actualizar'; } } } /// 全部界面文案的抽象契约。zh / en 各实现一份。 abstract class AppText { const AppText(); AppLang get lang; // ── 品牌 / 连接状态 ── String get brand; String get online; String get offline; String get capOff; String get capConnecting; String get capOn; String get connectNow; // 连接键 off 态圆内文字 String get secure; // 连接键圆内「已加密」 // ── 底部 / 侧栏导航 ── String get tabConnect; String get tabServers; String get tabStats; String get tabMe; // ── 连接页 ── String get currentNode; String get download; String get upload; String get latency; // ── 免费额度卡 ── String get quotaToday; String get minutes; String get quotaFree; String get planFreeTag; // 额度卡右上短标签「免费版」(不含「每日10分钟」,避免挤占行宽) String get watchAd; String get adUnlocked; // 免费版 10 分钟卡控 + 看广告加时(累加式) String get quotaLeftLabel; // 连接期倒计时前缀「剩余」 String get quotaUsedUp; // 额度卡:今日已用完 String get watchAdMore; // 「看广告加时」CTA String get quotaExhaustedNotice; // 倒计时归零自动切断提示 String get adPlaying; // 占位广告:播放中 String adRewarded(int minutes); // 占位广告:已加 N 分钟 String get adFailed; // 看广告加时失败 String get quotaDesktopTitle; // 桌面版额度耗尽弹窗标题 String get quotaDesktopBody; // 桌面版额度耗尽弹窗正文 String get gotIt; // 知道了 // ── 节点页 ── String get chooseNode; String get searchPh; String get smartSelect; // 已连接时点其他节点 → 切换确认(防误触) String nodeSwitchTitle(String name); String get nodeSwitchBody; String get nodeSwitchConfirm; // 连通看门狗:当前节点数据面不可用时的提示 String get nodeUnhealthySwitched; // 智能选择已自动切换 String get nodeUnhealthyError; // 手动节点:断开并提示 String get nodeReconnecting; // 弱网抖动:自动重连当前节点中(#18) String get smartSub; String get recommended; // ── 统计页 ── String get statsTitle; String get trafficMonth; String get avgPing; String get durMonth; String get weekTraffic; List get days7; String get byDevice; String get noDeviceUsage; // 统计页重设计:周期(月/周/日) · 指标(下行/上行/时长) · 折线 · 设备下拉 String get periodMonth; String get periodWeek; String get periodToday; String get statDown; String get statUp; String get statDuration; String get chartTwoWeeks; String get allDevices; // ── 账户页 ── String get meTitle; String get proMember; String get freePlanName; String get expires; String get upgradeBtn; String get accInfoTitle; String get accEmail; String get accPassword; String get accChange; String get myDevices; String get devicesSub; String get thisDevice; String get remove; // 设备数超上限挡板(#16):%s = 套餐设备上限数 String get deviceLimitTitle; String get deviceLimitDesc; // "你的套餐最多 %s 台设备,移除一台以继续。" String get deviceLimitRemoveOldest; // 设备管理(P6):在线状态 / 操作 / 危险确认 String get devOnline; String get devOffline; String get devLastOnline; // 「最后在线」前缀(离线设备,取 last_seen) String get devNeverLogin; String get devForceLogout; String get devClearLogin; String get devRename; String get devRenameHint; String get devSave; String get devForceLogoutConfirm; // 弹窗正文(含设备名占位 %s) String get devClearLoginConfirm; String get devCancel; // 本设备被其他设备强制下线时的提示框。 String get sessionRevokedTitle; String get sessionRevokedBody; String get sessionRevokedOk; String get redeemEntry; String get contactEntry; String get signOut; String get language; String get darkAppearance; String get stateOn; String get followLight; String get protocol; String get settingsTitle; String get autostart; String get autostartSub; String get autoConnect; String get autoConnectSub; String autoConnectingTo(String name); // 自动连接 toast String get smartRoute; String get smartRouteSub; String get killSwitch; String get killSwitchSub; String get checkUpdate; String get webUserCenter; // 「用户中心(网页)」入口(App→Web SSO 免登) // 更新检查(设置页「检查更新」手动触发) String updateAvailableTitle(String version); // 「发现新版本 vX.Y.Z」 String get updateNotesFallback; // 无 release_notes 时的兜底文案 String get updateLater; String get updateDownload; String get updateUpToDate; // 检查后:已是最新版本 String get updateCheckFailed; // 检查失败(网络异常) // ── 套餐选择 ── String get choosePlan; String get freePlan; String get proPlan; String get teamPlan; String get perMonth; String get current; String get upgrade; String get choose; String get mostPopular; List get featsFree; List get featsPro; List get featsTeam; // ── 兑换 & 购买 ── String get redeemTitle; String get redeemCodeTitle; String get redeemPh; String get redeemBtn; String get redeemOk; String get buyTitle; String get buySub; String get chStore; String get chEmail; // ── 联系我们 ── String get contactTitle; String get contactIntro; String get contactEmail; String get contactStore; String get contactHoursTitle; String get contactHours; // ── 登录 / 注册 ── String get authTagline; String get tabLogin; String get tabRegister; String get emailLabel; String get emailPh; String get pwLabel; String get pwPh; String get setPwPh; String get codeLabel; String codeSentTo(String email); String get sendCode; String get resend; String get doLogin; String get doNext; String get doCreate; String get forgotPw; String get tos; // ── 首次引导 ── String get obSkip; String get obNext; String get obAllow; String get obStart; String get ob1Title; String get ob1Sub; String get ob2Title; String get ob2Sub; String get ob3Title; String get ob3Sub; }