Files
pangolin/client/lib/state/app_providers.dart
wangjia 617b43083d feat(client/i18n): B1 硬编码文案 6 语化 + B2 语言持久化
B2 持久化:localeProvider 由 StateProvider 改 StateNotifier(LocaleNotifier),
默认英文,选择存 shared_preferences(key pg_lang),重启保留;新增 appTextFor(lang)
helper 供非 Consumer 场景复用。两处切换 .state= 改 .set()。

B1 硬编码 UI 文案 6 语化(AppLangMisc 扩展,exhaustive switch 不会漏):
- 不可用 / 流媒体优化 / 节点未就绪 / 连接失败 / 加载失败 / 暂无设备 / 改邮箱
- 相对时间(刚刚/X分钟前/…)relativeTime(Duration)
- 修 auth_screen 错误消息 bug:原 _errorZh 永远显示中文(连英文用户都中招)→
  按当前语言取 messageZh/En(非中文显英文)

仍回退英文(服务端数据,需服务端多语字段):节点/套餐名(nameZh/nameEn)、
API 错误消息(messageZh/En)。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 14:32:13 +08:00

96 lines
3.4 KiB
Dart

// app_providers.dart — 语言 / 主题 / 套餐视角等基础状态(Riverpod)
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.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';
/// AppLang → 文案资源实例。供 [appTextProvider] 与「拿不到 Consumer 的场景」
/// (model / 非 Consumer 的 tile,只有 AppLang)复用,避免各处重写 switch。
AppText appTextFor(AppLang lang) {
switch (lang) {
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();
}
}
/// 当前语言(单显)。默认英文(国际化默认语种);用户选择持久化到
/// shared_preferences(key `pg_lang`,存枚举 name),重启保留 —— 原来无持久化,
/// 切了语言重启会丢。设置/账户页经 `.notifier).set(lang)` 切换。
class LocaleNotifier extends StateNotifier<AppLang> {
LocaleNotifier() : super(AppLang.en) {
_load();
}
static const _key = 'pg_lang';
Future<void> _load() async {
try {
final saved = (await SharedPreferences.getInstance()).getString(_key);
if (saved != null) {
for (final l in AppLang.values) {
if (l.name == saved) {
state = l;
break;
}
}
}
} catch (_) {
/* 读失败保持默认 en */
}
}
Future<void> set(AppLang lang) async {
state = lang;
try {
await (await SharedPreferences.getInstance()).setString(_key, lang.name);
} catch (_) {
/* 持久化失败忽略,本次会话仍生效 */
}
}
}
final localeProvider =
StateNotifierProvider<LocaleNotifier, AppLang>((ref) => LocaleNotifier());
/// 由语言派生的文案资源——UI 一律通过它取文案,不写死字面量。
final appTextProvider = Provider<AppText>((ref) => appTextFor(ref.watch(localeProvider)));
/// 主题模式。默认跟随系统;设置页可显式切深色。
final themeModeProvider = StateProvider<ThemeMode>((ref) => ThemeMode.system);
/// 是否免费档——派生自真实账户(meProvider.plan == 'free')。
/// 免费版才显示额度卡与免费横幅;加载中默认按免费处理。
final isFreePlanProvider = Provider<bool>((ref) {
final me = ref.watch(meProvider).valueOrNull;
return me?.isFree ?? true;
});
/// 账户状态是否还没就绪——连接页据此先转圈再渲染,避免「先闪免费广告再跳会员」。
/// 覆盖两段窗口:① auth 还在从存储读 token(此时 isLoggedIn=false,me 会同步返回
/// 免费默认,故必须看 auth.isLoading);② 已登录但 me 还在拉真实值(此时 me 可能已有
/// 过期免费默认值,故看 me.isLoading 而非 !hasValue)。未登录(确定访客)不转圈。
final meLoadingProvider = Provider<bool>((ref) {
final auth = ref.watch(authProvider);
if (auth.isLoading) return true;
if (!auth.isLoggedIn) return false;
return ref.watch(meProvider).isLoading;
});