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
This commit is contained in:
@@ -34,6 +34,174 @@ extension AppLangLabel on AppLang {
|
||||
}
|
||||
}
|
||||
|
||||
/// 少量「拿不到 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 全部界面文案的抽象契约。zh / en 各实现一份。
|
||||
abstract class AppText {
|
||||
const AppText();
|
||||
|
||||
Reference in New Issue
Block a user