fix(admin-app): 消除UI层硬编码中文,补充i18n keys(zh/en/ja)

- 新增 29 个 i18n keys 到三语言文件(credit_score_unit, finance_confirm_withdraw,
  redemption_failed, store_no_stores, settings_confirm_logout, coupon_status_* 等)
- 修复 10 个页面中的硬编码中文字符串:
  credit_page: 分数单位、权重、空状态
  finance_page: 提现对话框、SnackBar、时间格式、空状态
  redemption_page: 错误消息、批量结果、空状态、时间格式
  store_management_page: 门店/员工空状态
  settings_page: 退出确认对话框、层级权益描述
  coupon_list_page: 状态徽章(在售/待审/售罄/下架)
  coupon_detail_page: 面值/发行价标签
  ai_agent_page: 错误回复消息
  issuer_dashboard_page: 信用分单位
- 剩余中文仅存于 /// 注释和 mock 数据数组中(将被真实API数据替换)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-02-23 23:29:10 -08:00
parent d9b07537a1
commit 24400ad663
10 changed files with 132 additions and 39 deletions

View File

@ -552,6 +552,37 @@ class AppLocalizations {
'settings_about': '关于企业券信', 'settings_about': '关于企业券信',
'settings_logout': '退出登录', 'settings_logout': '退出登录',
// Hardcoded string fixes
'credit_score_unit': '',
'credit_no_factors': '暂无评分因子',
'credit_no_suggestions': '暂无建议',
'finance_confirm_withdraw': '确认提现',
'finance_withdraw_amount': '提现金额',
'finance_withdraw_submitted': '提现申请已提交',
'finance_withdraw_failed': '提现失败',
'finance_no_transactions': '暂无交易记录',
'finance_today': '今天',
'finance_yesterday': '昨天',
'finance_no_reports': '暂无对账报表',
'redemption_failed': '核销失败',
'redemption_batch_failed': '批量核销失败',
'redemption_no_records': '暂无核销记录',
'redemption_minutes_ago': '分钟前',
'redemption_hours_ago': '小时前',
'redemption_days_ago': '天前',
'store_no_stores': '暂无门店',
'store_no_employees': '暂无员工',
'settings_confirm_logout': '确认退出',
'settings_confirm_logout_desc': '确定要退出登录吗?',
'settings_tier_benefits': '手续费率 1.2% · 高级数据分析',
'coupon_status_active': '在售中',
'coupon_status_pending': '待审核',
'coupon_status_sold_out': '已售罄',
'coupon_status_delisted': '已下架',
'ai_agent_error_reply': '抱歉,暂时无法回复。请稍后重试。',
'coupon_detail_type_info': '面值',
'coupon_detail_issue_price_label': '发行价',
// AI Suggestion Card // AI Suggestion Card
'ai_suggestion_label': 'AI 建议', 'ai_suggestion_label': 'AI 建议',
'ai_suggestion_dismiss': '忽略', 'ai_suggestion_dismiss': '忽略',
@ -1117,6 +1148,37 @@ class AppLocalizations {
'settings_about': 'About Genex', 'settings_about': 'About Genex',
'settings_logout': 'Sign Out', 'settings_logout': 'Sign Out',
// Hardcoded string fixes
'credit_score_unit': 'pts',
'credit_no_factors': 'No rating factors',
'credit_no_suggestions': 'No suggestions',
'finance_confirm_withdraw': 'Confirm Withdrawal',
'finance_withdraw_amount': 'Withdrawal Amount',
'finance_withdraw_submitted': 'Withdrawal request submitted',
'finance_withdraw_failed': 'Withdrawal failed',
'finance_no_transactions': 'No transactions',
'finance_today': 'Today',
'finance_yesterday': 'Yesterday',
'finance_no_reports': 'No reports',
'redemption_failed': 'Redemption failed',
'redemption_batch_failed': 'Batch redemption failed',
'redemption_no_records': 'No redemption records',
'redemption_minutes_ago': 'm ago',
'redemption_hours_ago': 'h ago',
'redemption_days_ago': 'd ago',
'store_no_stores': 'No stores',
'store_no_employees': 'No employees',
'settings_confirm_logout': 'Confirm Logout',
'settings_confirm_logout_desc': 'Are you sure you want to sign out?',
'settings_tier_benefits': 'Fee rate 1.2% · Advanced analytics',
'coupon_status_active': 'On Sale',
'coupon_status_pending': 'Pending',
'coupon_status_sold_out': 'Sold Out',
'coupon_status_delisted': 'Delisted',
'ai_agent_error_reply': 'Sorry, unable to respond. Please try again later.',
'coupon_detail_type_info': 'Face Value',
'coupon_detail_issue_price_label': 'Issue Price',
// AI Suggestion Card // AI Suggestion Card
'ai_suggestion_label': 'AI Suggestion', 'ai_suggestion_label': 'AI Suggestion',
'ai_suggestion_dismiss': 'Dismiss', 'ai_suggestion_dismiss': 'Dismiss',
@ -1682,6 +1744,37 @@ class AppLocalizations {
'settings_about': 'Genex について', 'settings_about': 'Genex について',
'settings_logout': 'ログアウト', 'settings_logout': 'ログアウト',
// Hardcoded string fixes
'credit_score_unit': '',
'credit_no_factors': '評価因子なし',
'credit_no_suggestions': '提案なし',
'finance_confirm_withdraw': '出金確認',
'finance_withdraw_amount': '出金金額',
'finance_withdraw_submitted': '出金申請が送信されました',
'finance_withdraw_failed': '出金に失敗しました',
'finance_no_transactions': '取引記録なし',
'finance_today': '本日',
'finance_yesterday': '昨日',
'finance_no_reports': 'レポートなし',
'redemption_failed': '検証に失敗しました',
'redemption_batch_failed': '一括検証に失敗しました',
'redemption_no_records': '検証記録なし',
'redemption_minutes_ago': '分前',
'redemption_hours_ago': '時間前',
'redemption_days_ago': '日前',
'store_no_stores': '店舗なし',
'store_no_employees': 'スタッフなし',
'settings_confirm_logout': 'ログアウト確認',
'settings_confirm_logout_desc': 'ログアウトしますか?',
'settings_tier_benefits': '手数料率 1.2% · 高度データ分析',
'coupon_status_active': '販売中',
'coupon_status_pending': '審査中',
'coupon_status_sold_out': '完売',
'coupon_status_delisted': '非公開',
'ai_agent_error_reply': '申し訳ございません、現在応答できません。後でもう一度お試しください。',
'coupon_detail_type_info': '額面',
'coupon_detail_issue_price_label': '発行価格',
// AI Suggestion Card // AI Suggestion Card
'ai_suggestion_label': 'AI 提案', 'ai_suggestion_label': 'AI 提案',
'ai_suggestion_dismiss': '無視', 'ai_suggestion_dismiss': '無視',

View File

@ -253,7 +253,7 @@ class _AiAgentPageState extends State<AiAgentPage> {
setState(() { setState(() {
_messages.add(_ChatMessage( _messages.add(_ChatMessage(
isAi: true, isAi: true,
text: '抱歉,暂时无法回复。请稍后重试。', text: context.t('ai_agent_error_reply'),
)); ));
_isSending = false; _isSending = false;
}); });

View File

@ -173,7 +173,7 @@ class _IssuerCouponDetailPageState extends State<IssuerCouponDetailPage> {
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
Text( Text(
'${coupon.couponType} · 面值 \$${coupon.faceValue.toStringAsFixed(0)} · 发行价 \$${coupon.currentPrice.toStringAsFixed(2)}', '${coupon.couponType} · ${context.t('coupon_detail_type_info')} \$${coupon.faceValue.toStringAsFixed(0)} · ${context.t('coupon_detail_issue_price_label')} \$${coupon.currentPrice.toStringAsFixed(2)}',
style: TextStyle(fontSize: 13, color: Colors.white.withValues(alpha: 0.8)), style: TextStyle(fontSize: 13, color: Colors.white.withValues(alpha: 0.8)),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),

View File

@ -323,19 +323,19 @@ class _CouponListPageState extends State<CouponListPage> {
switch (status) { switch (status) {
case 'active': case 'active':
color = AppColors.success; color = AppColors.success;
label = '在售中'; label = context.t('coupon_status_active');
break; break;
case 'pending': case 'pending':
color = AppColors.warning; color = AppColors.warning;
label = '待审核'; label = context.t('coupon_status_pending');
break; break;
case 'sold_out': case 'sold_out':
color = AppColors.info; color = AppColors.info;
label = '已售罄'; label = context.t('coupon_status_sold_out');
break; break;
case 'delisted': case 'delisted':
color = AppColors.textTertiary; color = AppColors.textTertiary;
label = '已下架'; label = context.t('coupon_status_delisted');
break; break;
default: default:
color = AppColors.textTertiary; color = AppColors.textTertiary;

View File

@ -128,7 +128,7 @@ class _CreditPageState extends State<CreditPage> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text(credit.grade, style: const TextStyle(fontSize: 32, fontWeight: FontWeight.w700, color: Colors.white)), Text(credit.grade, style: const TextStyle(fontSize: 32, fontWeight: FontWeight.w700, color: Colors.white)),
Text('${credit.score}', style: const TextStyle(fontSize: 14, color: Colors.white70)), Text('${credit.score}${context.t('credit_score_unit')}', style: const TextStyle(fontSize: 14, color: Colors.white70)),
], ],
), ),
), ),
@ -171,7 +171,7 @@ class _CreditPageState extends State<CreditPage> {
children: [ children: [
Text(f.name, style: const TextStyle(fontSize: 13)), Text(f.name, style: const TextStyle(fontSize: 13)),
Text( Text(
'${f.score.toInt()}分 (权重${(f.weight * 100).toInt()}%)', '${f.score.toInt()}${context.t('credit_score_unit')} (${(f.weight * 100).toInt()}%)',
style: const TextStyle(fontSize: 12, color: AppColors.textSecondary), style: const TextStyle(fontSize: 12, color: AppColors.textSecondary),
), ),
], ],
@ -193,7 +193,7 @@ class _CreditPageState extends State<CreditPage> {
if (factors.isEmpty) if (factors.isEmpty)
const Padding( const Padding(
padding: EdgeInsets.all(16), padding: EdgeInsets.all(16),
child: Center(child: Text('暂无评分因子', style: TextStyle(color: AppColors.textTertiary))), child: Center(child: Text(context.t('credit_no_factors'), style: const TextStyle(color: AppColors.textTertiary))),
), ),
], ],
), ),
@ -285,8 +285,8 @@ class _CreditPageState extends State<CreditPage> {
color: AppColors.primarySurface, color: AppColors.primarySurface,
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
child: const Center( child: Center(
child: Text('暂无建议', style: TextStyle(color: AppColors.textSecondary)), child: Text(context.t('credit_no_suggestions'), style: const TextStyle(color: AppColors.textSecondary)),
), ),
) )
else else

