diff --git a/client/lib/l10n/app_text.dart b/client/lib/l10n/app_text.dart index a44177f..d926e4d 100644 --- a/client/lib/l10n/app_text.dart +++ b/client/lib/l10n/app_text.dart @@ -11,7 +11,28 @@ // 接口形态与 .arb 一致,后续可平滑迁移到官方 l10n 工具链。 /// 受支持语言。单显——任一时刻只渲染其一。 -enum AppLang { zh, en } +/// 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'; + } + } +} /// 全部界面文案的抽象契约。zh / en 各实现一份。 abstract class AppText { diff --git a/client/lib/l10n/strings_es.dart b/client/lib/l10n/strings_es.dart new file mode 100644 index 0000000..53cf6ff --- /dev/null +++ b/client/lib/l10n/strings_es.dart @@ -0,0 +1,373 @@ +// strings_es.dart — Recursos de texto en español (visualización monolingüe) +// +// Palabras prohibidas: VPN / traspasar muros / red científica / romper el bloqueo / cruce libre / Go anywhere. +// Posicionamiento: aceleración de red / optimización de la experiencia + privacidad. +// La numeración de planes sigue design/CLAUDE.md §7. +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. Mira un anuncio para añadir tiempo o mejora tu plan.'; + @override + String get adPlaying => 'Reproduciendo anuncio…'; + @override + String adRewarded(int minutes) => '+$minutes min añadidos'; + @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 nodeSwitchTitle(String name) => '¿Cambiar a $name?'; + @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 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 + List get days7 => const ['L', 'M', 'X', 'J', 'V', 'S', 'D']; + @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 y comprar'; + @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 autoConnectingTo(String name) => 'Conectando automáticamente a $name…'; + @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 updateAvailableTitle(String version) => 'Nueva versión v$version disponible'; + @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 choosePlan => 'Elegir plan'; + @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 + List 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 get featsPro => + const ['Más de 80 ubicaciones', 'Ilimitado · rápido', '5 dispositivos', 'Optimizado para streaming']; + @override + List get featsTeam => + const ['Todo lo de Pro', '10 plazas', 'Facturación centralizada', 'Soporte prioritario']; + + @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 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 codeSentTo(String email) => 'Código enviado a $email'; + @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.'; +} diff --git a/client/lib/l10n/strings_ja.dart b/client/lib/l10n/strings_ja.dart new file mode 100644 index 0000000..14268b8 --- /dev/null +++ b/client/lib/l10n/strings_ja.dart @@ -0,0 +1,373 @@ +// strings_ja.dart — 日本語の文言リソース(単一言語表示) +// +// 禁止ワード: VPN / 翻墙 / 科学上网 / 突破封锁 / 自由穿越 / Go anywhere。 +// ポジショニング: ネットワーク高速化 / 体験最適化 + プライバシー。 +// プラン番号は design/CLAUDE.md §7 に準拠。 +import 'app_text.dart'; + +class StringsJa extends AppText { + const StringsJa(); + + @override + AppLang get lang => AppLang.ja; + + @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 => 'Ping'; + + @override + String get quotaToday => '本日の残り'; + @override + String get minutes => '分'; + @override + String get quotaFree => '無料 · 1日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 adRewarded(int minutes) => '+$minutes 分を追加しました'; + @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 nodeSwitchTitle(String name) => '$name に切り替えますか?'; + @override + String get nodeSwitchBody => '現在の接続を切断し、このノードに再接続します。'; + @override + String get nodeSwitchConfirm => '切り替える'; + @override + String get nodeUnhealthySwitched => '現在のノードに接続できないため、自動的に切り替えました'; + @override + String get nodeUnhealthyError => '現在のノードに接続できません。再接続するかノードを切り替えてください'; + @override + String get nodeReconnecting => 'ネットワークが不安定です。再接続中…'; + @override + String get smartSub => 'お使いのネットワークに最適なノードを選びます'; + @override + String get recommended => 'おすすめ'; + + @override + String get statsTitle => '統計'; + @override + String get trafficMonth => '通信量'; + @override + String get avgPing => '平均Ping'; + @override + String get durMonth => '接続時間'; + @override + String get weekTraffic => '今週 (GB)'; + @override + List get days7 => const ['月', '火', '水', '木', '金', '土', '日']; + @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 台まで利用できます。続けるには1台を削除してください。'; + @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 autoConnectingTo(String name) => '$name に自動接続中…'; + @override + String get smartRoute => 'スマートルーティング'; + @override + String get smartRouteSub => '海外は高速化、国内は直接接続'; + @override + String get killSwitch => 'キルスイッチ'; + @override + String get killSwitchSub => '接続が切れたら通信を遮断'; + @override + String get checkUpdate => 'アップデートを確認'; + @override + String get webUserCenter => 'ユーザーセンター (Web)'; + @override + String updateAvailableTitle(String version) => '新しいバージョン v$version が利用可能です'; + @override + String get updateNotesFallback => 'このアップデートには不具合の修正と安定性の向上が含まれています。'; + @override + String get updateLater => '後で'; + @override + String get updateDownload => 'アップデートをダウンロード'; + @override + String get updateUpToDate => '最新の状態です'; + @override + String get updateCheckFailed => 'アップデートの確認に失敗しました。後でもう一度お試しください'; + + @override + String get choosePlan => 'プランを選択'; + @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 + List get featsFree => const [ + '基本ノード1つのみ', + '1日10分', + '広告を見て開始', + '登録で7日間無料トライアル', + ]; + @override + List get featsPro => + const ['80以上の拠点', '無制限 · 高速', '5台のデバイス', 'ストリーミング最適化']; + @override + List get featsTeam => + const ['Proのすべての機能', '10ライセンス', '一括請求', '優先サポート']; + + @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 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 codeSentTo(String email) => '$email に確認コードを送信しました'; + @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 => 'エンドツーエンドで暗号化。閲覧ログは一切保持しません。'; +} diff --git a/client/lib/l10n/strings_ko.dart b/client/lib/l10n/strings_ko.dart new file mode 100644 index 0000000..8b37b5e --- /dev/null +++ b/client/lib/l10n/strings_ko.dart @@ -0,0 +1,373 @@ +// strings_ko.dart — 한국어 문구 리소스 (단일 언어 표시) +// +// 금지어: VPN / 翻墙 / 科学上网 / 突破封锁 / 自由穿越 / Go anywhere. +// 포지셔닝: 네트워크 가속 / 경험 최적화 + 프라이버시. +// 요금제 번호는 design/CLAUDE.md §7 을 따름. +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 adRewarded(int minutes) => '+$minutes분 추가됨'; + @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 nodeSwitchTitle(String name) => '$name(으)로 전환할까요?'; + @override + String get nodeSwitchBody => '현재 연결을 끊고 이 노드로 다시 연결합니다.'; + @override + String get nodeSwitchConfirm => '전환'; + @override + String get nodeUnhealthySwitched => '현재 노드에 연결할 수 없어 자동으로 전환했습니다'; + @override + String get nodeUnhealthyError => '현재 노드에 연결할 수 없습니다 — 재연결하거나 노드를 전환하세요'; + @override + String get nodeReconnecting => '네트워크가 불안정하여 다시 연결 중…'; + @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 + List get days7 => const ['월', '화', '수', '목', '금', '토', '일']; + @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 autoConnectingTo(String name) => '$name(으)로 자동 연결 중…'; + @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 updateAvailableTitle(String version) => '새 버전 v$version 사용 가능'; + @override + String get updateNotesFallback => '이번 업데이트에는 버그 수정과 안정성 개선이 포함되어 있습니다.'; + @override + String get updateLater => '나중에'; + @override + String get updateDownload => '업데이트 다운로드'; + @override + String get updateUpToDate => '최신 버전입니다'; + @override + String get updateCheckFailed => '업데이트 확인에 실패했습니다. 잠시 후 다시 시도하세요'; + + @override + String get choosePlan => '요금제 선택'; + @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 + List get featsFree => const [ + '기본 노드 1개', + '하루 10분', + '광고 시청 후 시작', + '가입 시 7일 무료 체험', + ]; + @override + List get featsPro => + const ['80개 이상 지역', '무제한 · 빠른 속도', '기기 5대', '스트리밍 최적화']; + @override + List get featsTeam => + const ['Pro의 모든 기능', '10인 좌석', '통합 결제', '우선 지원']; + + @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 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 codeSentTo(String email) => '$email(으)로 코드를 보냈습니다'; + @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 => '종단 간 암호화. 브라우징 기록을 전혀 저장하지 않습니다.'; +} diff --git a/client/lib/l10n/strings_ru.dart b/client/lib/l10n/strings_ru.dart new file mode 100644 index 0000000..74094d1 --- /dev/null +++ b/client/lib/l10n/strings_ru.dart @@ -0,0 +1,373 @@ +// strings_ru.dart — русские текстовые ресурсы (отображение на одном языке) +// +// Запрещённые слова: VPN /翻墙 / 科学上网 / 突破封锁 / 自由穿越 / Go anywhere / ВПН. +// Позиционирование: ускорение сети / оптимизация опыта + приватность. +// Номера тарифов соответствуют design/CLAUDE.md §7. +import 'app_text.dart'; + +class StringsRu extends AppText { + const StringsRu(); + + @override + AppLang get lang => AppLang.ru; + + @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 adRewarded(int minutes) => '+$minutes мин добавлено'; + @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 nodeSwitchTitle(String name) => 'Переключиться на $name?'; + @override + String get nodeSwitchBody => 'Текущее соединение будет разорвано и восстановлено через этот узел.'; + @override + String get nodeSwitchConfirm => 'Переключить'; + @override + String get nodeUnhealthySwitched => 'Текущий узел недоступен — переключение выполнено автоматически'; + @override + String get nodeUnhealthyError => 'Текущий узел недоступен — переподключитесь или смените узел'; + @override + String get nodeReconnecting => 'Сеть нестабильна, переподключение…'; + @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 => 'На этой неделе (ГБ)'; + @override + List get days7 => const ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']; + @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 => 'До 3 устройств на PRO'; + @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 autoConnectingTo(String name) => 'Автоподключение к $name…'; + @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 updateAvailableTitle(String version) => 'Доступна новая версия v$version'; + @override + String get updateNotesFallback => 'Это обновление включает исправления и улучшения стабильности.'; + @override + String get updateLater => 'Позже'; + @override + String get updateDownload => 'Скачать обновление'; + @override + String get updateUpToDate => 'У вас установлена последняя версия'; + @override + String get updateCheckFailed => 'Не удалось проверить обновления, попробуйте позже'; + + @override + String get choosePlan => 'Выбор тарифа'; + @override + String get freePlan => 'Бесплатный'; + @override + String get proPlan => 'Pro'; + @override + String get teamPlan => 'Команда'; + @override + String get perMonth => '/мес'; + @override + String get current => 'Текущий'; + @override + String get upgrade => 'Улучшить'; + @override + String get choose => 'Выбрать'; + @override + String get mostPopular => 'Популярный'; + @override + List get featsFree => const [ + 'Только 1 базовый узел', + '10 минут в день', + 'Смотрите рекламу, чтобы начать', + '7 дней бесплатно при регистрации', + ]; + @override + List get featsPro => + const ['80+ локаций', 'Безлимитно · быстро', '5 устройств', 'Оптимизация для стриминга']; + @override + List get featsTeam => + const ['Всё из Pro', '10 мест', 'Единый счёт', 'Приоритетная поддержка']; + + @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 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 codeSentTo(String email) => 'Код отправлен на $email'; + @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 => 'Сквозное шифрование. Мы не храним журналы просмотров.'; +} diff --git a/client/lib/screens/account_page.dart b/client/lib/screens/account_page.dart index 7ad6188..9bc3e84 100644 --- a/client/lib/screens/account_page.dart +++ b/client/lib/screens/account_page.dart @@ -351,31 +351,37 @@ class _LangSwitch extends StatelessWidget { @override Widget build(BuildContext context) { final c = context.pangolin; - Widget seg(AppLang v, String label) { - final active = lang == v; - return GestureDetector( - onTap: () => onChange(v), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 13, vertical: 5), - decoration: BoxDecoration( - color: active ? c.accent : Colors.transparent, - borderRadius: BorderRadius.circular(PangolinRadius.full), + // 6 种语言用下拉(段控横排会挤)。 + return PopupMenuButton( + initialValue: lang, + onSelected: onChange, + color: c.surface, + elevation: 8, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(PangolinRadius.md)), + itemBuilder: (_) => [ + for (final l in AppLang.values) + PopupMenuItem( + value: l, + height: 42, + child: Text(l.nativeLabel, + style: PangolinText.caption.copyWith( + color: l == lang ? c.accent : c.fg1, + fontWeight: l == lang ? FontWeight.w700 : FontWeight.w500, + fontSize: 13)), ), - child: Text(label, - style: PangolinText.caption.copyWith( - color: active ? c.fgOnAccent : c.fg3, fontWeight: FontWeight.w700, fontSize: 12.5)), - ), - ); - } - - return Container( - padding: const EdgeInsets.all(3), - decoration: BoxDecoration(color: c.bgSubtle, borderRadius: BorderRadius.circular(PangolinRadius.full)), - child: Row(mainAxisSize: MainAxisSize.min, children: [ - seg(AppLang.zh, '中文'), - const SizedBox(width: 2), - seg(AppLang.en, 'EN'), - ]), + ], + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 13, vertical: 6), + decoration: + BoxDecoration(color: c.bgSubtle, borderRadius: BorderRadius.circular(PangolinRadius.full)), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Text(lang.nativeLabel, + style: PangolinText.caption + .copyWith(color: c.fg2, fontWeight: FontWeight.w700, fontSize: 12.5)), + const SizedBox(width: 4), + Icon(PangolinIcons.chevronDown, size: 14, color: c.fg3), + ]), + ), ); } } diff --git a/client/lib/screens/settings_page.dart b/client/lib/screens/settings_page.dart index b96f8a6..31c8e27 100644 --- a/client/lib/screens/settings_page.dart +++ b/client/lib/screens/settings_page.dart @@ -148,27 +148,37 @@ class _LangSwitch extends StatelessWidget { @override Widget build(BuildContext context) { final c = context.pangolin; - Widget seg(AppLang v, String label) { - final on = lang == v; - return GestureDetector( - onTap: () => onPick(v), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - decoration: BoxDecoration( - color: on ? c.accent : Colors.transparent, - borderRadius: BorderRadius.circular(PangolinRadius.full), + // 6 种语言用下拉(段控横排会挤)。当前语言以本地名 + 下拉箭头展示。 + return PopupMenuButton( + initialValue: lang, + onSelected: onPick, + color: c.surface, + elevation: 8, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(PangolinRadius.md)), + itemBuilder: (_) => [ + for (final l in AppLang.values) + PopupMenuItem( + value: l, + height: 42, + child: Text(l.nativeLabel, + style: PangolinText.caption.copyWith( + color: l == lang ? c.accent : c.fg1, + fontWeight: l == lang ? FontWeight.w700 : FontWeight.w500, + fontSize: 13)), ), - child: Text(label, - style: PangolinText.caption.copyWith( - color: on ? c.fgOnAccent : c.fg3, fontWeight: FontWeight.w700, fontSize: 12)), - ), - ); - } - - return Container( - padding: const EdgeInsets.all(3), - decoration: BoxDecoration(color: c.bgSubtle, borderRadius: BorderRadius.circular(PangolinRadius.full)), - child: Row(mainAxisSize: MainAxisSize.min, children: [seg(AppLang.zh, '中文'), seg(AppLang.en, 'EN')]), + ], + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: + BoxDecoration(color: c.bgSubtle, borderRadius: BorderRadius.circular(PangolinRadius.full)), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Text(lang.nativeLabel, + style: PangolinText.caption + .copyWith(color: c.fg2, fontWeight: FontWeight.w700, fontSize: 12)), + const SizedBox(width: 4), + Icon(PangolinIcons.chevronDown, size: 14, color: c.fg3), + ]), + ), ); } } diff --git a/client/lib/state/app_providers.dart b/client/lib/state/app_providers.dart index 3300fd0..b4e5a2a 100644 --- a/client/lib/state/app_providers.dart +++ b/client/lib/state/app_providers.dart @@ -4,17 +4,33 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../l10n/app_text.dart'; import '../l10n/strings_en.dart'; +import '../l10n/strings_es.dart'; +import '../l10n/strings_ja.dart'; +import '../l10n/strings_ko.dart'; +import '../l10n/strings_ru.dart'; import '../l10n/strings_zh.dart'; import 'account_providers.dart'; import 'auth_provider.dart'; -/// 当前语言(单显)。设置/账户页段控切换。 -final localeProvider = StateProvider((ref) => AppLang.zh); +/// 当前语言(单显)。设置/账户页切换。默认英文(国际化默认语种)。 +final localeProvider = StateProvider((ref) => AppLang.en); /// 由语言派生的文案资源——UI 一律通过它取文案,不写死字面量。 final appTextProvider = Provider((ref) { - final lang = ref.watch(localeProvider); - return lang == AppLang.zh ? const StringsZh() : const StringsEn(); + switch (ref.watch(localeProvider)) { + case AppLang.zh: + return const StringsZh(); + case AppLang.en: + return const StringsEn(); + case AppLang.ja: + return const StringsJa(); + case AppLang.ko: + return const StringsKo(); + case AppLang.ru: + return const StringsRu(); + case AppLang.es: + return const StringsEs(); + } }); /// 主题模式。默认跟随系统;设置页可显式切深色。