feat(app): 按错误码展示中英印尼文案
This commit is contained in:
1 parent
0ef0d46919
commit
b1e95b370c
10 files changed
+331
-6
No files matched your search
@@ -2,14 +2,37 @@
|
||||
|
||||
import '../../l10n/app_localizations.dart';
|
||||
|
||||
const int _serverErrorCode = 500;
|
||||
|
||||
/// 用户端不展示内部故障文案。500 与空提示改成可理解的说明,业务提示原样保留。
|
||||
/// 按后端 `ErrorCode` 取当前语言文案。未收录的业务码回退服务端 message。
|
||||
String userFacingApiMessage(AppLocalizations l10n, int code, String message) {
|
||||
if (code >= _serverErrorCode || message.isEmpty) return l10n.authServerError;
|
||||
final localized = _apiMessage(l10n, code);
|
||||
if (localized != null) return localized;
|
||||
if (message.isEmpty) return l10n.authServerError;
|
||||
return message;
|
||||
}
|
||||
|
||||
/// 已定义错误码。500 用服务繁忙,不展示「系统异常」。
|
||||
String? _apiMessage(AppLocalizations l10n, int code) {
|
||||
return switch (code) {
|
||||
400 => l10n.apiErrorBadRequest,
|
||||
401 => l10n.apiErrorUnauthorized,
|
||||
403 => l10n.apiErrorForbidden,
|
||||
404 => l10n.apiErrorNotFound,
|
||||
429 => l10n.apiErrorTooManyRequests,
|
||||
500 => l10n.authServerError,
|
||||
100000 => l10n.apiErrorLoginFailed,
|
||||
100001 => l10n.apiErrorAccountDisabled,
|
||||
100002 => l10n.apiErrorCoinNotEnough,
|
||||
100003 => l10n.apiErrorEpisodeUnavailable,
|
||||
100004 => l10n.apiErrorEpisodeUnlocked,
|
||||
100005 => l10n.apiErrorOrderInvalid,
|
||||
100006 => l10n.apiErrorVipExpired,
|
||||
200001 => l10n.apiErrorDataExists,
|
||||
200002 => l10n.apiErrorDataNotFound,
|
||||
200003 => l10n.apiErrorNotAllowed,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// logic 里的错误在两种情况下是"哨兵值":本地网络异常(读不到后端提示语)与
|
||||
/// 我们自己包装的失败。这里统一翻译成当前语言的文案;服务端给的中文提示原样展示。
|
||||
String errorText(AppLocalizations l10n, String? code, String fallback) {
|
||||
|
||||
@@ -438,6 +438,21 @@
|
||||
"authPasswordWeak": "Password needs both letters and numbers",
|
||||
"authNetworkError": "Network error, please try again",
|
||||
"authServerError": "The service is busy. Please try again shortly.",
|
||||
"apiErrorBadRequest": "Invalid request",
|
||||
"apiErrorUnauthorized": "Sign in to continue",
|
||||
"apiErrorForbidden": "You don't have permission",
|
||||
"apiErrorNotFound": "Not found",
|
||||
"apiErrorTooManyRequests": "Too many requests. Please try again later.",
|
||||
"apiErrorLoginFailed": "Incorrect account or password",
|
||||
"apiErrorAccountDisabled": "This account has been disabled",
|
||||
"apiErrorCoinNotEnough": "Not enough coins",
|
||||
"apiErrorEpisodeUnavailable": "This episode is unavailable",
|
||||
"apiErrorEpisodeUnlocked": "This episode is already unlocked",
|
||||
"apiErrorOrderInvalid": "This order is already paid or closed",
|
||||
"apiErrorVipExpired": "Membership has expired",
|
||||
"apiErrorDataExists": "This record already exists",
|
||||
"apiErrorDataNotFound": "Not found",
|
||||
"apiErrorNotAllowed": "This action isn't available right now",
|
||||
"authSwitchToSignUp": "New here? Create an account",
|
||||
"authSwitchToSignIn": "Already have an account? Sign in",
|
||||
"authGateTitle": "Sign in to continue",
|
||||
|
||||
@@ -412,6 +412,21 @@
|
||||
"authPasswordWeak": "Kata sandi perlu huruf dan angka",
|
||||
"authNetworkError": "Jaringan bermasalah, coba lagi",
|
||||
"authServerError": "Layanan sedang sibuk. Coba lagi sebentar.",
|
||||
"apiErrorBadRequest": "Permintaan tidak valid",
|
||||
"apiErrorUnauthorized": "Masuk untuk melanjutkan",
|
||||
"apiErrorForbidden": "Anda tidak memiliki izin",
|
||||
"apiErrorNotFound": "Tidak ditemukan",
|
||||
"apiErrorTooManyRequests": "Terlalu banyak permintaan. Coba lagi nanti.",
|
||||
"apiErrorLoginFailed": "Akun atau kata sandi salah",
|
||||
"apiErrorAccountDisabled": "Akun ini telah dinonaktifkan",
|
||||
"apiErrorCoinNotEnough": "Koin tidak cukup",
|
||||
"apiErrorEpisodeUnavailable": "Episode ini tidak tersedia",
|
||||
"apiErrorEpisodeUnlocked": "Episode ini sudah terbuka",
|
||||
"apiErrorOrderInvalid": "Pesanan ini sudah dibayar atau ditutup",
|
||||
"apiErrorVipExpired": "Keanggotaan telah berakhir",
|
||||
"apiErrorDataExists": "Data sudah ada",
|
||||
"apiErrorDataNotFound": "Data tidak ditemukan",
|
||||
"apiErrorNotAllowed": "Tindakan ini tidak tersedia saat ini",
|
||||
"authSwitchToSignUp": "Baru di sini? Buat akun",
|
||||
"authSwitchToSignIn": "Sudah punya akun? Masuk",
|
||||
"authGateTitle": "Masuk untuk melanjutkan",
|
||||
|
||||
@@ -1906,6 +1906,96 @@ abstract class AppLocalizations {
|
||||
/// **'The service is busy. Please try again shortly.'**
|
||||
String get authServerError;
|
||||
|
||||
/// No description provided for @apiErrorBadRequest.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Invalid request'**
|
||||
String get apiErrorBadRequest;
|
||||
|
||||
/// No description provided for @apiErrorUnauthorized.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sign in to continue'**
|
||||
String get apiErrorUnauthorized;
|
||||
|
||||
/// No description provided for @apiErrorForbidden.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'You don\'t have permission'**
|
||||
String get apiErrorForbidden;
|
||||
|
||||
/// No description provided for @apiErrorNotFound.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Not found'**
|
||||
String get apiErrorNotFound;
|
||||
|
||||
/// No description provided for @apiErrorTooManyRequests.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Too many requests. Please try again later.'**
|
||||
String get apiErrorTooManyRequests;
|
||||
|
||||
/// No description provided for @apiErrorLoginFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Incorrect account or password'**
|
||||
String get apiErrorLoginFailed;
|
||||
|
||||
/// No description provided for @apiErrorAccountDisabled.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This account has been disabled'**
|
||||
String get apiErrorAccountDisabled;
|
||||
|
||||
/// No description provided for @apiErrorCoinNotEnough.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Not enough coins'**
|
||||
String get apiErrorCoinNotEnough;
|
||||
|
||||
/// No description provided for @apiErrorEpisodeUnavailable.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This episode is unavailable'**
|
||||
String get apiErrorEpisodeUnavailable;
|
||||
|
||||
/// No description provided for @apiErrorEpisodeUnlocked.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This episode is already unlocked'**
|
||||
String get apiErrorEpisodeUnlocked;
|
||||
|
||||
/// No description provided for @apiErrorOrderInvalid.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This order is already paid or closed'**
|
||||
String get apiErrorOrderInvalid;
|
||||
|
||||
/// No description provided for @apiErrorVipExpired.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Membership has expired'**
|
||||
String get apiErrorVipExpired;
|
||||
|
||||
/// No description provided for @apiErrorDataExists.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This record already exists'**
|
||||
String get apiErrorDataExists;
|
||||
|
||||
/// No description provided for @apiErrorDataNotFound.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Not found'**
|
||||
String get apiErrorDataNotFound;
|
||||
|
||||
/// No description provided for @apiErrorNotAllowed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This action isn\'t available right now'**
|
||||
String get apiErrorNotAllowed;
|
||||
|
||||
/// No description provided for @authSwitchToSignUp.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -968,6 +968,52 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get authServerError =>
|
||||
'The service is busy. Please try again shortly.';
|
||||
|
||||
@override
|
||||
String get apiErrorBadRequest => 'Invalid request';
|
||||
|
||||
@override
|
||||
String get apiErrorUnauthorized => 'Sign in to continue';
|
||||
|
||||
@override
|
||||
String get apiErrorForbidden => 'You don\'t have permission';
|
||||
|
||||
@override
|
||||
String get apiErrorNotFound => 'Not found';
|
||||
|
||||
@override
|
||||
String get apiErrorTooManyRequests =>
|
||||
'Too many requests. Please try again later.';
|
||||
|
||||
@override
|
||||
String get apiErrorLoginFailed => 'Incorrect account or password';
|
||||
|
||||
@override
|
||||
String get apiErrorAccountDisabled => 'This account has been disabled';
|
||||
|
||||
@override
|
||||
String get apiErrorCoinNotEnough => 'Not enough coins';
|
||||
|
||||
@override
|
||||
String get apiErrorEpisodeUnavailable => 'This episode is unavailable';
|
||||
|
||||
@override
|
||||
String get apiErrorEpisodeUnlocked => 'This episode is already unlocked';
|
||||
|
||||
@override
|
||||
String get apiErrorOrderInvalid => 'This order is already paid or closed';
|
||||
|
||||
@override
|
||||
String get apiErrorVipExpired => 'Membership has expired';
|
||||
|
||||
@override
|
||||
String get apiErrorDataExists => 'This record already exists';
|
||||
|
||||
@override
|
||||
String get apiErrorDataNotFound => 'Not found';
|
||||
|
||||
@override
|
||||
String get apiErrorNotAllowed => 'This action isn\'t available right now';
|
||||
|
||||
@override
|
||||
String get authSwitchToSignUp => 'New here? Create an account';
|
||||
|
||||
|
||||
@@ -971,6 +971,52 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
@override
|
||||
String get authServerError => 'Layanan sedang sibuk. Coba lagi sebentar.';
|
||||
|
||||
@override
|
||||
String get apiErrorBadRequest => 'Permintaan tidak valid';
|
||||
|
||||
@override
|
||||
String get apiErrorUnauthorized => 'Masuk untuk melanjutkan';
|
||||
|
||||
@override
|
||||
String get apiErrorForbidden => 'Anda tidak memiliki izin';
|
||||
|
||||
@override
|
||||
String get apiErrorNotFound => 'Tidak ditemukan';
|
||||
|
||||
@override
|
||||
String get apiErrorTooManyRequests =>
|
||||
'Terlalu banyak permintaan. Coba lagi nanti.';
|
||||
|
||||
@override
|
||||
String get apiErrorLoginFailed => 'Akun atau kata sandi salah';
|
||||
|
||||
@override
|
||||
String get apiErrorAccountDisabled => 'Akun ini telah dinonaktifkan';
|
||||
|
||||
@override
|
||||
String get apiErrorCoinNotEnough => 'Koin tidak cukup';
|
||||
|
||||
@override
|
||||
String get apiErrorEpisodeUnavailable => 'Episode ini tidak tersedia';
|
||||
|
||||
@override
|
||||
String get apiErrorEpisodeUnlocked => 'Episode ini sudah terbuka';
|
||||
|
||||
@override
|
||||
String get apiErrorOrderInvalid => 'Pesanan ini sudah dibayar atau ditutup';
|
||||
|
||||
@override
|
||||
String get apiErrorVipExpired => 'Keanggotaan telah berakhir';
|
||||
|
||||
@override
|
||||
String get apiErrorDataExists => 'Data sudah ada';
|
||||
|
||||
@override
|
||||
String get apiErrorDataNotFound => 'Data tidak ditemukan';
|
||||
|
||||
@override
|
||||
String get apiErrorNotAllowed => 'Tindakan ini tidak tersedia saat ini';
|
||||
|
||||
@override
|
||||
String get authSwitchToSignUp => 'Baru di sini? Buat akun';
|
||||
|
||||
|
||||
@@ -955,6 +955,51 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get authServerError => '服务繁忙,请稍后再试';
|
||||
|
||||
@override
|
||||
String get apiErrorBadRequest => '参数校验失败';
|
||||
|
||||
@override
|
||||
String get apiErrorUnauthorized => '未登录或登录已过期';
|
||||
|
||||
@override
|
||||
String get apiErrorForbidden => '无权限';
|
||||
|
||||
@override
|
||||
String get apiErrorNotFound => '资源不存在';
|
||||
|
||||
@override
|
||||
String get apiErrorTooManyRequests => '请求过于频繁,请稍后再试';
|
||||
|
||||
@override
|
||||
String get apiErrorLoginFailed => '账号或密码错误';
|
||||
|
||||
@override
|
||||
String get apiErrorAccountDisabled => '账号已停用';
|
||||
|
||||
@override
|
||||
String get apiErrorCoinNotEnough => '金币余额不足';
|
||||
|
||||
@override
|
||||
String get apiErrorEpisodeUnavailable => '剧集未发布或不存在';
|
||||
|
||||
@override
|
||||
String get apiErrorEpisodeUnlocked => '剧集已解锁';
|
||||
|
||||
@override
|
||||
String get apiErrorOrderInvalid => '订单已支付或已关闭';
|
||||
|
||||
@override
|
||||
String get apiErrorVipExpired => '会员已过期';
|
||||
|
||||
@override
|
||||
String get apiErrorDataExists => '数据已存在';
|
||||
|
||||
@override
|
||||
String get apiErrorDataNotFound => '数据不存在';
|
||||
|
||||
@override
|
||||
String get apiErrorNotAllowed => '当前状态不允许该操作';
|
||||
|
||||
@override
|
||||
String get authSwitchToSignUp => '还没有账号?去注册';
|
||||
|
||||
|
||||
@@ -374,6 +374,21 @@
|
||||
"authPasswordWeak": "密码需同时包含字母和数字",
|
||||
"authNetworkError": "网络异常,请稍后重试",
|
||||
"authServerError": "服务繁忙,请稍后再试",
|
||||
"apiErrorBadRequest": "参数校验失败",
|
||||
"apiErrorUnauthorized": "未登录或登录已过期",
|
||||
"apiErrorForbidden": "无权限",
|
||||
"apiErrorNotFound": "资源不存在",
|
||||
"apiErrorTooManyRequests": "请求过于频繁,请稍后再试",
|
||||
"apiErrorLoginFailed": "账号或密码错误",
|
||||
"apiErrorAccountDisabled": "账号已停用",
|
||||
"apiErrorCoinNotEnough": "金币余额不足",
|
||||
"apiErrorEpisodeUnavailable": "剧集未发布或不存在",
|
||||
"apiErrorEpisodeUnlocked": "剧集已解锁",
|
||||
"apiErrorOrderInvalid": "订单已支付或已关闭",
|
||||
"apiErrorVipExpired": "会员已过期",
|
||||
"apiErrorDataExists": "数据已存在",
|
||||
"apiErrorDataNotFound": "数据不存在",
|
||||
"apiErrorNotAllowed": "当前状态不允许该操作",
|
||||
"authSwitchToSignUp": "还没有账号?去注册",
|
||||
"authSwitchToSignIn": "已有账号?去登录",
|
||||
"authGateTitle": "登录后可继续",
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:provider/provider.dart';
|
||||
|
||||
import '../../core/data/remote/api_client.dart';
|
||||
import '../../core/data/services/auth_controller.dart';
|
||||
import '../../core/formatting/error_text.dart';
|
||||
import '../../core/platform/haptics.dart';
|
||||
import '../../core/platform/keyboard_inset.dart';
|
||||
import '../../core/theme/app_colors.dart';
|
||||
@@ -180,7 +181,7 @@ class _PhoneAuthSheetState extends State<PhoneAuthSheet> {
|
||||
).showSnackBar(SnackBar(content: Text(l10n.authCodeSent)));
|
||||
_codeFocus.requestFocus();
|
||||
} on ApiException catch (error) {
|
||||
failureMessage = error.message;
|
||||
failureMessage = userFacingApiMessage(l10n, error.code, error.message);
|
||||
} catch (_) {
|
||||
failureMessage = l10n.authNetworkError;
|
||||
} finally {
|
||||
@@ -228,7 +229,7 @@ class _PhoneAuthSheetState extends State<PhoneAuthSheet> {
|
||||
}
|
||||
if (mounted) Navigator.of(context).pop(true);
|
||||
} on ApiException catch (error) {
|
||||
failureMessage = error.message;
|
||||
failureMessage = userFacingApiMessage(l10n, error.code, error.message);
|
||||
} catch (_) {
|
||||
failureMessage = l10n.authNetworkError;
|
||||
} finally {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/core/formatting/error_text.dart';
|
||||
import 'package:fruitshorts/l10n/app_localizations.dart';
|
||||
|
||||
void main() {
|
||||
test('should_keep_login_failure_message_when_code_is_business', () async {
|
||||
final l10n = await AppLocalizations.delegate.load(const Locale('zh'));
|
||||
|
||||
expect(
|
||||
userFacingApiMessage(l10n, 100000, '账号或密码错误'),
|
||||
'账号或密码错误',
|
||||
);
|
||||
expect(userFacingApiMessage(l10n, 500, '系统异常'), l10n.authServerError);
|
||||
|
||||
final english = await AppLocalizations.delegate.load(const Locale('en'));
|
||||
expect(
|
||||
userFacingApiMessage(english, 100000, '账号或密码错误'),
|
||||
'Incorrect account or password',
|
||||
);
|
||||
final indonesian = await AppLocalizations.delegate.load(const Locale('id'));
|
||||
expect(
|
||||
userFacingApiMessage(indonesian, 100000, '账号或密码错误'),
|
||||
'Akun atau kata sandi salah',
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user