View File

@ -355,7 +355,7 @@ class _IssuerDashboardPageState extends State<IssuerDashboardPage> {
children: [ children: [
Text(context.t('dashboard_credit_rating'), style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), Text(context.t('dashboard_credit_rating'), style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600)),
Text( Text(
stats != null ? '${context.t('dashboard_credit_gap')} (${stats.creditScore})' : context.t('dashboard_credit_gap'), stats != null ? '${context.t('dashboard_credit_gap')} (${stats.creditScore}${context.t('credit_score_unit')})' : context.t('dashboard_credit_gap'),
style: const TextStyle(fontSize: 12, color: AppColors.textTertiary), style: const TextStyle(fontSize: 12, color: AppColors.textTertiary),
), ),
], ],

View File

@ -111,8 +111,8 @@ class _OverviewTabState extends State<_OverviewTab> {
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
title: const Text('确认提现'), title: Text(context.t('finance_confirm_withdraw')),
content: Text('提现金额: \$${_balance!.withdrawable.toStringAsFixed(2)}'), content: Text('${context.t('finance_withdraw_amount')}: \$${_balance!.withdrawable.toStringAsFixed(2)}'),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(context.t('cancel'))), TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(context.t('cancel'))),
ElevatedButton(onPressed: () => Navigator.pop(ctx, true), child: Text(context.t('confirm'))), ElevatedButton(onPressed: () => Navigator.pop(ctx, true), child: Text(context.t('confirm'))),
@ -126,14 +126,14 @@ class _OverviewTabState extends State<_OverviewTab> {
await _financeService.withdraw(_balance!.withdrawable); await _financeService.withdraw(_balance!.withdrawable);
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('提现申请已提交'), backgroundColor: AppColors.success), SnackBar(content: Text(context.t('finance_withdraw_submitted')), backgroundColor: AppColors.success),
); );
_loadData(); _loadData();
} catch (e) { } catch (e) {
debugPrint('[FinancePage] withdraw error: $e'); debugPrint('[FinancePage] withdraw error: $e');
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('提现失败: $e'), backgroundColor: AppColors.error), SnackBar(content: Text('${context.t('finance_withdraw_failed')}: $e'), backgroundColor: AppColors.error),
); );
} }
} }
@ -397,8 +397,8 @@ class _TransactionDetailTabState extends State<_TransactionDetailTab> {
} }
if (_transactions.isEmpty) { if (_transactions.isEmpty) {
return const Center( return Center(
child: Text('暂无交易记录', style: TextStyle(color: AppColors.textSecondary)), child: Text(context.t('finance_no_transactions'), style: const TextStyle(color: AppColors.textSecondary)),
); );
} }
@ -433,10 +433,10 @@ class _TransactionDetailTabState extends State<_TransactionDetailTab> {
final now = DateTime.now(); final now = DateTime.now();
final isToday = dt.year == now.year && dt.month == now.month && dt.day == now.day; final isToday = dt.year == now.year && dt.month == now.month && dt.day == now.day;
final timeStr = '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; final timeStr = '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
if (isToday) return '今天 $timeStr'; if (isToday) return '${context.t('finance_today')} $timeStr';
final yesterday = now.subtract(const Duration(days: 1)); final yesterday = now.subtract(const Duration(days: 1));
final isYesterday = dt.year == yesterday.year && dt.month == yesterday.month && dt.day == yesterday.day; final isYesterday = dt.year == yesterday.year && dt.month == yesterday.month && dt.day == yesterday.day;
if (isYesterday) return '昨天 $timeStr'; if (isYesterday) return '${context.t('finance_yesterday')} $timeStr';
return '${dt.month}/${dt.day} $timeStr'; return '${dt.month}/${dt.day} $timeStr';
} }
} }
@ -527,17 +527,17 @@ class _ReconciliationTabState extends State<_ReconciliationTab> {
const SizedBox(height: 20), const SizedBox(height: 20),
if (reports.isEmpty) if (reports.isEmpty)
const Center( Center(
child: Padding( child: Padding(
padding: EdgeInsets.all(32), padding: const EdgeInsets.all(32),
child: Text('暂无对账报表', style: TextStyle(color: AppColors.textSecondary)), child: Text(context.t('finance_no_reports'), style: const TextStyle(color: AppColors.textSecondary)),
), ),
) )
else else
...reports.map((r) { ...reports.map((r) {
final title = r['title'] ?? ''; final title = r['title'] ?? '';
final summary = r['summary'] ?? ''; final summary = r['summary'] ?? '';
final status = r['status'] ?? '已生成'; final status = r['status'] ?? '--';
return Container( return Container(
margin: const EdgeInsets.only(bottom: 12), margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),

View File

@ -94,7 +94,7 @@ class _ScanRedeemTabState extends State<_ScanRedeemTab> {
if (!mounted) return; if (!mounted) return;
setState(() => _isRedeeming = false); setState(() => _isRedeeming = false);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('核销失败: $e'), backgroundColor: AppColors.error), SnackBar(content: Text('${context.t('redemption_failed')}: $e'), backgroundColor: AppColors.error),
); );
} }
} }
@ -196,7 +196,7 @@ class _ScanRedeemTabState extends State<_ScanRedeemTab> {
_loadTodayStats(); _loadTodayStats();
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('成功: ${result.successCount}, 失败: ${result.failCount}'), content: Text('${result.successCount} / ${result.failCount} (${context.t('redemption_failed')})'),
backgroundColor: result.failCount == 0 ? AppColors.success : AppColors.warning, backgroundColor: result.failCount == 0 ? AppColors.success : AppColors.warning,
), ),
); );
@ -205,7 +205,7 @@ class _ScanRedeemTabState extends State<_ScanRedeemTab> {
setLocalState(() => isBatching = false); setLocalState(() => isBatching = false);
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('批量核销失败: $e'), backgroundColor: AppColors.error), SnackBar(content: Text('${context.t('redemption_batch_failed')}: $e'), backgroundColor: AppColors.error),
); );
} }
} }
@ -413,8 +413,8 @@ class _RedeemHistoryTabState extends State<_RedeemHistoryTab> {
} }
if (_records.isEmpty) { if (_records.isEmpty) {
return const Center( return Center(
child: Text('暂无核销记录', style: TextStyle(color: AppColors.textSecondary)), child: Text(context.t('redemption_no_records'), style: const TextStyle(color: AppColors.textSecondary)),
); );
} }
@ -460,9 +460,9 @@ class _RedeemHistoryTabState extends State<_RedeemHistoryTab> {
String _formatTime(DateTime dt) { String _formatTime(DateTime dt) {
final now = DateTime.now(); final now = DateTime.now();
final diff = now.difference(dt); final diff = now.difference(dt);
if (diff.inMinutes < 60) return '${diff.inMinutes}分钟前'; if (diff.inMinutes < 60) return '${diff.inMinutes}${context.t('redemption_minutes_ago')}';
if (diff.inHours < 24) return '${diff.inHours}小时前'; if (diff.inHours < 24) return '${diff.inHours}${context.t('redemption_hours_ago')}';
if (diff.inDays < 7) return '${diff.inDays}天前'; if (diff.inDays < 7) return '${diff.inDays}${context.t('redemption_days_ago')}';
return '${dt.month}/${dt.day}'; return '${dt.month}/${dt.day}';
} }
} }

