Files
pangolin/client/lib/l10n/app_text.dart
T
wangjia 0b86138b55
Deploy Client / build-windows (push) Successful in 1m47s
Deploy Client / build-android (push) Has been cancelled
Deploy Client / build-macos (push) Has been cancelled
Deploy Client / build-ios (push) Has been cancelled
Deploy Client / release-deploy (push) Has been cancelled
feat(client/update): 更新提示改 jiu 式顶部横条(非强制不再弹窗)
按需求去掉居中弹窗,改成不打断的顶部 banner(对照 jiu 黄条):
- 非强制更新 → home_shell 顶部 UpdateBanner「发现新版本 vX + 立即更新 + ×」;
  「立即更新」App 内下载安装,「×」忽略此版本(banner 隐藏,更新版本会重现)
- 强制更新(force_update)→ 仍走不可关闭弹窗 showUpdateDialog
- 忽略状态改 dismissedUpdateVersionProvider(按版本号,响应式);去掉 notifier
  的 _dismissed
- 设置页「检查更新」有更新时:清忽略版本→banner 显示 + toast,不再弹窗
- 新增 6 语「立即更新」文案。flutter analyze 仅 2 个既有 withOpacity info

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-07 19:19:05 +08:00

539 lines
17 KiB
Dart

// 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<String> 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<String> get featsFree;
List<String> get featsPro;
List<String> 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;
}