View File

@ -60,8 +60,8 @@ class _SettingsPageState extends State<SettingsPage> {
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
title: const Text('确认退出'), title: Text(context.t('settings_confirm_logout')),
content: const Text('确定要退出登录吗?'), content: Text(context.t('settings_confirm_logout_desc')),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(context.t('cancel'))), TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(context.t('cancel'))),
ElevatedButton( ElevatedButton(
@ -272,7 +272,7 @@ class _SettingsPageState extends State<SettingsPage> {
: context.t('settings_gold_issuer'), : context.t('settings_gold_issuer'),
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: AppColors.tierGold), style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: AppColors.tierGold),
), ),
const Text('手续费率 1.2% · 高级数据分析', style: TextStyle(fontSize: 12, color: AppColors.textSecondary)), Text(context.t('settings_tier_benefits'), style: const TextStyle(fontSize: 12, color: AppColors.textSecondary)),
], ],
), ),
), ),

View File

@ -107,8 +107,8 @@ class _StoreListTabState extends State<_StoreListTab> {
} }
if (_stores.isEmpty) { if (_stores.isEmpty) {
return const Center( return Center(
child: Text('暂无门店', style: TextStyle(color: AppColors.textSecondary)), child: Text(context.t('store_no_stores'), style: const TextStyle(color: AppColors.textSecondary)),
); );
} }
@ -265,8 +265,8 @@ class _EmployeeListTabState extends State<_EmployeeListTab> {
} }
if (_employees.isEmpty) { if (_employees.isEmpty) {
return const Center( return Center(
child: Text('暂无员工', style: TextStyle(color: AppColors.textSecondary)), child: Text(context.t('store_no_employees'), style: const TextStyle(color: AppColors.textSecondary)),
); );
} }