feat(flutter): i18n体系(zh/zh_TW/en) + 智能体解聘功能

- 建立完整 flutter_localizations i18n 体系:zh/zh_TW/en 三语言
- l10n.yaml + ARB 文件 (app_zh.arb 约120键作模板,zh_TW/en 对应覆盖)
- localeProvider 连接 SharedPreferences language 设置,实时切换语言
- 设置页加入语言选择器(简体中文/繁体中文/English)
- 我的智能体页实现解聘(解聘确认弹窗 + DELETE API)与重命名功能
- 全部页面 (~18个) UI 字符串替换为 AppLocalizations.of(context).xxx

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-03-08 00:05:55 -08:00
parent 3074ea54a9
commit 6be84617d2
27 changed files with 6299 additions and 456 deletions

5
it0_app/l10n.yaml Normal file
View File

@ -0,0 +1,5 @@
arb-dir: lib/l10n
template-arb-file: app_zh.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
nullable-getter: false

View File

@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'core/router/app_router.dart';
import 'core/theme/app_theme.dart';
import 'features/settings/presentation/providers/settings_providers.dart';
import 'l10n/app_localizations.dart';
class IT0App extends ConsumerWidget {
const IT0App({super.key});
@ -11,9 +12,13 @@ class IT0App extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final router = ref.watch(routerProvider);
final themeMode = ref.watch(themeModeProvider);
final locale = ref.watch(localeProvider);
return MaterialApp.router(
title: '我智能体',
onGenerateTitle: (ctx) => AppLocalizations.of(ctx).appTitle,
locale: locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: themeMode,

View File

@ -1,6 +1,7 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:it0_app/l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../updater/update_service.dart';
import '../network/connectivity_provider.dart';
@ -161,15 +162,15 @@ class _ScaffoldWithNavState extends ConsumerState<ScaffoldWithNav>
bottomNavigationBar: NavigationBar(
selectedIndex: currentIndex,
destinations: [
const NavigationDestination(
icon: Icon(Icons.home_outlined),
selectedIcon: Icon(Icons.home),
label: '主页',
NavigationDestination(
icon: const Icon(Icons.home_outlined),
selectedIcon: const Icon(Icons.home),
label: AppLocalizations.of(context).navHome,
),
const NavigationDestination(
icon: Icon(Icons.smart_toy_outlined),
selectedIcon: Icon(Icons.smart_toy),
label: '我的智能体',
NavigationDestination(
icon: const Icon(Icons.smart_toy_outlined),
selectedIcon: const Icon(Icons.smart_toy),
label: AppLocalizations.of(context).navMyAgents,
),
NavigationDestination(
icon: Badge(
@ -178,12 +179,12 @@ class _ScaffoldWithNavState extends ConsumerState<ScaffoldWithNav>
child: const Icon(Icons.credit_card_outlined),
),
selectedIcon: const Icon(Icons.credit_card),
label: '账单',
label: AppLocalizations.of(context).navBilling,
),
const NavigationDestination(
icon: Icon(Icons.person_outline),
selectedIcon: Icon(Icons.person),
label: '',
NavigationDestination(
icon: const Icon(Icons.person_outline),
selectedIcon: const Icon(Icons.person),
label: AppLocalizations.of(context).navProfile,
),
],
onDestinationSelected: (index) {

View File

@ -4,6 +4,7 @@ import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:it0_app/l10n/app_localizations.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:image_picker/image_picker.dart';
import 'package:file_picker/file_picker.dart';
@ -797,11 +798,11 @@ class _AgentCallPageState extends ConsumerState<AgentCallPage>
),
),
const SizedBox(width: 8),
const Text('我智能体',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 15)),
Text(AppLocalizations.of(context).appTitle,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 15)),
const SizedBox(width: 8),
Text(
_agentState == 'thinking' ? '思考中...' : _durationLabel,
_agentState == 'thinking' ? AppLocalizations.of(context).agentCallThinking : _durationLabel,
style: const TextStyle(color: AppColors.textSecondary, fontSize: 13)),
if (_isReconnecting) ...[
const SizedBox(width: 6),
@ -944,13 +945,13 @@ class _AgentCallPageState extends ConsumerState<AgentCallPage>
String get _statusText {
switch (_phase) {
case _CallPhase.ringing:
return '我智能体 语音通话';
return AppLocalizations.of(context).agentCallRingingStatus;
case _CallPhase.connecting:
return '连接中...';
return AppLocalizations.of(context).agentCallConnectingStatus;
case _CallPhase.active:
return '我智能体';
return AppLocalizations.of(context).agentCallActiveStatus;
case _CallPhase.ended:
return '通话结束';
return AppLocalizations.of(context).agentCallEndedStatus;
}
}
@ -964,7 +965,7 @@ class _AgentCallPageState extends ConsumerState<AgentCallPage>
case _CallPhase.connecting:
return '正在建立安全连接';
case _CallPhase.active:
if (_agentState == 'thinking') return '思考中...';
if (_agentState == 'thinking') return AppLocalizations.of(context).agentCallThinking;
if (_agentState == 'initializing') return '正在初始化...';
return '语音通话中';
case _CallPhase.ended:

View File

@ -5,6 +5,7 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import '../../../../core/theme/app_colors.dart';
import '../../data/providers/auth_provider.dart';
import 'package:it0_app/l10n/app_localizations.dart';
enum _LoginMode { password, otp }
@ -41,7 +42,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
final phone = _phoneController.text.trim();
if (phone.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请先输入手机号')),
SnackBar(content: Text(AppLocalizations.of(context).enterPhoneFirstError)),
);
return;
}
@ -88,9 +89,9 @@ class _LoginPageState extends ConsumerState<LoginPage> {
children: [
SvgPicture.asset('assets/icons/logo.svg', width: 96, height: 96),
const SizedBox(height: 12),
const Text(
'我智能体',
style: TextStyle(
Text(
AppLocalizations.of(context).appTitle,
style: const TextStyle(
color: AppColors.textPrimary,
fontSize: 28,
fontWeight: FontWeight.bold,
@ -98,9 +99,9 @@ class _LoginPageState extends ConsumerState<LoginPage> {
),
),
const SizedBox(height: 4),
const Text(
'服务器集群运维智能体',
style: TextStyle(color: AppColors.textSecondary, fontSize: 14),
Text(
AppLocalizations.of(context).appSubtitle,
style: const TextStyle(color: AppColors.textSecondary, fontSize: 14),
),
const SizedBox(height: 32),
@ -113,8 +114,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
),
child: Row(
children: [
Expanded(child: _modeButton('密码登录', _LoginMode.password)),
Expanded(child: _modeButton('验证码登录', _LoginMode.otp)),
Expanded(child: _modeButton(AppLocalizations.of(context).loginPasswordTab, _LoginMode.password)),
Expanded(child: _modeButton(AppLocalizations.of(context).loginOtpTab, _LoginMode.otp)),
],
),
),
@ -124,10 +125,10 @@ class _LoginPageState extends ConsumerState<LoginPage> {
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: '邮箱',
hintText: 'user@example.com',
prefixIcon: Icon(Icons.email_outlined),
decoration: InputDecoration(
labelText: AppLocalizations.of(context).emailLabel,
hintText: AppLocalizations.of(context).emailHint,
prefixIcon: const Icon(Icons.email_outlined),
),
validator: (v) {
if (v == null || v.isEmpty) return '请输入邮箱地址';
@ -139,9 +140,9 @@ class _LoginPageState extends ConsumerState<LoginPage> {
TextFormField(
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: '密码',
prefixIcon: Icon(Icons.lock_outline),
decoration: InputDecoration(
labelText: AppLocalizations.of(context).passwordLabel,
prefixIcon: const Icon(Icons.lock_outline),
),
validator: (v) => (v == null || v.isEmpty) ? '请输入密码' : null,
onFieldSubmitted: (_) => _handlePasswordLogin(),
@ -150,10 +151,10 @@ class _LoginPageState extends ConsumerState<LoginPage> {
TextFormField(
controller: _phoneController,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
labelText: '手机号',
hintText: '+86 138 0000 0000',
prefixIcon: Icon(Icons.phone_outlined),
decoration: InputDecoration(
labelText: AppLocalizations.of(context).phoneLabel,
hintText: AppLocalizations.of(context).phoneHint,
prefixIcon: const Icon(Icons.phone_outlined),
),
validator: (v) => (v == null || v.isEmpty) ? '请输入手机号' : null,
),
@ -166,10 +167,10 @@ class _LoginPageState extends ConsumerState<LoginPage> {
controller: _otpController,
keyboardType: TextInputType.number,
maxLength: 6,
decoration: const InputDecoration(
labelText: '验证码',
hintText: '6 位数字',
prefixIcon: Icon(Icons.sms_outlined),
decoration: InputDecoration(
labelText: AppLocalizations.of(context).otpLabel,
hintText: AppLocalizations.of(context).otpHint,
prefixIcon: const Icon(Icons.sms_outlined),
counterText: '',
),
validator: (v) => (v == null || v.isEmpty) ? '请输入验证码' : null,
@ -183,10 +184,10 @@ class _LoginPageState extends ConsumerState<LoginPage> {
onPressed: (_smsSending || _smsCooldown > 0) ? null : _sendSmsCode,
child: Text(
_smsSending
? '发送中'
? AppLocalizations.of(context).sendingLabel
: _smsCooldown > 0
? '${_smsCooldown}s'
: '获取验证码',
: AppLocalizations.of(context).getOtpButton,
style: const TextStyle(fontSize: 13),
),
),
@ -234,13 +235,13 @@ class _LoginPageState extends ConsumerState<LoginPage> {
child: CircularProgressIndicator(
strokeWidth: 2, color: Colors.white),
)
: const Text('登录', style: TextStyle(fontSize: 16)),
: Text(AppLocalizations.of(context).loginButton, style: const TextStyle(fontSize: 16)),
),
),
const SizedBox(height: 32),
Text(
'账号由管理员在后台创建或通过邀请链接注册',
style: TextStyle(color: AppColors.textMuted, fontSize: 12),
AppLocalizations.of(context).accountCreationNote,
style: const TextStyle(color: AppColors.textMuted, fontSize: 12),
textAlign: TextAlign.center,
),
],

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:it0_app/l10n/app_localizations.dart';
import '../../../../core/theme/app_colors.dart';
import '../providers/billing_provider.dart';
@ -14,7 +15,7 @@ class BillingOverviewPage extends ConsumerWidget {
final cardColor = isDark ? AppColors.surface : Colors.white;
return Scaffold(
appBar: AppBar(title: const Text('订阅与用量')),
appBar: AppBar(title: Text(AppLocalizations.of(context).billingTitle)),
body: billingAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('加载失败: $e')),
@ -58,7 +59,7 @@ class BillingOverviewPage extends ConsumerWidget {
_showUpgradeDialog(context);
},
icon: const Icon(Icons.upgrade),
label: const Text('升级套餐'),
label: Text(AppLocalizations.of(context).upgradeButton),
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 48),
backgroundColor: AppColors.primary,
@ -78,12 +79,12 @@ class BillingOverviewPage extends ConsumerWidget {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('升级套餐'),
content: const Text('请前往 Web 管理后台 → 账单 → 套餐 完成升级。'),
title: Text(AppLocalizations.of(ctx).upgradeDialogTitle),
content: Text(AppLocalizations.of(ctx).upgradeDialogMessage),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('知道了'),
child: Text(AppLocalizations.of(ctx).acknowledgeButton),
),
],
),
@ -113,14 +114,17 @@ class _SubscriptionCard extends StatelessWidget {
_ => Colors.grey,
};
String _statusLabel(String status) => switch (status) {
'active' => '正常',
'trialing' => '试用期',
'past_due' => '待付款',
'cancelled' => '已取消',
'expired' => '已过期',
String _statusLabel(String status, BuildContext context) {
final l = AppLocalizations.of(context);
return switch (status) {
'active' => l.billingStatusActive,
'trialing' => l.billingStatusTrialing,
'past_due' => l.billingStatusPastDue,
'cancelled' => l.billingStatusCancelled,
'expired' => l.billingStatusExpired,
_ => status,
};
}
@override
Widget build(BuildContext context) {
@ -140,7 +144,7 @@ class _SubscriptionCard extends StatelessWidget {
children: [
const Icon(Icons.credit_card, size: 20, color: AppColors.primary),
const SizedBox(width: 8),
Text('当前套餐', style: theme.textTheme.bodySmall?.copyWith(color: Colors.grey)),
Text(AppLocalizations.of(context).currentPlanLabel, style: theme.textTheme.bodySmall?.copyWith(color: Colors.grey)),
],
),
const SizedBox(height: 12),
@ -159,7 +163,7 @@ class _SubscriptionCard extends StatelessWidget {
borderRadius: BorderRadius.circular(20),
),
child: Text(
_statusLabel(status),
_statusLabel(status, context),
style: TextStyle(fontSize: 12, color: _statusColor(status), fontWeight: FontWeight.w600),
),
),
@ -168,7 +172,7 @@ class _SubscriptionCard extends StatelessWidget {
if (periodEnd != null) ...[
const SizedBox(height: 6),
Text(
'当期结束:${_formatDate(periodEnd!)}',
'${AppLocalizations.of(context).periodEndLabel}${_formatDate(periodEnd!)}',
style: theme.textTheme.bodySmall?.copyWith(color: Colors.grey),
),
],
@ -222,7 +226,7 @@ class _UsageCard extends StatelessWidget {
children: [
const Icon(Icons.bolt, size: 20, color: AppColors.primary),
const SizedBox(width: 8),
Text('本月 Token 用量', style: theme.textTheme.bodySmall?.copyWith(color: Colors.grey)),
Text(AppLocalizations.of(context).tokenUsageLabel, style: theme.textTheme.bodySmall?.copyWith(color: Colors.grey)),
],
),
const SizedBox(height: 16),
@ -234,7 +238,7 @@ class _UsageCard extends StatelessWidget {
style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
Text(
isUnlimited ? '不限量' : _formatTokens(limitTokens),
isUnlimited ? AppLocalizations.of(context).unlimitedLabel : _formatTokens(limitTokens),
style: theme.textTheme.bodyMedium?.copyWith(color: Colors.grey),
),
],
@ -297,7 +301,7 @@ class _InvoiceCard extends StatelessWidget {
borderRadius: BorderRadius.circular(20),
),
child: Text(
invoice.status == 'paid' ? '已付款' : '待付款',
invoice.status == 'paid' ? AppLocalizations.of(context).invoicePaidStatus : AppLocalizations.of(context).invoiceUnpaidStatus,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,

View File

@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:it0_app/l10n/app_localizations.dart';
import 'package:image_picker/image_picker.dart';
import 'package:file_picker/file_picker.dart';
import '../../../../core/theme/app_colors.dart';
@ -60,7 +61,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
Future<void> _transcribeToInput(String audioPath) async {
setState(() {
_sttLoading = true;
_messageController.text = '识别中…';
_messageController.text = AppLocalizations.of(context).chatRecognizingLabel;
});
try {
final text = await ref.read(chatProvider.notifier).transcribeAudio(audioPath);
@ -76,7 +77,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
if (mounted) {
setState(() => _messageController.text = '');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('语音识别失败,请重试')),
SnackBar(content: Text(AppLocalizations.of(context).chatSpeechRecognitionError)),
);
}
} finally {
@ -125,18 +126,18 @@ class _ChatPageState extends ConsumerState<ChatPage> {
children: [
ListTile(
leading: const Icon(Icons.photo_library),
title: const Text('从相册选择'),
title: Text(AppLocalizations.of(context).chatSelectFromAlbum),
subtitle: const Text('支持多选'),
onTap: () { Navigator.pop(ctx); _pickMultipleImages(); },
),
ListTile(
leading: const Icon(Icons.camera_alt),
title: const Text('拍照'),
title: Text(AppLocalizations.of(context).chatTakePhoto),
onTap: () { Navigator.pop(ctx); _pickImage(ImageSource.camera); },
),
ListTile(
leading: const Icon(Icons.attach_file),
title: const Text('选择文件'),
title: Text(AppLocalizations.of(context).chatSelectFile),
subtitle: const Text('图片、PDF'),
onTap: () { Navigator.pop(ctx); _pickFile(); },
),
@ -400,7 +401,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
case MessageType.thinking:
return TimelineEventNode(
status: isStreamingNow ? NodeStatus.active : NodeStatus.completed,
label: '思考中...',
label: AppLocalizations.of(context).chatThinkingLabel,
isFirst: isFirst,
isLast: isLast,
content: _CollapsibleThinking(
@ -429,7 +430,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
final isError = tool?.status == ToolStatus.error;
return TimelineEventNode(
status: isError ? NodeStatus.error : NodeStatus.completed,
label: isError ? '执行失败' : '执行结果',
label: isError ? AppLocalizations.of(context).chatExecutionFailedLabel : AppLocalizations.of(context).chatExecutionResultLabel,
isFirst: isFirst,
isLast: isLast,
icon: isError ? Icons.cancel_outlined : Icons.check_circle_outline,
@ -444,7 +445,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
case MessageType.approval:
return TimelineEventNode(
status: NodeStatus.warning,
label: '需要审批',
label: AppLocalizations.of(context).chatNeedsApprovalLabel,
isFirst: isFirst,
isLast: isLast,
icon: Icons.shield_outlined,
@ -462,7 +463,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
case MessageType.standingOrderDraft:
return TimelineEventNode(
status: NodeStatus.warning,
label: '常驻指令草案',
label: AppLocalizations.of(context).chatStandingOrderDraftLabel,
isFirst: isFirst,
isLast: isLast,
icon: Icons.schedule,
@ -486,7 +487,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
default:
return TimelineEventNode(
status: isStreamingNow ? NodeStatus.active : NodeStatus.completed,
label: isStreamingNow ? '回复中...' : '回复',
label: isStreamingNow ? AppLocalizations.of(context).chatReplyingLabel : AppLocalizations.of(context).chatReplyLabel,
isFirst: isFirst,
isLast: isLast,
icon: isStreamingNow ? null : Icons.check_circle_outline,
@ -527,8 +528,8 @@ class _ChatPageState extends ConsumerState<ChatPage> {
children: [
RobotAvatar(state: robotState, size: 32),
const SizedBox(width: 8),
const Text('我智能体',
style: TextStyle(
Text(AppLocalizations.of(context).appTitle,
style: const TextStyle(
fontSize: 16, fontWeight: FontWeight.w600)),
],
);
@ -537,20 +538,20 @@ class _ChatPageState extends ConsumerState<ChatPage> {
actions: [
IconButton(
icon: const Icon(Icons.edit_outlined, size: 20),
tooltip: '新对话',
tooltip: AppLocalizations.of(context).chatNewConversationTooltip,
visualDensity: VisualDensity.compact,
onPressed: () => ref.read(chatProvider.notifier).startNewChat(),
),
if (chatState.isStreaming)
IconButton(
icon: const Icon(Icons.stop_circle_outlined, size: 20),
tooltip: '停止',
tooltip: AppLocalizations.of(context).chatStopTooltip,
visualDensity: VisualDensity.compact,
onPressed: () => ref.read(chatProvider.notifier).cancelCurrentTask(),
),
IconButton(
icon: const Icon(Icons.call, size: 20),
tooltip: '语音通话',
tooltip: AppLocalizations.of(context).chatVoiceCallTooltip,
visualDensity: VisualDensity.compact,
onPressed: _openVoiceCall,
),
@ -603,7 +604,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
_needsWorkingNode(chatState)) {
return TimelineEventNode(
status: NodeStatus.active,
label: '处理中...',
label: AppLocalizations.of(context).chatProcessingLabel,
isFirst: false,
isLast: false,
);
@ -644,13 +645,13 @@ class _ChatPageState extends ConsumerState<ChatPage> {
Icon(Icons.smart_toy_outlined, size: 64, color: AppColors.textMuted),
const SizedBox(height: 16),
Text(
'开始与 我智能体 对话',
style: TextStyle(color: AppColors.textSecondary, fontSize: 16),
AppLocalizations.of(context).chatStartConversationPrompt,
style: const TextStyle(color: AppColors.textSecondary, fontSize: 16),
),
const SizedBox(height: 8),
Text(
'输入指令或拨打语音通话',
style: TextStyle(color: AppColors.textMuted, fontSize: 13),
AppLocalizations.of(context).chatInputInstructionHint,
style: const TextStyle(color: AppColors.textMuted, fontSize: 13),
),
const SizedBox(height: 24),
OutlinedButton.icon(
@ -700,7 +701,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
padding: const EdgeInsets.only(left: 4),
child: IconButton(
icon: const Icon(Icons.add_circle_outline, size: 22),
tooltip: '添加图片',
tooltip: AppLocalizations.of(context).chatAddImageTooltip,
onPressed: isAwaitingApproval ? null : _showAttachmentOptions,
),
),
@ -708,7 +709,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
child: TextField(
controller: _messageController,
decoration: InputDecoration(
hintText: isStreaming ? '追加指令...' : '输入指令...',
hintText: isStreaming ? AppLocalizations.of(context).chatAdditionalInstructionHint : AppLocalizations.of(context).chatInstructionHint,
hintStyle: TextStyle(color: AppColors.textMuted),
border: InputBorder.none,
contentPadding: EdgeInsets.only(
@ -729,14 +730,14 @@ class _ChatPageState extends ConsumerState<ChatPage> {
children: [
IconButton(
icon: const Icon(Icons.send, color: AppColors.info, size: 20),
tooltip: '追加指令',
tooltip: AppLocalizations.of(context).chatInjectionTooltip,
onPressed: _inject,
),
Padding(
padding: const EdgeInsets.only(right: 4),
child: IconButton(
icon: const Icon(Icons.stop_circle_outlined, color: AppColors.error, size: 20),
tooltip: '停止',
tooltip: AppLocalizations.of(context).chatStopTooltip,
onPressed: () => ref.read(chatProvider.notifier).cancelCurrentTask(),
),
),
@ -829,7 +830,7 @@ class _CollapsibleCodeBlockState extends State<_CollapsibleCodeBlock> {
child: Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
_expanded ? '收起' : '展开 ($lineCount 行)',
_expanded ? AppLocalizations.of(context).chatCollapseLabel : '展开 ($lineCount 行)',
style: TextStyle(
color: AppColors.info,
fontSize: 12,
@ -1000,7 +1001,7 @@ class _StandingOrderContent extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
minimumSize: Size.zero,
),
child: const Text('取消', style: TextStyle(fontSize: 12)),
child: Text(AppLocalizations.of(context).cancelButton, style: const TextStyle(fontSize: 12)),
),
const SizedBox(width: 8),
FilledButton(

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:it0_app/l10n/app_localizations.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/widgets/robot_avatar.dart';
import '../../../chat/presentation/providers/chat_providers.dart';
@ -47,9 +48,9 @@ class HomePage extends ConsumerWidget {
),
),
const SizedBox(height: 2),
const Text(
'我智能体 随时为你服务',
style: TextStyle(
Text(
AppLocalizations.of(context).homeSubtitle,
style: const TextStyle(
fontSize: 12,
color: AppColors.textSecondary,
),
@ -103,13 +104,13 @@ class HomePage extends ConsumerWidget {
const SizedBox(height: 20),
// IT0 official agent cards
const _SectionHeader(title: 'IT0 官方智能体'),
_SectionHeader(title: AppLocalizations.of(context).officialAgentsSection),
const SizedBox(height: 12),
const _OfficialAgentsRow(),
const SizedBox(height: 20),
// Guide if nothing created yet
const _SectionHeader(title: '我的智能体'),
_SectionHeader(title: AppLocalizations.of(context).myAgentsSection),
const SizedBox(height: 12),
const _MyAgentsPlaceholder(),
const SizedBox(height: 20),
@ -190,9 +191,9 @@ class _AgentStatusCard extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'我智能体',
style: TextStyle(
Text(
AppLocalizations.of(context).appTitle,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
@ -271,39 +272,40 @@ class _SectionHeader extends StatelessWidget {
class _OfficialAgentsRow extends StatelessWidget {
const _OfficialAgentsRow();
static const _agents = [
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context);
final agents = [
_AgentCard(
name: '我智能体 运维助手',
desc: '服务器管理、SSH 执行、日志分析',
name: l.officialAgent1Name,
desc: l.officialAgent1Desc,
icon: Icons.dns_outlined,
color: Color(0xFF6366F1),
color: const Color(0xFF6366F1),
isOfficial: true,
),
_AgentCard(
name: '安全审计助手',
desc: '漏洞扫描、权限审查、合规检查',
name: l.officialAgent2Name,
desc: l.officialAgent2Desc,
icon: Icons.security_outlined,
color: Color(0xFF22C55E),
color: const Color(0xFF22C55E),
isOfficial: true,
),
_AgentCard(
name: '数据库巡检',
desc: '慢查询分析、索引优化、备份验证',
name: l.officialAgent3Name,
desc: l.officialAgent3Desc,
icon: Icons.storage_outlined,
color: Color(0xFF0EA5E9),
color: const Color(0xFF0EA5E9),
isOfficial: true,
),
];
@override
Widget build(BuildContext context) {
return SizedBox(
height: 130,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: _agents.length,
itemCount: agents.length,
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemBuilder: (context, index) => _agents[index],
itemBuilder: (context, index) => agents[index],
),
);
}
@ -358,7 +360,7 @@ class _AgentCard extends StatelessWidget {
borderRadius: BorderRadius.circular(6),
),
child: Text(
'官方',
AppLocalizations.of(context).officialBadge,
style: TextStyle(
fontSize: 10,
color: color,
@ -423,18 +425,18 @@ class _MyAgentsPlaceholder extends StatelessWidget {
color: AppColors.primary,
),
const SizedBox(height: 12),
const Text(
'还没有自己的智能体',
style: TextStyle(
Text(
AppLocalizations.of(context).noOwnAgentsTitle,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 6),
const Text(
'点击下方机器人按钮,告诉 我智能体\n"帮我招募一个 OpenClaw 智能体"',
style: TextStyle(
Text(
AppLocalizations.of(context).noOwnAgentsDesc,
style: const TextStyle(
fontSize: 13,
color: AppColors.textSecondary,
height: 1.5,
@ -454,15 +456,16 @@ class _MyAgentsPlaceholder extends StatelessWidget {
class _QuickTipsCard extends StatelessWidget {
const _QuickTipsCard();
static const _tips = [
'💬 "帮我招募一个监控 GitHub Actions 的智能体"',
'🔧 "把我的 OpenClaw 配置导出为 JSON"',
'📊 "分析我的服务器最近7天的负载情况"',
'🛡️ "帮我设置每天凌晨2点自动备份数据库"',
];
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context);
final tips = [
l.quickTip1,
l.quickTip2,
l.quickTip3,
l.quickTip4,
];
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
@ -472,16 +475,16 @@ class _QuickTipsCard extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'你可以这样说...',
style: TextStyle(
Text(
l.quickTipsHeader,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.textSecondary,
),
),
const SizedBox(height: 10),
..._tips.map(
...tips.map(
(tip) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:it0_app/l10n/app_localizations.dart';
import '../../../../core/config/api_endpoints.dart';
import '../../../../core/errors/error_handler.dart';
import '../../../../core/network/dio_client.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/utils/date_formatter.dart';
@ -79,12 +81,16 @@ const _statusColors = {
'error': Color(0xFFEF4444),
};
const _statusLabels = {
'running': '运行中',
'deploying': '部署中',
'stopped': '已停止',
'error': '错误',
String _statusLabel(BuildContext context, String status) {
final l = AppLocalizations.of(context);
return switch (status) {
'running' => l.statusRunning,
'deploying' => l.statusDeploying,
'stopped' => l.statusStopped,
'error' => l.statusError,
_ => status,
};
}
const _statusIcons = {
'running': Icons.circle,
@ -108,7 +114,7 @@ class MyAgentsPage extends ConsumerWidget {
backgroundColor: AppColors.background,
appBar: AppBar(
backgroundColor: AppColors.background,
title: const Text('我的智能体'),
title: Text(AppLocalizations.of(context).myAgentsTitle),
actions: [
IconButton(
icon: const Icon(Icons.refresh_outlined),
@ -145,22 +151,22 @@ class MyAgentsPage extends ConsumerWidget {
child: const Icon(Icons.smart_toy_outlined, size: 60, color: AppColors.primary),
),
const SizedBox(height: 24),
const Text(
'招募你的专属智能体',
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: AppColors.textPrimary),
Text(
AppLocalizations.of(context).myAgentsEmptyTitle,
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: AppColors.textPrimary),
),
const SizedBox(height: 10),
const Text(
'通过与 我智能体 对话,你可以招募各种智能体:\nOpenClaw 编程助手、运维机器人、数据分析师...',
style: TextStyle(fontSize: 14, color: AppColors.textSecondary, height: 1.6),
Text(
AppLocalizations.of(context).myAgentsEmptyDesc,
style: const TextStyle(fontSize: 14, color: AppColors.textSecondary, height: 1.6),
textAlign: TextAlign.center,
),
const SizedBox(height: 36),
_StepCard(step: '1', title: '点击下方机器人', desc: '打开与 我智能体 的对话窗口', icon: Icons.smart_toy_outlined, color: AppColors.primary),
_StepCard(step: '1', title: AppLocalizations.of(context).myAgentsStep1Title, desc: AppLocalizations.of(context).myAgentsStep1Desc, icon: Icons.smart_toy_outlined, color: AppColors.primary),
const SizedBox(height: 12),
_StepCard(step: '2', title: '描述你想要的智能体', desc: '例如:"帮我招募一个 OpenClaw 编程助手"', icon: Icons.record_voice_over_outlined, color: const Color(0xFF0EA5E9)),
_StepCard(step: '2', title: AppLocalizations.of(context).myAgentsStep2Title, desc: AppLocalizations.of(context).myAgentsStep2Desc, icon: Icons.record_voice_over_outlined, color: const Color(0xFF0EA5E9)),
const SizedBox(height: 12),
_StepCard(step: '3', title: '我智能体 自动部署', desc: '部署完成后出现在这里,通过 Telegram/WhatsApp 等渠道与它对话', icon: Icons.check_circle_outline, color: AppColors.success),
_StepCard(step: '3', title: AppLocalizations.of(context).myAgentsStep3Title, desc: AppLocalizations.of(context).myAgentsStep3Desc, icon: Icons.check_circle_outline, color: AppColors.success),
const SizedBox(height: 36),
const _TemplatesSection(),
const SizedBox(height: 100),
@ -180,11 +186,11 @@ class MyAgentsPage extends ConsumerWidget {
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Row(
children: [
_SummaryChip(label: '总计 ${instances.length}', color: AppColors.textMuted),
_SummaryChip(label: AppLocalizations.of(context).summaryTotal(instances.length), color: AppColors.textMuted),
const SizedBox(width: 8),
_SummaryChip(label: '运行中 $running', color: const Color(0xFF22C55E)),
_SummaryChip(label: AppLocalizations.of(context).summaryRunning(running), color: const Color(0xFF22C55E)),
const SizedBox(width: 8),
_SummaryChip(label: '已停止 ${instances.length - running}', color: const Color(0xFFF59E0B)),
_SummaryChip(label: AppLocalizations.of(context).summaryStopped(instances.length - running), color: const Color(0xFFF59E0B)),
],
),
),
@ -196,7 +202,12 @@ class MyAgentsPage extends ConsumerWidget {
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index.isOdd) return const SizedBox(height: 10);
return _InstanceCard(instance: instances[index ~/ 2]);
final inst = instances[index ~/ 2];
return _InstanceCard(
instance: inst,
onDismiss: () => _handleDismiss(context, ref, inst),
onRename: () => _handleRename(context, ref, inst),
);
},
childCount: instances.length * 2 - 1,
),
@ -208,6 +219,69 @@ class MyAgentsPage extends ConsumerWidget {
}
}
// ---------------------------------------------------------------------------
// Dismiss / Rename helpers (top-level functions for ConsumerWidget access)
// ---------------------------------------------------------------------------
Future<void> _handleDismiss(
BuildContext context, WidgetRef ref, AgentInstance instance) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => _DismissConfirmDialog(name: instance.name),
);
if (confirmed != true) return;
try {
final dio = ref.read(dioClientProvider);
await dio.delete('${ApiEndpoints.agentInstances}/${instance.id}');
ref.invalidate(myInstancesProvider);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).dismissSuccessMessage(instance.name))),
);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context).dismissErrorMessage(ErrorHandler.friendlyMessage(e))),
backgroundColor: AppColors.error,
),
);
}
}
}
Future<void> _handleRename(
BuildContext context, WidgetRef ref, AgentInstance instance) async {
final newName = await showDialog<String>(
context: context,
builder: (ctx) => _RenameDialog(currentName: instance.name),
);
if (newName == null || newName.trim().isEmpty) return;
try {
final dio = ref.read(dioClientProvider);
await dio.put(
'${ApiEndpoints.agentInstances}/${instance.id}/name',
data: {'name': newName.trim()},
);
ref.invalidate(myInstancesProvider);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).renameSuccessMessage)),
);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context).renameErrorMessage(ErrorHandler.friendlyMessage(e))),
backgroundColor: AppColors.error,
),
);
}
}
}
// ---------------------------------------------------------------------------
// Summary chip
// ---------------------------------------------------------------------------
@ -237,13 +311,52 @@ class _SummaryChip extends StatelessWidget {
class _InstanceCard extends StatelessWidget {
final AgentInstance instance;
final VoidCallback? onDismiss;
final VoidCallback? onRename;
const _InstanceCard({required this.instance});
const _InstanceCard({required this.instance, this.onDismiss, this.onRename});
void _showActions(BuildContext context) {
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (ctx) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 36,
height: 4,
margin: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: AppColors.textMuted.withOpacity(0.4),
borderRadius: BorderRadius.circular(2),
),
),
ListTile(
leading: const Icon(Icons.drive_file_rename_outline),
title: Text(AppLocalizations.of(ctx).renameButton),
onTap: () { Navigator.pop(ctx); onRename?.call(); },
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.person_remove_outlined, color: AppColors.error),
title: Text(AppLocalizations.of(ctx).dismissButton, style: const TextStyle(color: AppColors.error)),
onTap: () { Navigator.pop(ctx); onDismiss?.call(); },
),
const SizedBox(height: 8),
],
),
),
);
}
@override
Widget build(BuildContext context) {
final statusColor = _statusColors[instance.status] ?? AppColors.textMuted;
final statusLabel = _statusLabels[instance.status] ?? instance.status;
final statusLabel = _statusLabel(context, instance.status);
final statusIcon = _statusIcons[instance.status] ?? Icons.help_outline;
final timeLabel = DateFormatter.timeAgo(instance.createdAt);
final isDeploying = instance.status == 'deploying';
@ -310,6 +423,14 @@ class _InstanceCard extends StatelessWidget {
],
),
),
// More options
IconButton(
icon: const Icon(Icons.more_vert, size: 18),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
color: AppColors.textMuted,
onPressed: () => _showActions(context),
),
],
),
@ -433,7 +554,7 @@ class _TemplatesSection extends StatelessWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('热门模板(告诉 我智能体 你想要哪种)', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.textSecondary)),
Text(AppLocalizations.of(context).myAgentsTemplatesHeader, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.textSecondary)),
const SizedBox(height: 12),
GridView.count(
shrinkWrap: true,
@ -488,3 +609,95 @@ class _TemplateChip extends StatelessWidget {
);
}
}
// ---------------------------------------------------------------------------
// Dismiss confirm dialog
// ---------------------------------------------------------------------------
class _DismissConfirmDialog extends StatelessWidget {
final String name;
const _DismissConfirmDialog({required this.name});
@override
Widget build(BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: Row(
children: [
const Icon(Icons.warning_amber_rounded, color: AppColors.error),
const SizedBox(width: 8),
Text(AppLocalizations.of(context).dismissTitle),
],
),
content: Text(
AppLocalizations.of(context).dismissConfirmContent(name),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(AppLocalizations.of(context).cancelButton),
),
FilledButton(
style: FilledButton.styleFrom(backgroundColor: AppColors.error),
onPressed: () => Navigator.of(context).pop(true),
child: Text(AppLocalizations.of(context).dismissButton),
),
],
);
}
}
// ---------------------------------------------------------------------------
// Rename dialog
// ---------------------------------------------------------------------------
class _RenameDialog extends StatefulWidget {
final String currentName;
const _RenameDialog({required this.currentName});
@override
State<_RenameDialog> createState() => _RenameDialogState();
}
class _RenameDialogState extends State<_RenameDialog> {
late final TextEditingController _ctrl;
@override
void initState() {
super.initState();
_ctrl = TextEditingController(text: widget.currentName);
}
@override
void dispose() {
_ctrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: Text(AppLocalizations.of(context).renameTitle),
content: TextField(
controller: _ctrl,
autofocus: true,
decoration: InputDecoration(
hintText: AppLocalizations.of(context).renameHint,
border: const OutlineInputBorder(),
),
onSubmitted: (_) => Navigator.of(context).pop(_ctrl.text),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(null),
child: Text(AppLocalizations.of(context).cancelButton),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(_ctrl.text),
child: Text(AppLocalizations.of(context).confirmButton),
),
],
);
}
}

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:it0_app/l10n/app_localizations.dart';
import '../../data/in_site_notification.dart';
import '../../data/in_site_notification_repository.dart';
import '../providers/notification_providers.dart';
@ -72,7 +73,7 @@ class _NotificationInboxPageState
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('操作失败,请重试')),
SnackBar(content: Text(AppLocalizations.of(context).operationFailedError)),
);
}
}
@ -159,7 +160,7 @@ class _NotificationInboxPageState
if (item.linkUrl != null) ...[
const SizedBox(height: 16),
Text(
'链接:${item.linkUrl}',
'${AppLocalizations.of(ctx).linkLabel}${item.linkUrl}',
style: const TextStyle(
fontSize: 13, color: Colors.blue),
),
@ -180,12 +181,12 @@ class _NotificationInboxPageState
return Scaffold(
appBar: AppBar(
title: const Text('站内消息'),
title: Text(AppLocalizations.of(context).notificationInboxTitle),
actions: [
if (hasUnread)
TextButton(
onPressed: _markAllRead,
child: const Text('全部已读'),
child: Text(AppLocalizations.of(context).notificationMarkAllRead),
),
],
),
@ -199,24 +200,24 @@ class _NotificationInboxPageState
const Icon(Icons.error_outline,
size: 48, color: Colors.grey),
const SizedBox(height: 12),
Text('加载失败', style: TextStyle(color: Colors.grey[600])),
Text(AppLocalizations.of(context).notificationLoadingFailed, style: TextStyle(color: Colors.grey[600])),
const SizedBox(height: 8),
TextButton(
onPressed: _refresh, child: const Text('重试')),
onPressed: _refresh, child: Text(AppLocalizations.of(context).retryButton)),
],
),
),
data: (items) {
if (items.isEmpty) {
return const Center(
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.notifications_none,
const Icon(Icons.notifications_none,
size: 64, color: Colors.grey),
SizedBox(height: 12),
Text('暂无消息',
style: TextStyle(color: Colors.grey)),
const SizedBox(height: 12),
Text(AppLocalizations.of(context).noMessagesTitle,
style: const TextStyle(color: Colors.grey)),
],
),
);

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:it0_app/l10n/app_localizations.dart';
import '../../data/notification_channel.dart';
import '../../../../core/network/dio_client.dart';
@ -43,7 +44,7 @@ class _NotificationPreferencesPageState extends ConsumerState<NotificationPrefer
ref.invalidate(_channelPrefsProvider);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('通知偏好已保存'), duration: Duration(seconds: 2)),
SnackBar(content: Text(AppLocalizations.of(context).preferencesSavedMessage), duration: const Duration(seconds: 2)),
);
}
} catch (e) {
@ -64,14 +65,14 @@ class _NotificationPreferencesPageState extends ConsumerState<NotificationPrefer
return Scaffold(
appBar: AppBar(
title: const Text('通知偏好设置'),
title: Text(AppLocalizations.of(context).notificationPreferencesTitle),
actions: [
if (_pendingChanges.isNotEmpty)
TextButton(
onPressed: _saving ? null : _saveChanges,
child: _saving
? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2))
: const Text('保存'),
: Text(AppLocalizations.of(context).saveButton),
),
],
),
@ -81,18 +82,18 @@ class _NotificationPreferencesPageState extends ConsumerState<NotificationPrefer
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('加载失败', style: theme.textTheme.titleMedium),
Text(AppLocalizations.of(context).notificationLoadingFailed, style: theme.textTheme.titleMedium),
const SizedBox(height: 8),
TextButton(
onPressed: () => ref.invalidate(_channelPrefsProvider),
child: const Text('重试'),
child: Text(AppLocalizations.of(context).retryButton),
),
],
),
),
data: (prefs) {
if (prefs.isEmpty) {
return const Center(child: Text('暂无可配置的通知频道'));
return Center(child: Text(AppLocalizations.of(context).noNotificationChannels));
}
final mandatory = prefs.where((p) => p.isMandatory).toList();
@ -114,7 +115,7 @@ class _NotificationPreferencesPageState extends ConsumerState<NotificationPrefer
const SizedBox(width: 8),
Expanded(
child: Text(
'您可以选择接收哪些类型的通知。强制通知(如安全告警)无法关闭。',
AppLocalizations.of(context).notificationPreferencesInfo,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
),
@ -129,7 +130,7 @@ class _NotificationPreferencesPageState extends ConsumerState<NotificationPrefer
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
child: Text(
'重要通知(不可关闭)',
AppLocalizations.of(context).mandatoryNotificationsSection,
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.5),
letterSpacing: 0.5,
@ -148,7 +149,7 @@ class _NotificationPreferencesPageState extends ConsumerState<NotificationPrefer
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
child: Text(
'可选通知',
AppLocalizations.of(context).optionalNotificationsSection,
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.5),
letterSpacing: 0.5,
@ -179,7 +180,7 @@ class _NotificationPreferencesPageState extends ConsumerState<NotificationPrefer
width: 20, height: 20,
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
)
: const Text('保存偏好设置'),
: Text(AppLocalizations.of(context).savePreferencesButton),
),
),
],
@ -217,7 +218,7 @@ class _ChannelTile extends StatelessWidget {
borderRadius: BorderRadius.circular(4),
),
child: Text(
'必需',
AppLocalizations.of(context).requiredLabel,
style: theme.textTheme.labelSmall?.copyWith(
color: Colors.amber.shade800,
fontWeight: FontWeight.w600,

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:it0_app/l10n/app_localizations.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/updater/update_service.dart';
import '../../../auth/data/providers/auth_provider.dart';
@ -42,6 +43,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final settings = ref.watch(settingsProvider);
final profile = ref.watch(accountProfileProvider);
final isDark = Theme.of(context).brightness == Brightness.dark;
@ -53,7 +55,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
backgroundColor: AppColors.background,
appBar: AppBar(
backgroundColor: AppColors.background,
title: const Text(''),
title: Text(l10n.navProfile),
),
body: ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
@ -69,7 +71,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
_SettingsRow(
icon: Icons.workspace_premium_outlined,
iconBg: const Color(0xFF10B981),
title: '订阅套餐与用量',
title: l10n.profileSubscriptionLabel,
trailing: Text(
'Free',
style: TextStyle(color: subtitleColor, fontSize: 14),
@ -79,9 +81,9 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
_SettingsRow(
icon: Icons.card_giftcard_outlined,
iconBg: const Color(0xFF6366F1),
title: '邀请有礼',
title: l10n.profileReferralLabel,
trailing: Text(
'推荐赚积分',
l10n.profileReferralHint,
style: TextStyle(color: subtitleColor, fontSize: 14),
),
onTap: () => context.push('/referral'),
@ -90,7 +92,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
_SettingsRow(
icon: Icons.tune_outlined,
iconBg: const Color(0xFF8B5CF6),
title: '通知偏好设置',
title: l10n.notificationPreferencesTitle,
onTap: () => context.push('/notifications/preferences'),
),
],
@ -104,15 +106,15 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
_SettingsRow(
icon: Icons.palette_outlined,
iconBg: const Color(0xFF6366F1),
title: '外观主题',
title: l10n.appearanceThemeLabel,
trailing: _ThemeLabel(mode: settings.themeMode),
onTap: () => _showThemePicker(settings.themeMode),
),
_SettingsRow(
icon: Icons.language,
iconBg: const Color(0xFF3B82F6),
title: '语言',
trailing: Text('简体中文',
title: l10n.languageLabel,
trailing: Text(l10n.languageZh,
style: TextStyle(color: subtitleColor, fontSize: 14)),
),
],
@ -129,7 +131,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
if (false) _SettingsRow(
icon: Icons.psychology,
iconBg: const Color(0xFF7C3AED),
title: '对话引擎',
title: l10n.conversationEngineLabel,
trailing: Text(
settings.engineType == 'claude_agent_sdk'
? 'Agent SDK'
@ -141,9 +143,9 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
_SettingsRow(
icon: Icons.record_voice_over,
iconBg: const Color(0xFF0EA5E9),
title: '语音音色',
title: l10n.ttsVoiceLabel,
trailing: Text(
_voiceDisplayLabel(settings.ttsVoice),
_voiceDisplayLabel(context, settings.ttsVoice),
style: TextStyle(color: subtitleColor, fontSize: 14),
),
onTap: () => _showVoicePicker(settings.ttsVoice),
@ -151,9 +153,9 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
_SettingsRow(
icon: Icons.tune,
iconBg: const Color(0xFFF97316),
title: '语音风格',
title: l10n.ttsStyleLabel,
trailing: Text(
_styleDisplayName(settings.ttsStyle),
_styleDisplayName(context, settings.ttsStyle),
style: TextStyle(color: subtitleColor, fontSize: 14),
),
onTap: () => _showStylePicker(settings.ttsStyle),
@ -169,7 +171,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
_SettingsToggleRow(
icon: Icons.notifications_outlined,
iconBg: const Color(0xFFEF4444),
title: '推送通知',
title: l10n.pushNotificationsLabel,
value: settings.notificationsEnabled,
onChanged: (v) => ref
.read(settingsProvider.notifier)
@ -178,7 +180,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
_SettingsToggleRow(
icon: Icons.volume_up_outlined,
iconBg: const Color(0xFFF59E0B),
title: '提示音',
title: l10n.soundLabel,
value: settings.soundEnabled,
onChanged: settings.notificationsEnabled
? (v) =>
@ -196,7 +198,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
_SettingsRow(
icon: Icons.lock_outline,
iconBg: const Color(0xFF8B5CF6),
title: '修改密码',
title: l10n.changePasswordLabel,
onTap: _showChangePasswordSheet,
),
],
@ -210,7 +212,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
_SettingsRow(
icon: Icons.info_outline,
iconBg: const Color(0xFF64748B),
title: '版本',
title: l10n.versionLabel,
trailing: Text(
_appVersion.isNotEmpty ? _appVersion : 'v1.0.0',
style: TextStyle(color: subtitleColor, fontSize: 14),
@ -243,7 +245,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
_SettingsRow(
icon: Icons.business_outlined,
iconBg: const Color(0xFF0EA5E9),
title: '租户',
title: l10n.tenantLabel,
trailing: Text(settings.selectedTenantName!,
style: TextStyle(color: subtitleColor, fontSize: 14)),
),
@ -263,8 +265,8 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
child: const Text('退出登录',
style: TextStyle(fontSize: 16)),
child: Text(l10n.logoutButton,
style: const TextStyle(fontSize: 16)),
),
),
const SizedBox(height: 48),
@ -277,6 +279,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
Widget _buildProfileCard(
AccountProfile profile, Color cardColor, Color? subtitleColor) {
final l10n = AppLocalizations.of(context);
final initial = profile.displayName.isNotEmpty
? profile.displayName[0].toUpperCase()
: '?';
@ -323,7 +326,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
Text(
profile.displayName.isNotEmpty
? profile.displayName
: '加载中...',
: l10n.loadingLabel,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 18,
@ -350,6 +353,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
// Pickers
void _showThemePicker(ThemeMode current) {
final l10n = AppLocalizations.of(context);
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
@ -362,14 +366,14 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
const SizedBox(height: 12),
_handle(),
const SizedBox(height: 12),
Text('选择主题',
Text(l10n.selectThemeTitle,
style: Theme.of(ctx).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
)),
const SizedBox(height: 8),
_ThemeOption(
icon: Icons.dark_mode,
label: '深色模式',
label: l10n.darkModeLabel,
selected: current == ThemeMode.dark,
onTap: () {
ref
@ -380,7 +384,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
),
_ThemeOption(
icon: Icons.light_mode,
label: '浅色模式',
label: l10n.lightModeLabel,
selected: current == ThemeMode.light,
onTap: () {
ref
@ -391,7 +395,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
),
_ThemeOption(
icon: Icons.settings_brightness,
label: '跟随系统',
label: l10n.followSystemLabel,
selected: current == ThemeMode.system,
onTap: () {
ref
@ -407,19 +411,22 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
);
}
static const _voices = [
('coral', 'Coral', '女 · 温暖'),
('nova', 'Nova', '女 · 活泼'),
('sage', 'Sage', '女 · 知性'),
('shimmer', 'Shimmer', '女 · 柔和'),
('ash', 'Ash', '男 · 沉稳'),
('echo', 'Echo', '男 · 清朗'),
('onyx', 'Onyx', '男 · 低沉'),
('alloy', 'Alloy', '中性'),
List<(String, String, String)> _voiceList(BuildContext context) {
final l10n = AppLocalizations.of(context);
return [
('coral', 'Coral', l10n.voiceCoralDesc),
('nova', 'Nova', l10n.voiceNovaDesc),
('sage', 'Sage', l10n.voiceSageDesc),
('shimmer', 'Shimmer', l10n.voiceShimmerDesc),
('ash', 'Ash', l10n.voiceAshDesc),
('echo', 'Echo', l10n.voiceEchoDesc),
('onyx', 'Onyx', l10n.voiceOnyxDesc),
('alloy', 'Alloy', l10n.voiceAlloyDesc),
];
}
String _voiceDisplayLabel(String voice) {
for (final v in _voices) {
String _voiceDisplayLabel(BuildContext context, String voice) {
for (final v in _voiceList(context)) {
if (v.$1 == voice) return '${v.$2} · ${v.$3}';
}
return voice.isNotEmpty
@ -428,6 +435,8 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
}
void _showVoicePicker(String current) {
final l10n = AppLocalizations.of(context);
final voices = _voiceList(context);
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
@ -440,7 +449,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
const SizedBox(height: 12),
_handle(),
const SizedBox(height: 12),
Text('选择语音音色',
Text(l10n.selectVoiceTitle,
style: Theme.of(ctx).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
)),
@ -448,7 +457,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
Flexible(
child: ListView(
shrinkWrap: true,
children: _voices
children: voices
.map((v) => ListTile(
leading: Icon(Icons.record_voice_over,
color: current == v.$1
@ -492,6 +501,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
];
void _showEngineTypePicker(String current) {
final l10n = AppLocalizations.of(context);
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
@ -504,7 +514,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
children: [
_handle(),
const SizedBox(height: 12),
Text('选择对话引擎',
Text(l10n.selectEngineTitle,
style: Theme.of(ctx).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
)),
@ -542,25 +552,31 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
);
}
static const _stylePresets = [
('专业干练', '用专业、简洁、干练的语气说话,不拖泥带水。'),
('温柔耐心', '用温柔、耐心的语气说话,像一个贴心的朋友。'),
('轻松活泼', '用轻松、活泼的语气说话,带一点幽默感。'),
('严肃正式', '用严肃、正式的语气说话,像在正式会议中发言。'),
('科幻AI', '用科幻电影中AI的语气说话冷静、理性、略带未来感。'),
List<(String, String)> _stylePresetList(BuildContext context) {
final l10n = AppLocalizations.of(context);
return [
(l10n.styleProfessionalName, '用专业、简洁、干练的语气说话,不拖泥带水。'),
(l10n.styleGentleName, '用温柔、耐心的语气说话,像一个贴心的朋友。'),
(l10n.styleRelaxedName, '用轻松、活泼的语气说话,带一点幽默感。'),
(l10n.styleFormalName, '用严肃、正式的语气说话,像在正式会议中发言。'),
(l10n.styleScifiName, '用科幻电影中AI的语气说话冷静、理性、略带未来感。'),
];
}
String _styleDisplayName(String style) {
if (style.isEmpty) return '默认';
for (final p in _stylePresets) {
String _styleDisplayName(BuildContext context, String style) {
final l10n = AppLocalizations.of(context);
if (style.isEmpty) return l10n.defaultStyleLabel;
for (final p in _stylePresetList(context)) {
if (p.$2 == style) return p.$1;
}
return style.length > 6 ? '${style.substring(0, 6)}...' : style;
}
void _showStylePicker(String current) {
final l10n = AppLocalizations.of(context);
final stylePresets = _stylePresetList(context);
final controller = TextEditingController(
text: _stylePresets.any((p) => p.$2 == current) ? '' : current,
text: stylePresets.any((p) => p.$2 == current) ? '' : current,
);
showModalBottomSheet(
context: context,
@ -576,7 +592,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
children: [
_handle(),
const SizedBox(height: 12),
Text('选择语音风格',
Text(l10n.selectStyleTitle,
style: Theme.of(ctx).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
)),
@ -584,7 +600,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
Wrap(
spacing: 8,
runSpacing: 8,
children: _stylePresets
children: stylePresets
.map((p) => ChoiceChip(
label: Text(p.$1),
selected: current == p.$2,
@ -601,8 +617,8 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
TextField(
controller: controller,
decoration: InputDecoration(
labelText: '自定义风格',
hintText: '例如:用东北话说话,幽默风趣',
labelText: l10n.customStyleLabel,
hintText: l10n.customStyleHint,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12)),
),
@ -619,7 +635,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
.setTtsStyle('');
Navigator.pop(ctx);
},
child: const Text('恢复默认'),
child: Text(l10n.resetToDefaultButton),
),
),
const SizedBox(width: 12),
@ -646,18 +662,20 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
}
void _showEditNameDialog(String currentName) {
final l10n = AppLocalizations.of(context);
final controller = TextEditingController(text: currentName);
showDialog(
context: context,
builder: (ctx) => AlertDialog(
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: const Text('修改显示名称'),
title: Text(l10n.editNameDialogTitle),
content: TextField(
controller: controller,
autofocus: true,
decoration: InputDecoration(
labelText: '显示名称',
labelText: l10n.displayNameLabel,
hintText: l10n.displayNameHint,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12)),
),
@ -665,7 +683,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('取消'),
child: Text(l10n.cancelButton),
),
FilledButton(
onPressed: () async {
@ -678,11 +696,11 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(success ? '名称已更新' : '更新失败')),
content: Text(success ? l10n.nameUpdatedMessage : l10n.updateFailedMessage)),
);
}
},
child: const Text('保存'),
child: Text(l10n.saveButton),
),
],
),
@ -690,6 +708,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
}
void _showChangePasswordSheet() {
final l10n = AppLocalizations.of(context);
final currentCtrl = TextEditingController();
final newCtrl = TextEditingController();
final confirmCtrl = TextEditingController();
@ -709,7 +728,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
children: [
_handle(),
const SizedBox(height: 12),
Text('修改密码',
Text(l10n.changePasswordTitle,
style: Theme.of(ctx)
.textTheme
.titleLarge
@ -719,7 +738,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
controller: currentCtrl,
obscureText: true,
decoration: InputDecoration(
labelText: '当前密码',
labelText: l10n.currentPasswordLabel,
prefixIcon: const Icon(Icons.lock_outline),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12)),
@ -730,7 +749,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
controller: newCtrl,
obscureText: true,
decoration: InputDecoration(
labelText: '新密码',
labelText: l10n.newPasswordLabel,
prefixIcon: const Icon(Icons.lock_reset),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12)),
@ -741,7 +760,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
controller: confirmCtrl,
obscureText: true,
decoration: InputDecoration(
labelText: '确认新密码',
labelText: l10n.confirmPasswordLabel,
prefixIcon: const Icon(Icons.lock_reset),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12)),
@ -778,13 +797,13 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(result.success
? '密码已修改'
: result.message ?? '修改失败'),
? l10n.passwordChangedMessage
: result.message ?? l10n.changeFailedMessage),
),
);
}
},
child: const Text('确认修改', style: TextStyle(fontSize: 16)),
child: Text(l10n.confirmChangeButton, style: const TextStyle(fontSize: 16)),
),
],
),
@ -793,23 +812,24 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
}
void _confirmLogout() async {
final l10n = AppLocalizations.of(context);
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: const Text('退出登录'),
content: const Text('确定要退出登录吗?'),
title: Text(l10n.logoutDialogTitle),
content: Text(l10n.logoutConfirmMessage),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('取消'),
child: Text(l10n.cancelButton),
),
FilledButton(
style:
FilledButton.styleFrom(backgroundColor: AppColors.error),
onPressed: () => Navigator.of(ctx).pop(true),
child: const Text('退出'),
child: Text(l10n.logoutConfirmButton),
),
],
),
@ -953,10 +973,11 @@ class _ThemeLabel extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final label = switch (mode) {
ThemeMode.dark => '深色',
ThemeMode.light => '浅色',
ThemeMode.system => '跟随系统',
ThemeMode.dark => l10n.darkModeLabel,
ThemeMode.light => l10n.lightModeLabel,
ThemeMode.system => l10n.followSystemLabel,
};
return Text(label,
style: TextStyle(
@ -971,6 +992,7 @@ class _InSiteMessageRow extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final unreadAsync = ref.watch(inSiteUnreadCountProvider);
final unread = unreadAsync.valueOrNull ?? 0;
@ -985,7 +1007,7 @@ class _InSiteMessageRow extends ConsumerWidget {
child: const Icon(Icons.notifications_outlined,
color: Colors.white, size: 18),
),
title: const Text('站内消息'),
title: Text(l10n.profileInSiteMessagesLabel),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
@ -1005,7 +1027,7 @@ class _InSiteMessageRow extends ConsumerWidget {
),
)
else
Text('查看消息',
Text(l10n.profileViewMessagesLabel,
style: TextStyle(color: subtitleColor, fontSize: 14)),
const SizedBox(width: 4),
Icon(Icons.chevron_right,

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:it0_app/l10n/app_localizations.dart';
import '../../../../core/theme/app_colors.dart';
import '../providers/referral_providers.dart';
import '../../domain/models/referral_info.dart';
@ -10,6 +11,7 @@ class ReferralScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final infoAsync = ref.watch(referralInfoProvider);
final isDark = Theme.of(context).brightness == Brightness.dark;
final cardColor = isDark ? AppColors.surface : Colors.white;
@ -18,7 +20,7 @@ class ReferralScreen extends ConsumerWidget {
backgroundColor: AppColors.background,
appBar: AppBar(
backgroundColor: AppColors.background,
title: const Text('邀请有礼'),
title: Text(l10n.referralScreenTitle),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
@ -47,6 +49,7 @@ class _ReferralBody extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
return ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
children: [
@ -66,13 +69,13 @@ class _ReferralBody extends ConsumerWidget {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'推荐记录',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16),
Text(
l10n.referralRecordsSection,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16),
),
TextButton(
onPressed: () => _showReferralList(context),
child: const Text('查看全部 >'),
child: Text(l10n.viewAllReferralsLink),
),
],
),
@ -83,13 +86,13 @@ class _ReferralBody extends ConsumerWidget {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'待领积分',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16),
Text(
l10n.pendingRewardsSection,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16),
),
TextButton(
onPressed: () => _showRewardList(context),
child: const Text('查看全部 >'),
child: Text(l10n.viewAllRewardsLink),
),
],
),
@ -124,6 +127,7 @@ class _ReferralCodeCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Card(
color: cardColor,
elevation: 0,
@ -133,9 +137,9 @@ class _ReferralCodeCard extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'你的推荐码',
style: TextStyle(color: Colors.grey, fontSize: 13),
Text(
l10n.yourReferralCodeLabel,
style: const TextStyle(color: Colors.grey, fontSize: 13),
),
const SizedBox(height: 8),
Row(
@ -153,7 +157,7 @@ class _ReferralCodeCard extends StatelessWidget {
),
IconButton(
icon: const Icon(Icons.copy, color: AppColors.primary),
tooltip: '复制推荐码',
tooltip: l10n.copyReferralCodeTooltip,
onPressed: () => _copy(context, info.referralCode),
),
],
@ -164,7 +168,7 @@ class _ReferralCodeCard extends StatelessWidget {
Expanded(
child: OutlinedButton.icon(
icon: const Icon(Icons.link, size: 18),
label: const Text('复制邀请链接'),
label: Text(l10n.copyInviteLinkButton),
onPressed: () => _copy(context, info.shareUrl),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primary,
@ -178,7 +182,7 @@ class _ReferralCodeCard extends StatelessWidget {
Expanded(
child: FilledButton.icon(
icon: const Icon(Icons.share, size: 18),
label: const Text('分享'),
label: Text(l10n.shareButton),
onPressed: () => _share(context, info),
style: FilledButton.styleFrom(
backgroundColor: AppColors.primary,
@ -196,9 +200,10 @@ class _ReferralCodeCard extends StatelessWidget {
}
void _copy(BuildContext context, String text) {
final l10n = AppLocalizations.of(context);
Clipboard.setData(ClipboardData(text: text));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('已复制到剪贴板'), duration: Duration(seconds: 2)),
SnackBar(content: Text(l10n.copiedToClipboard), duration: const Duration(seconds: 2)),
);
}
@ -220,27 +225,28 @@ class _StatsRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Row(
children: [
_StatCard(
cardColor: cardColor,
label: '已推荐',
label: l10n.referredLabel,
value: '${info.directCount}',
unit: '',
unit: l10n.peopleUnit,
color: const Color(0xFF6366F1),
),
const SizedBox(width: 10),
_StatCard(
cardColor: cardColor,
label: '已激活',
label: l10n.activatedLabel,
value: '${info.activeCount}',
unit: '',
unit: l10n.peopleUnit,
color: const Color(0xFF10B981),
),
const SizedBox(width: 10),
_StatCard(
cardColor: cardColor,
label: '待领积分',
label: l10n.pendingCreditsLabel,
value: info.pendingCreditFormatted,
unit: '',
color: const Color(0xFFF59E0B),
@ -317,6 +323,7 @@ class _RewardRulesCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Card(
color: cardColor,
elevation: 0,
@ -326,13 +333,13 @@ class _RewardRulesCard extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
Row(
children: [
Icon(Icons.card_giftcard, color: Color(0xFFF59E0B), size: 20),
SizedBox(width: 6),
const Icon(Icons.card_giftcard, color: Color(0xFFF59E0B), size: 20),
const SizedBox(width: 6),
Text(
'奖励规则',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 15),
l10n.rewardRulesTitle,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 15),
),
],
),
@ -397,6 +404,7 @@ class _ReferralPreviewList extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final async = ref.watch(referralListProvider);
return async.when(
loading: () => const SizedBox(
@ -411,12 +419,12 @@ class _ReferralPreviewList extends ConsumerWidget {
elevation: 0,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: const Padding(
padding: EdgeInsets.all(20),
child: Padding(
padding: const EdgeInsets.all(20),
child: Center(
child: Text(
'暂无推荐记录,分享推荐码邀请好友吧',
style: TextStyle(color: Colors.grey, fontSize: 13),
l10n.noReferralsMessage,
style: const TextStyle(color: Colors.grey, fontSize: 13),
),
),
),
@ -444,16 +452,17 @@ class _ReferralTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final statusColor = item.isActive
? const Color(0xFF10B981)
: item.status == 'EXPIRED'
? Colors.grey
: const Color(0xFFF59E0B);
final statusLabel = switch (item.status) {
'PENDING' => '待付款',
'ACTIVE' => '已激活',
'REWARDED' => '已奖励',
'EXPIRED' => '已过期',
'PENDING' => l10n.pendingPaymentStatus,
'ACTIVE' => l10n.activeStatus,
'REWARDED' => l10n.rewardedStatus,
'EXPIRED' => l10n.expiredStatus,
_ => item.status,
};
@ -473,7 +482,7 @@ class _ReferralTile extends StatelessWidget {
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
subtitle: Text(
'注册于 ${_formatDate(item.registeredAt)}',
'${l10n.registeredAt} ${_formatDate(item.registeredAt)}',
style: const TextStyle(fontSize: 12),
),
trailing: Container(
@ -501,6 +510,7 @@ class _RewardPreviewList extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final async = ref.watch(pendingRewardsProvider);
return async.when(
loading: () => const SizedBox(
@ -513,12 +523,12 @@ class _RewardPreviewList extends ConsumerWidget {
elevation: 0,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: const Padding(
padding: EdgeInsets.all(20),
child: Padding(
padding: const EdgeInsets.all(20),
child: Center(
child: Text(
'暂无待领积分',
style: TextStyle(color: Colors.grey, fontSize: 13),
l10n.noPendingRewardsMessage,
style: const TextStyle(color: Colors.grey, fontSize: 13),
),
),
),
@ -548,6 +558,7 @@ class _RewardTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return ListTile(
leading: CircleAvatar(
backgroundColor: const Color(0xFFF59E0B).withAlpha(30),
@ -569,9 +580,9 @@ class _RewardTile extends StatelessWidget {
color: const Color(0xFFF59E0B).withAlpha(20),
borderRadius: BorderRadius.circular(20),
),
child: const Text(
'待抵扣',
style: TextStyle(
child: Text(
l10n.pendingDeductionStatus,
style: const TextStyle(
fontSize: 12,
color: Color(0xFFF59E0B),
fontWeight: FontWeight.w500),
@ -588,14 +599,15 @@ class _ReferralListPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final async = ref.watch(referralListProvider);
return Scaffold(
appBar: AppBar(title: const Text('推荐记录')),
appBar: AppBar(title: Text(l10n.referralRecordsSection)),
body: async.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('加载失败: $e')),
data: (result) => result.items.isEmpty
? const Center(child: Text('暂无推荐记录'))
? Center(child: Text(l10n.noReferralsMessage))
: ListView.separated(
itemCount: result.items.length,
separatorBuilder: (_, __) => const Divider(height: 1),

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:it0_app/l10n/app_localizations.dart';
import '../../../../core/config/api_endpoints.dart';
import '../../../../core/network/dio_client.dart';
import '../../../../core/theme/app_colors.dart';
@ -48,7 +49,7 @@ class _ServersPageState extends ConsumerState<ServersPage> {
return Scaffold(
appBar: AppBar(
title: const Text('服务器'),
title: Text(AppLocalizations.of(context).serversPageTitle),
),
body: RefreshIndicator(
onRefresh: () async => ref.invalidate(serversProvider),
@ -62,10 +63,10 @@ class _ServersPageState extends ConsumerState<ServersPage> {
data: (servers) {
final filtered = _filterServers(servers);
if (filtered.isEmpty) {
return const EmptyState(
return EmptyState(
icon: Icons.dns_outlined,
title: '未找到服务器',
subtitle: '没有匹配当前筛选条件的服务器',
title: AppLocalizations.of(context).noServersTitle,
subtitle: AppLocalizations.of(context).noServersFiltered,
);
}
return ListView.builder(
@ -106,7 +107,7 @@ class _ServersPageState extends ConsumerState<ServersPage> {
return Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
label: Text(env == 'all' ? '全部' : env),
label: Text(env == 'all' ? AppLocalizations.of(context).allEnvironments : env),
selected: selected,
onSelected: (_) => setState(() => _envFilter = env),
selectedColor: _envColor(env).withOpacity(0.25),
@ -142,7 +143,7 @@ class _ServersPageState extends ConsumerState<ServersPage> {
void _showServerDetails(
BuildContext context, Map<String, dynamic> server) {
final hostname =
server['hostname'] as String? ?? server['name'] as String? ?? '未知';
server['hostname'] as String? ?? server['name'] as String? ?? AppLocalizations.of(context).unknownLabel;
final ip = server['ip_address'] as String? ??
server['ipAddress'] as String? ??
server['ip'] as String? ??
@ -222,17 +223,17 @@ class _ServersPageState extends ConsumerState<ServersPage> {
const SizedBox(height: 20),
// Detail rows
_DetailRow(label: 'IP 地址', value: ip),
if (os.isNotEmpty) _DetailRow(label: '操作系统', value: os),
_DetailRow(label: AppLocalizations.of(context).ipAddressLabel, value: ip),
if (os.isNotEmpty) _DetailRow(label: AppLocalizations.of(context).osLabel, value: os),
if (cpu.isNotEmpty) _DetailRow(label: 'CPU', value: cpu),
if (memory.isNotEmpty)
_DetailRow(label: '内存', value: memory),
_DetailRow(label: AppLocalizations.of(context).memoryLabel, value: memory),
if (region.isNotEmpty)
_DetailRow(label: '区域', value: region),
_DetailRow(label: AppLocalizations.of(context).regionLabel, value: region),
if (provider.isNotEmpty)
_DetailRow(label: '云厂商', value: provider),
_DetailRow(label: AppLocalizations.of(context).cloudProviderLabel, value: provider),
if (createdAt.isNotEmpty)
_DetailRow(label: '创建时间', value: createdAt),
_DetailRow(label: AppLocalizations.of(context).createdAtLabel, value: createdAt),
],
),
),
@ -269,7 +270,7 @@ class _ServerCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final hostname =
server['hostname'] as String? ?? server['name'] as String? ?? '未知';
server['hostname'] as String? ?? server['name'] as String? ?? AppLocalizations.of(context).unknownLabel;
final ip = server['ip_address'] as String? ??
server['ipAddress'] as String? ??
server['ip'] as String? ??

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:it0_app/l10n/app_localizations.dart';
import 'package:package_info_plus/package_info_plus.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/updater/update_service.dart';
@ -44,7 +45,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
final subtitleColor = isDark ? AppColors.textSecondary : Colors.grey[600];
return Scaffold(
appBar: AppBar(title: const Text('设置')),
appBar: AppBar(title: Text(AppLocalizations.of(context).settingsPageTitle)),
body: ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
children: [
@ -59,16 +60,19 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
_SettingsRow(
icon: Icons.palette_outlined,
iconBg: const Color(0xFF6366F1),
title: '外观主题',
title: AppLocalizations.of(context).appearanceThemeLabel,
trailing: _ThemeLabel(mode: settings.themeMode),
onTap: () => _showThemePicker(settings.themeMode),
),
_SettingsRow(
icon: Icons.language,
iconBg: const Color(0xFF3B82F6),
title: '语言',
trailing: Text('简体中文',
style: TextStyle(color: subtitleColor, fontSize: 14)),
title: AppLocalizations.of(context).languageLabel,
trailing: Text(
_languageDisplayLabel(settings.language),
style: TextStyle(color: subtitleColor, fontSize: 14),
),
onTap: () => _showLanguagePicker(settings.language),
),
],
),
@ -81,7 +85,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
_SettingsToggleRow(
icon: Icons.notifications_outlined,
iconBg: const Color(0xFFEF4444),
title: '推送通知',
title: AppLocalizations.of(context).pushNotificationsLabel,
value: settings.notificationsEnabled,
onChanged: (v) =>
ref.read(settingsProvider.notifier).setNotificationsEnabled(v),
@ -89,7 +93,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
_SettingsToggleRow(
icon: Icons.volume_up_outlined,
iconBg: const Color(0xFFF59E0B),
title: '提示音',
title: AppLocalizations.of(context).soundLabel,
value: settings.soundEnabled,
onChanged: settings.notificationsEnabled
? (v) =>
@ -99,7 +103,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
_SettingsToggleRow(
icon: Icons.vibration,
iconBg: const Color(0xFF22C55E),
title: '震动反馈',
title: AppLocalizations.of(context).hapticFeedbackLabel,
value: settings.hapticFeedback,
onChanged: settings.notificationsEnabled
? (v) =>
@ -117,7 +121,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
_SettingsRow(
icon: Icons.psychology,
iconBg: const Color(0xFF7C3AED),
title: '对话引擎',
title: AppLocalizations.of(context).conversationEngineLabel,
trailing: Text(
settings.engineType == 'claude_agent_sdk' ? 'Agent SDK' : 'Claude API',
style: TextStyle(color: subtitleColor, fontSize: 14),
@ -127,7 +131,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
_SettingsRow(
icon: Icons.record_voice_over,
iconBg: const Color(0xFF0EA5E9),
title: '语音音色',
title: AppLocalizations.of(context).ttsVoiceLabel,
trailing: Text(
_voiceDisplayLabel(settings.ttsVoice),
style: TextStyle(color: subtitleColor, fontSize: 14),
@ -137,7 +141,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
_SettingsRow(
icon: Icons.tune,
iconBg: const Color(0xFFF97316),
title: '语音风格',
title: AppLocalizations.of(context).ttsStyleLabel,
trailing: Text(
_styleDisplayName(settings.ttsStyle),
style: TextStyle(color: subtitleColor, fontSize: 14),
@ -155,7 +159,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
_SettingsRow(
icon: Icons.credit_card_outlined,
iconBg: const Color(0xFF10B981),
title: '订阅与用量',
title: AppLocalizations.of(context).subscriptionLabel,
onTap: () => context.push('/settings/billing'),
),
],
@ -169,7 +173,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
_SettingsRow(
icon: Icons.lock_outline,
iconBg: const Color(0xFF8B5CF6),
title: '修改密码',
title: AppLocalizations.of(context).changePasswordLabel,
onTap: _showChangePasswordSheet,
),
],
@ -183,7 +187,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
_SettingsRow(
icon: Icons.info_outline,
iconBg: const Color(0xFF64748B),
title: '版本',
title: AppLocalizations.of(context).versionLabel,
trailing: Text(
_appVersion.isNotEmpty ? _appVersion : 'v1.0.0',
style: TextStyle(color: subtitleColor, fontSize: 14)),
@ -191,14 +195,14 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
_SettingsRow(
icon: Icons.system_update_outlined,
iconBg: const Color(0xFF22C55E),
title: '检查更新',
title: AppLocalizations.of(context).checkUpdateLabel,
onTap: () => UpdateService().manualCheckUpdate(context),
),
if (settings.selectedTenantName != null)
_SettingsRow(
icon: Icons.business_outlined,
iconBg: const Color(0xFF0EA5E9),
title: '租户',
title: AppLocalizations.of(context).tenantLabel,
trailing: Text(settings.selectedTenantName!,
style: TextStyle(color: subtitleColor, fontSize: 14)),
),
@ -237,7 +241,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
child: const Text('退出登录', style: TextStyle(fontSize: 16)),
child: Text(AppLocalizations.of(context).logoutButton, style: const TextStyle(fontSize: 16)),
),
),
const SizedBox(height: 48),
@ -319,6 +323,61 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
);
}
// ---- Language Picker ------------------------------------------------------
String _languageDisplayLabel(String lang) => switch (lang) {
'zh_TW' => '繁體中文',
'en' => 'English',
_ => '简体中文',
};
void _showLanguagePicker(String current) {
const options = [
('zh', '简体中文'),
('zh_TW', '繁體中文'),
('en', 'English'),
];
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (ctx) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 36,
height: 4,
margin: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: Colors.grey.withOpacity(0.4),
borderRadius: BorderRadius.circular(2),
),
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Text('选择语言 / 語言 / Language',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 15)),
),
const Divider(height: 1),
...options.map((o) => ListTile(
title: Text(o.$2),
trailing: o.$1 == current
? const Icon(Icons.check, color: Colors.blue)
: null,
onTap: () {
Navigator.pop(ctx);
ref.read(settingsProvider.notifier).setLanguage(o.$1);
},
)),
const SizedBox(height: 8),
],
),
),
);
}
// ---- Theme Picker ---------------------------------------------------------
void _showThemePicker(ThemeMode current) {
@ -342,14 +401,14 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
),
),
const SizedBox(height: 16),
Text('选择主题',
Text(AppLocalizations.of(context).selectThemeTitle,
style: Theme.of(ctx).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
)),
const SizedBox(height: 8),
_ThemeOption(
icon: Icons.dark_mode,
label: '深色模式',
label: AppLocalizations.of(context).darkModeLabel,
selected: current == ThemeMode.dark,
onTap: () {
ref.read(settingsProvider.notifier).setThemeMode(ThemeMode.dark);
@ -358,7 +417,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
),
_ThemeOption(
icon: Icons.light_mode,
label: '浅色模式',
label: AppLocalizations.of(context).lightModeLabel,
selected: current == ThemeMode.light,
onTap: () {
ref.read(settingsProvider.notifier).setThemeMode(ThemeMode.light);
@ -367,7 +426,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
),
_ThemeOption(
icon: Icons.settings_brightness,
label: '跟随系统',
label: AppLocalizations.of(context).followSystemLabel,
selected: current == ThemeMode.system,
onTap: () {
ref.read(settingsProvider.notifier).setThemeMode(ThemeMode.system);
@ -384,26 +443,30 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
// ---- Voice Picker ----------------------------------------------------------
static const _voices = [
('coral', 'Coral', '女 · 温暖'),
('nova', 'Nova', '女 · 活泼'),
('sage', 'Sage', '女 · 知性'),
('shimmer', 'Shimmer', '女 · 柔和'),
('marin', 'Marin', '女 · 清澈'),
('ash', 'Ash', '男 · 沉稳'),
('echo', 'Echo', '男 · 清朗'),
('onyx', 'Onyx', '男 · 低沉'),
('verse', 'Verse', '男 · 磁性'),
('ballad', 'Ballad', '男 · 浑厚'),
('cedar', 'Cedar', '男 · 自然'),
('alloy', 'Alloy', '中性'),
('fable', 'Fable', '中性 · 叙事'),
List<(String, String, String)> _voices(BuildContext context) {
final l = AppLocalizations.of(context);
return [
('coral', 'Coral', l.voiceCoralDesc),
('nova', 'Nova', l.voiceNovaDesc),
('sage', 'Sage', l.voiceSageDesc),
('shimmer', 'Shimmer', l.voiceShimmerDesc),
('marin', 'Marin', l.voiceMarinDesc),
('ash', 'Ash', l.voiceAshDesc),
('echo', 'Echo', l.voiceEchoDesc),
('onyx', 'Onyx', l.voiceOnyxDesc),
('verse', 'Verse', l.voiceVerseDesc),
('ballad', 'Ballad', l.voiceBalladDesc),
('cedar', 'Cedar', l.voiceCedarDesc),
('alloy', 'Alloy', l.voiceAlloyDesc),
('fable', 'Fable', l.voiceFableDesc),
];
}
void _showEngineTypePicker(String current) {
final l = AppLocalizations.of(context);
final engines = [
('claude_agent_sdk', 'Agent SDK', '支持工具审批、技能注入、会话恢复'),
('claude_api', 'Claude API', '直连 API响应更快'),
('claude_agent_sdk', 'Agent SDK', l.agentSdkDesc),
('claude_api', 'Claude API', l.claudeApiDesc),
];
showModalBottomSheet(
context: context,
@ -424,7 +487,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
),
),
const SizedBox(height: 16),
Text('选择对话引擎',
Text(AppLocalizations.of(context).selectEngineTitle,
style: Theme.of(ctx).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
)),
@ -475,7 +538,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
),
),
const SizedBox(height: 16),
Text('选择语音音色',
Text(AppLocalizations.of(context).selectVoiceTitle,
style: Theme.of(ctx).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
)),
@ -483,7 +546,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
Flexible(
child: ListView(
shrinkWrap: true,
children: _voices
children: _voices(context)
.map((v) => ListTile(
leading: Icon(
Icons.record_voice_over,
@ -532,23 +595,26 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
// ---- Style Picker ---------------------------------------------------------
String _voiceDisplayLabel(String voice) {
for (final v in _voices) {
for (final v in _voices(context)) {
if (v.$1 == voice) return '${v.$2} · ${v.$3}';
}
return voice[0].toUpperCase() + voice.substring(1);
}
static const _stylePresets = [
('专业干练', '用专业、简洁、干练的语气说话,不拖泥带水。'),
('温柔耐心', '用温柔、耐心的语气说话,像一个贴心的朋友。'),
('轻松活泼', '用轻松、活泼的语气说话,带一点幽默感。'),
('严肃正式', '用严肃、正式的语气说话,像在正式会议中发言。'),
('科幻AI', '用科幻电影中AI的语气说话冷静、理性、略带未来感。'),
List<(String, String)> _stylePresets(BuildContext context) {
final l = AppLocalizations.of(context);
return [
(l.styleProfessionalName, '用专业、简洁、干练的语气说话,不拖泥带水。'),
(l.styleGentleName, '用温柔、耐心的语气说话,像一个贴心的朋友。'),
(l.styleRelaxedName, '用轻松、活泼的语气说话,带一点幽默感。'),
(l.styleFormalName, '用严肃、正式的语气说话,像在正式会议中发言。'),
(l.styleScifiName, '用科幻电影中AI的语气说话冷静、理性、略带未来感。'),
];
}
String _styleDisplayName(String style) {
if (style.isEmpty) return '默认';
for (final p in _stylePresets) {
if (style.isEmpty) return AppLocalizations.of(context).defaultStyleLabel;
for (final p in _stylePresets(context)) {
if (p.$2 == style) return p.$1;
}
return style.length > 6 ? '${style.substring(0, 6)}...' : style;
@ -556,7 +622,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
void _showStylePicker(String current) {
final controller = TextEditingController(
text: _stylePresets.any((p) => p.$2 == current) ? '' : current,
text: _stylePresets(context).any((p) => p.$2 == current) ? '' : current,
);
showModalBottomSheet(
@ -581,7 +647,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
),
),
const SizedBox(height: 16),
Text('选择语音风格',
Text(AppLocalizations.of(context).selectStyleTitle,
style: Theme.of(ctx).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
)),
@ -589,7 +655,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
Wrap(
spacing: 8,
runSpacing: 8,
children: _stylePresets
children: _stylePresets(context)
.map((p) => ChoiceChip(
label: Text(p.$1),
selected: current == p.$2,
@ -606,8 +672,8 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
TextField(
controller: controller,
decoration: InputDecoration(
labelText: '自定义风格',
hintText: '例如:用东北话说话,幽默风趣',
labelText: AppLocalizations.of(context).customStyleLabel,
hintText: AppLocalizations.of(context).customStyleHint,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12)),
),
@ -622,7 +688,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
ref.read(settingsProvider.notifier).setTtsStyle('');
Navigator.pop(ctx);
},
child: const Text('恢复默认'),
child: Text(AppLocalizations.of(context).resetToDefaultButton),
),
),
const SizedBox(width: 12),
@ -637,7 +703,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
}
Navigator.pop(ctx);
},
child: const Text('确认'),
child: Text(AppLocalizations.of(context).confirmButton),
),
),
],
@ -657,13 +723,13 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
context: context,
builder: (ctx) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: const Text('修改显示名称'),
title: Text(AppLocalizations.of(context).editNameDialogTitle),
content: TextField(
controller: controller,
autofocus: true,
decoration: InputDecoration(
labelText: '显示名称',
hintText: '输入新的显示名称',
labelText: AppLocalizations.of(context).displayNameLabel,
hintText: AppLocalizations.of(context).displayNameHint,
border:
OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
),
@ -671,7 +737,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('取消'),
child: Text(AppLocalizations.of(context).cancelButton),
),
FilledButton(
onPressed: () async {
@ -683,11 +749,11 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
.updateDisplayName(name);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(success ? '名称已更新' : '更新失败')),
SnackBar(content: Text(success ? AppLocalizations.of(context).nameUpdatedMessage : AppLocalizations.of(context).updateFailedMessage)),
);
}
},
child: const Text('保存'),
child: Text(AppLocalizations.of(context).saveButton),
),
],
),
@ -725,7 +791,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
),
),
const SizedBox(height: 16),
Text('修改密码',
Text(AppLocalizations.of(context).changePasswordTitle,
style: Theme.of(ctx)
.textTheme
.titleLarge
@ -735,7 +801,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
controller: currentCtrl,
obscureText: true,
decoration: InputDecoration(
labelText: '当前密码',
labelText: AppLocalizations.of(context).currentPasswordLabel,
prefixIcon: const Icon(Icons.lock_outline),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12)),
@ -746,7 +812,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
controller: newCtrl,
obscureText: true,
decoration: InputDecoration(
labelText: '新密码',
labelText: AppLocalizations.of(context).newPasswordLabel,
prefixIcon: const Icon(Icons.lock_reset),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12)),
@ -757,7 +823,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
controller: confirmCtrl,
obscureText: true,
decoration: InputDecoration(
labelText: '确认新密码',
labelText: AppLocalizations.of(context).confirmPasswordLabel,
prefixIcon: const Icon(Icons.lock_reset),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12)),
@ -794,13 +860,13 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(result.success
? '密码已修改'
: result.message ?? '修改失败'),
? AppLocalizations.of(context).passwordChangedMessage
: result.message ?? AppLocalizations.of(context).changeFailedMessage),
),
);
}
},
child: const Text('确认修改', style: TextStyle(fontSize: 16)),
child: Text(AppLocalizations.of(context).confirmChangeButton, style: const TextStyle(fontSize: 16)),
),
],
),
@ -815,17 +881,17 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
context: context,
builder: (ctx) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: const Text('退出登录'),
content: const Text('确定要退出登录吗?'),
title: Text(AppLocalizations.of(context).logoutDialogTitle),
content: Text(AppLocalizations.of(context).logoutConfirmMessage),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('取消'),
child: Text(AppLocalizations.of(context).cancelButton),
),
FilledButton(
style: FilledButton.styleFrom(backgroundColor: AppColors.error),
onPressed: () => Navigator.of(ctx).pop(true),
child: const Text('退出'),
child: Text(AppLocalizations.of(context).logoutConfirmButton),
),
],
),

View File

@ -224,3 +224,12 @@ final themeModeProvider = Provider<ThemeMode>((ref) {
final notificationsEnabledProvider = Provider<bool>((ref) {
return ref.watch(settingsProvider).notificationsEnabled;
});
final localeProvider = Provider<Locale>((ref) {
final lang = ref.watch(settingsProvider).language;
return switch (lang) {
'zh_TW' => const Locale('zh', 'TW'),
'en' => const Locale('en'),
_ => const Locale('zh'),
};
});

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:it0_app/l10n/app_localizations.dart';
import '../../../../core/config/api_endpoints.dart';
import '../../../../core/network/dio_client.dart';
import '../../../../core/theme/app_colors.dart';
@ -41,17 +42,17 @@ class StandingOrdersPage extends ConsumerWidget {
return Scaffold(
appBar: AppBar(
title: const Text('常驻指令'),
title: Text(AppLocalizations.of(context).standingOrdersPageTitle),
),
body: RefreshIndicator(
onRefresh: () async => ref.invalidate(standingOrdersProvider),
child: ordersAsync.when(
data: (orders) {
if (orders.isEmpty) {
return const EmptyState(
return EmptyState(
icon: Icons.rule_outlined,
title: '暂无常驻指令',
subtitle: '配置后常驻指令将显示在此处',
title: AppLocalizations.of(context).noStandingOrdersTitle,
subtitle: AppLocalizations.of(context).standingOrdersEmptyHint,
);
}
return ListView.builder(
@ -93,7 +94,7 @@ class StandingOrderCardState extends ConsumerState<StandingOrderCard> {
@override
Widget build(BuildContext context) {
final order = widget.order;
final name = order['name'] as String? ?? '未命名指令';
final name = order['name'] as String? ?? AppLocalizations.of(context).unnamedOrderName;
final triggerType = order['trigger_type'] as String? ??
order['triggerType'] as String? ??
'unknown';
@ -113,7 +114,7 @@ class StandingOrderCardState extends ConsumerState<StandingOrderCard> {
final lastExecLabel = lastExecution != null
? DateFormatter.timeAgo(DateTime.parse(lastExecution))
: '从未执行';
: AppLocalizations.of(context).neverExecuted;
return Card(
color: AppColors.surface,

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:it0_app/l10n/app_localizations.dart';
import '../../../../core/config/api_endpoints.dart';
import '../../../../core/errors/error_handler.dart';
import '../../../../core/network/dio_client.dart';
@ -74,12 +75,12 @@ class _TasksPageState extends ConsumerState<TasksPage>
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('任务'),
title: Text(AppLocalizations.of(context).tasksPageTitle),
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(text: '运维任务'),
Tab(text: '常驻指令'),
tabs: [
Tab(text: AppLocalizations.of(context).opsTasksTab),
Tab(text: AppLocalizations.of(context).standingOrdersTab),
],
),
),
@ -130,10 +131,10 @@ class _TasksListBody extends StatelessWidget {
child: tasksAsync.when(
data: (tasks) {
if (tasks.isEmpty) {
return const EmptyState(
return EmptyState(
icon: Icons.assignment_outlined,
title: '暂无任务',
subtitle: '点击 + 创建新任务',
title: AppLocalizations.of(context).noTasksTitle,
subtitle: AppLocalizations.of(context).createNewTaskHint,
);
}
return ListView.builder(
@ -172,10 +173,10 @@ class _StandingOrdersListBody extends StatelessWidget {
child: ordersAsync.when(
data: (orders) {
if (orders.isEmpty) {
return const EmptyState(
return EmptyState(
icon: Icons.rule_outlined,
title: '暂无常驻指令',
subtitle: '通过 我智能体 对话新增常驻指令',
title: AppLocalizations.of(context).noStandingOrdersTitle,
subtitle: AppLocalizations.of(context).standingOrdersHint,
);
}
return ListView.builder(
@ -354,19 +355,19 @@ class _CreateTaskSheetState extends State<_CreateTaskSheet> {
),
),
const SizedBox(height: 16),
const Text(
'新建任务',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
Text(
AppLocalizations.of(context).createTaskTitle,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 20),
// Title
TextFormField(
controller: _titleController,
decoration: const InputDecoration(
labelText: '标题',
hintText: '例如: 重启 web-01 的 nginx',
border: OutlineInputBorder(),
decoration: InputDecoration(
labelText: AppLocalizations.of(context).taskTitleLabel,
hintText: AppLocalizations.of(context).taskTitleHint,
border: const OutlineInputBorder(),
),
validator: (v) => (v == null || v.trim().isEmpty) ? '请输入标题' : null,
textInputAction: TextInputAction.next,
@ -376,10 +377,10 @@ class _CreateTaskSheetState extends State<_CreateTaskSheet> {
// Description
TextFormField(
controller: _descriptionController,
decoration: const InputDecoration(
labelText: '描述',
hintText: '可选详情...',
border: OutlineInputBorder(),
decoration: InputDecoration(
labelText: AppLocalizations.of(context).taskDescriptionLabel,
hintText: AppLocalizations.of(context).taskDescriptionHint,
border: const OutlineInputBorder(),
),
maxLines: 3,
textInputAction: TextInputAction.next,
@ -389,9 +390,9 @@ class _CreateTaskSheetState extends State<_CreateTaskSheet> {
// Priority dropdown
DropdownButtonFormField<String>(
value: _priority,
decoration: const InputDecoration(
labelText: '优先级',
border: OutlineInputBorder(),
decoration: InputDecoration(
labelText: AppLocalizations.of(context).taskPriorityLabel,
border: const OutlineInputBorder(),
),
items: _priorities
.map((p) => DropdownMenuItem(
@ -411,12 +412,12 @@ class _CreateTaskSheetState extends State<_CreateTaskSheet> {
if (servers.isEmpty) return const SizedBox.shrink();
return DropdownButtonFormField<String?>(
value: _selectedServerId,
decoration: const InputDecoration(
labelText: '服务器(可选)',
border: OutlineInputBorder(),
decoration: InputDecoration(
labelText: AppLocalizations.of(context).taskServerOptionalLabel,
border: const OutlineInputBorder(),
),
items: [
const DropdownMenuItem(value: null, child: Text('不指定')),
DropdownMenuItem(value: null, child: Text(AppLocalizations.of(context).taskNoServerSelection)),
...servers.map((s) {
final id = s['id']?.toString() ?? '';
final name = s['hostname'] as String? ??
@ -442,7 +443,7 @@ class _CreateTaskSheetState extends State<_CreateTaskSheet> {
height: 20,
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
)
: const Text('创建任务'),
: Text(AppLocalizations.of(context).createTaskButton),
),
const SizedBox(height: 8),
],

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:it0_app/l10n/app_localizations.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:xterm/xterm.dart';
import '../../../../core/config/api_endpoints.dart';
@ -182,14 +183,14 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
}
}
String get _statusLabel {
String _statusLabel(BuildContext context) {
switch (_status) {
case _ConnectionStatus.connected:
return '已连接';
return AppLocalizations.of(context).terminalConnectedLabel;
case _ConnectionStatus.connecting:
return '连接中...';
return AppLocalizations.of(context).terminalConnectingLabel;
case _ConnectionStatus.disconnected:
return '未连接';
return AppLocalizations.of(context).terminalDisconnectedLabel;
}
}
@ -201,7 +202,7 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
return Scaffold(
appBar: AppBar(
title: const Text('远程终端'),
title: Text(AppLocalizations.of(context).terminalTitle),
actions: [
// Connection status indicator
Padding(
@ -219,7 +220,7 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
),
const SizedBox(width: 6),
Text(
_statusLabel,
_statusLabel(context),
style: TextStyle(
color: _statusColor,
fontSize: 12,
@ -261,9 +262,9 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
child: serversAsync.when(
data: (servers) {
if (servers.isEmpty) {
return const Text(
'暂无可用服务器',
style: TextStyle(
return Text(
AppLocalizations.of(context).terminalNoAvailableServers,
style: const TextStyle(
color: AppColors.textMuted,
fontSize: 14,
),
@ -272,9 +273,9 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
return DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: _selectedServerId,
hint: const Text(
'选择服务器...',
style: TextStyle(
hint: Text(
AppLocalizations.of(context).terminalSelectServerHint,
style: const TextStyle(
color: AppColors.textMuted,
fontSize: 14,
),
@ -317,7 +318,7 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
const SizedBox(width: 6),
Expanded(
child: Text(
'加载服务器失败',
AppLocalizations.of(context).terminalLoadServersError,
style: const TextStyle(
color: AppColors.error,
fontSize: 13,
@ -355,10 +356,10 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
),
label: Text(
_status == _ConnectionStatus.connected
? '断开'
? AppLocalizations.of(context).terminalDisconnectButton
: _status == _ConnectionStatus.connecting
? '连接中...'
: '连接',
? AppLocalizations.of(context).terminalConnectingMessage
: AppLocalizations.of(context).terminalConnectButton,
style: const TextStyle(fontSize: 13),
),
),

404
it0_app/lib/l10n/app_en.arb Normal file
View File

@ -0,0 +1,404 @@
{
"@@locale": "en",
"appTitle": "My Agent",
"appSubtitle": "Server Cluster Operations AI Agent",
"navHome": "Home",
"navMyAgents": "My Agents",
"navBilling": "Billing",
"navProfile": "Me",
"cancelButton": "Cancel",
"confirmButton": "Confirm",
"saveButton": "Save",
"retryButton": "Retry",
"loadingLabel": "Loading...",
"unknownLabel": "Unknown",
"unnamedLabel": "Unnamed",
"homeGreeting": "{greeting}, {name}",
"@homeGreeting": {
"placeholders": {
"greeting": { "type": "String" },
"name": { "type": "String" }
}
},
"homeSubtitle": "My Agent is always here for you",
"greetingEarlyMorning": "Good morning",
"greetingNoon": "Good noon",
"greetingAfternoon": "Good afternoon",
"greetingEvening": "Good evening",
"greetingLateNight": "Late night",
"agentStatusIdle": "Idle",
"agentStatusThinking": "Thinking...",
"agentStatusExecuting": "Executing...",
"agentStatusAwaitingApproval": "Awaiting approval",
"agentStatusError": "Error occurred",
"officialAgentsSection": "IT0 Official Agents",
"myAgentsSection": "My Agents",
"officialBadge": "Official",
"officialAgent1Name": "My Agent Ops Assistant",
"officialAgent1Desc": "Server management, SSH execution, log analysis",
"officialAgent2Name": "Security Audit Assistant",
"officialAgent2Desc": "Vulnerability scanning, permission review, compliance",
"officialAgent3Name": "Database Inspector",
"officialAgent3Desc": "Slow query analysis, index optimization, backup verification",
"noOwnAgentsTitle": "No agents yet",
"noOwnAgentsDesc": "Tap the robot button below and tell My Agent:\n\"Recruit an OpenClaw agent for me\"",
"quickTipsHeader": "You can say...",
"quickTip1": "💬 \"Recruit an agent to monitor GitHub Actions\"",
"quickTip2": "🔧 \"Export my OpenClaw config as JSON\"",
"quickTip3": "📊 \"Analyze server load for the past 7 days\"",
"quickTip4": "🛡️ \"Set up automatic database backup at 2AM daily\"",
"myAgentsTitle": "My Agents",
"myAgentsEmptyTitle": "Recruit your own agent",
"myAgentsEmptyDesc": "Chat with My Agent to recruit various agents:\nOpenClaw coding assistant, ops bot, data analyst...",
"myAgentsStep1Title": "Tap the robot button",
"myAgentsStep1Desc": "Open a conversation with My Agent",
"myAgentsStep2Title": "Describe the agent you want",
"myAgentsStep2Desc": "e.g. \"Recruit an OpenClaw coding assistant for me\"",
"myAgentsStep3Title": "My Agent auto-deploys",
"myAgentsStep3Desc": "It appears here after deployment. Chat via Telegram/WhatsApp.",
"myAgentsTemplatesHeader": "Popular templates (tell My Agent which one you want)",
"summaryTotal": "Total {count}",
"@summaryTotal": {
"placeholders": { "count": { "type": "int" } }
},
"summaryRunning": "Running {count}",
"@summaryRunning": {
"placeholders": { "count": { "type": "int" } }
},
"summaryStopped": "Stopped {count}",
"@summaryStopped": {
"placeholders": { "count": { "type": "int" } }
},
"statusRunning": "Running",
"statusDeploying": "Deploying",
"statusStopped": "Stopped",
"statusError": "Error",
"dismissTitle": "Dismiss Agent",
"dismissConfirmContent": "Confirm dismissal of \"{name}\"?\n\nThe agent container will be stopped and deleted. This cannot be undone.",
"@dismissConfirmContent": {
"placeholders": { "name": { "type": "String" } }
},
"dismissButton": "Dismiss",
"renameButton": "Rename",
"renameTitle": "Rename",
"renameHint": "Enter new name",
"dismissSuccessMessage": "Dismissed \"{name}\"",
"@dismissSuccessMessage": {
"placeholders": { "name": { "type": "String" } }
},
"dismissErrorMessage": "Dismiss failed: {error}",
"@dismissErrorMessage": {
"placeholders": { "error": { "type": "String" } }
},
"renameSuccessMessage": "Renamed successfully",
"renameErrorMessage": "Rename failed: {error}",
"@renameErrorMessage": {
"placeholders": { "error": { "type": "String" } }
},
"loginPasswordTab": "Password",
"loginOtpTab": "OTP",
"emailLabel": "Email",
"emailHint": "user@example.com",
"emailRequiredError": "Please enter email",
"invalidEmailError": "Please enter a valid email",
"passwordLabel": "Password",
"passwordRequiredError": "Please enter password",
"phoneLabel": "Phone",
"phoneHint": "+1 234 567 8900",
"phoneRequiredError": "Please enter phone number",
"otpLabel": "OTP",
"otpHint": "6-digit code",
"otpRequiredError": "Please enter OTP",
"sendingLabel": "Sending",
"getOtpButton": "Get OTP",
"enterPhoneFirstError": "Please enter phone number first",
"loginButton": "Login",
"accountCreationNote": "Accounts are created by admin or via invitation link",
"chatNewConversationTooltip": "New conversation",
"chatStopTooltip": "Stop",
"chatVoiceCallTooltip": "Voice call",
"chatSelectFromAlbum": "Select from album",
"chatMultiSelectSupport": "Multi-select supported",
"chatTakePhoto": "Take photo",
"chatSelectFile": "Select file",
"chatImagesPdfLabel": "Images, PDF",
"chatThinkingLabel": "Thinking...",
"chatNeedsApprovalLabel": "Needs approval",
"chatExecutionFailedLabel": "Execution failed",
"chatExecutionResultLabel": "Execution result",
"chatStandingOrderDraftLabel": "Standing order draft",
"chatProcessingLabel": "Processing...",
"chatReplyingLabel": "Replying...",
"chatReplyLabel": "Reply",
"chatStartConversationPrompt": "Start chatting with My Agent",
"chatInputInstructionHint": "Enter command or make a voice call",
"chatAdditionalInstructionHint": "Additional instruction...",
"chatInstructionHint": "Enter instruction...",
"chatAddImageTooltip": "Add image",
"chatInjectionTooltip": "Inject instruction",
"chatCollapseLabel": "Collapse",
"chatExpandLabel": "Expand ({lineCount} lines)",
"@chatExpandLabel": {
"placeholders": { "lineCount": { "type": "int" } }
},
"chatRecognizingLabel": "Recognizing…",
"chatSpeechRecognitionError": "Speech recognition failed, please retry",
"chatTargetsLabel": "Targets: ",
"agentCallVoiceCallTitle": "Voice Call",
"agentCallRingingStatus": "My Agent Voice Call",
"agentCallActiveStatus": "My Agent",
"agentCallConnectingStatus": "Connecting...",
"agentCallEndedStatus": "Call ended",
"agentCallThinking": "Thinking...",
"terminalTitle": "Remote Terminal",
"terminalInitMessage": "My Agent Remote Terminal",
"terminalSelectServerMessage": "Select a server and click Connect.",
"terminalSelectServerFirst": "Please select a server first",
"terminalConnectingMessage": "Connecting to server",
"terminalConnectedLabel": "Connected",
"terminalConnectingLabel": "Connecting...",
"terminalDisconnectedLabel": "Disconnected",
"terminalSelectServerHint": "Select server...",
"terminalNoAvailableServers": "No servers available",
"terminalLoadServersError": "Failed to load servers",
"terminalConnectButton": "Connect",
"terminalDisconnectButton": "Disconnect",
"terminalDisconnectMessage": "Disconnected",
"tasksPageTitle": "Tasks",
"opsTasksTab": "Ops Tasks",
"standingOrdersTab": "Standing Orders",
"noTasksTitle": "No tasks",
"createNewTaskHint": "Tap + to create a new task",
"noStandingOrdersTitle": "No standing orders",
"standingOrdersHint": "Chat with My Agent to add standing orders",
"createTaskTitle": "New Task",
"taskTitleLabel": "Title",
"taskTitleHint": "e.g. Restart nginx on web-01",
"taskDescriptionLabel": "Description",
"taskDescriptionHint": "Optional details...",
"taskPriorityLabel": "Priority",
"taskServerOptionalLabel": "Server (optional)",
"taskNoServerSelection": "Any",
"createTaskButton": "Create Task",
"createTaskError": "Failed to create task: {error}",
"@createTaskError": {
"placeholders": { "error": { "type": "String" } }
},
"notificationInboxTitle": "Inbox",
"notificationMarkAllRead": "Mark all read",
"notificationLoadingFailed": "Failed to load",
"noMessagesTitle": "No messages",
"operationFailedError": "Operation failed, please retry",
"linkLabel": "Link: ",
"notificationPreferencesTitle": "Notification Preferences",
"noNotificationChannels": "No configurable notification channels",
"notificationPreferencesInfo": "Choose which notifications to receive. Mandatory notifications (e.g. security alerts) cannot be disabled.",
"mandatoryNotificationsSection": "Mandatory notifications",
"optionalNotificationsSection": "Optional notifications",
"savePreferencesButton": "Save preferences",
"requiredLabel": "Required",
"preferencesSavedMessage": "Preferences saved",
"saveFailedMessage": "Save failed: {error}",
"@saveFailedMessage": {
"placeholders": { "error": { "type": "String" } }
},
"referralScreenTitle": "Refer & Earn",
"yourReferralCodeLabel": "Your referral code",
"copyReferralCodeTooltip": "Copy referral code",
"copyInviteLinkButton": "Copy invite link",
"shareButton": "Share",
"copiedToClipboard": "Copied to clipboard",
"referralRecordsSection": "Referral records",
"viewAllReferralsLink": "View all >",
"pendingRewardsSection": "Pending credits",
"viewAllRewardsLink": "View all >",
"referredLabel": "Referred",
"peopleUnit": " people",
"activatedLabel": "Activated",
"pendingCreditsLabel": "Pending credits",
"rewardRulesTitle": "Reward rules",
"proReferralReward": "Refer Pro plan: you get USD15, they get USD5",
"enterpriseReferralReward": "Refer Enterprise plan: you get USD50, they get USD20",
"renewalBonusReward": "Earn 10% of their monthly payments for up to 12 months",
"creditDeductionNote": "Credits automatically applied to your next invoice",
"noReferralsMessage": "No referrals yet. Share your code!",
"pendingPaymentStatus": "Pending",
"activeStatus": "Active",
"rewardedStatus": "Rewarded",
"expiredStatus": "Expired",
"registeredAt": "Registered",
"noPendingRewardsMessage": "No pending credits",
"noReferralRecordsMessage": "No referral records",
"noRewardRecordsMessage": "No reward records",
"pendingDeductionStatus": "Pending deduction",
"billingTitle": "Subscription & Usage",
"upgradeButton": "Upgrade",
"upgradeDialogTitle": "Upgrade Plan",
"upgradeDialogMessage": "Go to Web Admin → Billing → Plans to complete the upgrade.",
"acknowledgeButton": "Got it",
"currentPlanLabel": "Current plan",
"periodEndLabel": "Period ends: ",
"tokenUsageLabel": "Token usage this month",
"unlimitedLabel": "Unlimited",
"billingStatusActive": "Active",
"billingStatusTrialing": "Trial",
"billingStatusPastDue": "Past due",
"billingStatusCancelled": "Cancelled",
"billingStatusExpired": "Expired",
"invoicePaidStatus": "Paid",
"invoiceUnpaidStatus": "Unpaid",
"serversPageTitle": "Servers",
"noServersTitle": "No servers found",
"noServersFiltered": "No servers match the current filter",
"allEnvironments": "All",
"ipAddressLabel": "IP Address",
"osLabel": "OS",
"cpuLabel": "CPU",
"memoryLabel": "Memory",
"regionLabel": "Region",
"cloudProviderLabel": "Cloud provider",
"createdAtLabel": "Created at",
"standingOrdersPageTitle": "Standing Orders",
"standingOrdersEmptyHint": "Standing orders will appear here after configuration",
"executionHistoryLabel": "Execution history ({count})",
"@executionHistoryLabel": {
"placeholders": { "count": { "type": "int" } }
},
"unnamedOrderName": "Unnamed order",
"neverExecuted": "Never executed",
"updateStatusError": "Failed to update status: {error}",
"@updateStatusError": {
"placeholders": { "error": { "type": "String" } }
},
"settingsPageTitle": "Settings",
"appearanceThemeLabel": "Appearance",
"languageLabel": "Language",
"languageZh": "简体中文",
"languageZhTW": "繁體中文",
"languageEn": "English",
"selectLanguageTitle": "Select Language",
"pushNotificationsLabel": "Push notifications",
"soundLabel": "Sound",
"hapticFeedbackLabel": "Haptic feedback",
"conversationEngineLabel": "AI engine",
"ttsVoiceLabel": "Voice",
"ttsStyleLabel": "Voice style",
"subscriptionLabel": "Subscription & Usage",
"changePasswordLabel": "Change password",
"versionLabel": "Version",
"checkUpdateLabel": "Check for updates",
"tenantLabel": "Tenant",
"logoutButton": "Log out",
"selectThemeTitle": "Select Theme",
"darkModeLabel": "Dark",
"lightModeLabel": "Light",
"followSystemLabel": "System",
"selectVoiceTitle": "Select Voice",
"voiceCoralDesc": "Female · Warm",
"voiceNovaDesc": "Female · Lively",
"voiceSageDesc": "Female · Intellectual",
"voiceShimmerDesc": "Female · Soft",
"voiceMarinDesc": "Female · Clear",
"voiceAshDesc": "Male · Steady",
"voiceEchoDesc": "Male · Bright",
"voiceOnyxDesc": "Male · Deep",
"voiceVerseDesc": "Male · Magnetic",
"voiceBalladDesc": "Male · Rich",
"voiceCedarDesc": "Male · Natural",
"voiceAlloyDesc": "Neutral",
"voiceFableDesc": "Neutral · Narrative",
"selectEngineTitle": "Select AI Engine",
"agentSdkDesc": "Supports tool approval, skill injection, session restore",
"claudeApiDesc": "Direct API, faster response",
"selectStyleTitle": "Select Voice Style",
"defaultStyleLabel": "Default",
"customStyleLabel": "Custom style",
"customStyleHint": "e.g. Speak like a pirate, with humor",
"resetToDefaultButton": "Reset to default",
"styleProfessionalName": "Professional",
"styleProfessionalDesc": "Speak in a professional, concise, and efficient tone.",
"styleGentleName": "Gentle & Patient",
"styleGentleDesc": "Speak in a warm and patient tone, like a caring friend.",
"styleRelaxedName": "Relaxed & Lively",
"styleRelaxedDesc": "Speak in a relaxed, lively tone with a bit of humor.",
"styleFormalName": "Formal",
"styleFormalDesc": "Speak in a serious, formal tone, like in a business meeting.",
"styleScifiName": "Sci-Fi AI",
"styleScifiDesc": "Speak like an AI in a sci-fi movie — calm, rational, futuristic.",
"editNameDialogTitle": "Edit Display Name",
"displayNameLabel": "Display name",
"displayNameHint": "Enter new display name",
"changePasswordTitle": "Change Password",
"currentPasswordLabel": "Current password",
"newPasswordLabel": "New password",
"confirmPasswordLabel": "Confirm new password",
"passwordMismatchError": "Passwords do not match",
"passwordMinLengthError": "New password must be at least 6 characters",
"confirmChangeButton": "Confirm",
"passwordChangedMessage": "Password changed",
"nameUpdatedMessage": "Name updated",
"updateFailedMessage": "Update failed",
"changeFailedMessage": "Change failed",
"logoutDialogTitle": "Log out",
"logoutConfirmMessage": "Are you sure you want to log out?",
"logoutConfirmButton": "Log out",
"profileSubscriptionLabel": "Subscription & Usage",
"profileFreePlanLabel": "Free",
"profileReferralLabel": "Refer & Earn",
"profileReferralHint": "Earn credits by referring",
"profileInSiteMessagesLabel": "Inbox",
"profileViewMessagesLabel": "View messages",
"errorNetworkError": "Cannot connect to server, check your network",
"errorDataFormat": "Invalid data format",
"errorUnknown": "Unknown error occurred",
"errorConnectionTimeout": "Connection timed out",
"errorSendTimeout": "Request timed out, check your network",
"errorReceiveTimeout": "Response timed out, please retry",
"errorBadCertificate": "SSL certificate verification failed",
"errorRequestCancelled": "Request cancelled",
"errorBadRequest": "Invalid request parameters",
"errorPermissionDenied": "Permission denied",
"errorNotFound": "Resource not found",
"errorConflict": "Data conflict, please refresh",
"errorInvalidData": "Invalid data submitted",
"errorTooManyRequests": "Too many requests, please slow down",
"errorInternalServer": "Server error, please retry",
"errorBadGateway": "Gateway error, please retry",
"errorServiceUnavailable": "Service unavailable, please retry",
"errorConnectionReset": "Connection reset, please retry",
"errorConnectionRefused": "Connection refused, check if service is running",
"errorConnectionClosed": "Connection closed, please retry",
"errorSocketException": "Network error, check your connection",
"errorTlsException": "Secure connection failed, check your network",
"errorNetworkRequestFailed": "Network request failed, check your connection"
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

404
it0_app/lib/l10n/app_zh.arb Normal file
View File

@ -0,0 +1,404 @@
{
"@@locale": "zh",
"appTitle": "我智能体",
"appSubtitle": "服务器集群运维智能体",
"navHome": "主页",
"navMyAgents": "我的智能体",
"navBilling": "账单",
"navProfile": "我",
"cancelButton": "取消",
"confirmButton": "确认",
"saveButton": "保存",
"retryButton": "重试",
"loadingLabel": "加载中...",
"unknownLabel": "未知",
"unnamedLabel": "未命名",
"homeGreeting": "{greeting}{name}",
"@homeGreeting": {
"placeholders": {
"greeting": { "type": "String" },
"name": { "type": "String" }
}
},
"homeSubtitle": "我智能体 随时为你服务",
"greetingEarlyMorning": "早上好",
"greetingNoon": "中午好",
"greetingAfternoon": "下午好",
"greetingEvening": "晚上好",
"greetingLateNight": "夜深了",
"agentStatusIdle": "空闲中",
"agentStatusThinking": "正在思考...",
"agentStatusExecuting": "执行指令中...",
"agentStatusAwaitingApproval": "等待审批",
"agentStatusError": "发生错误",
"officialAgentsSection": "IT0 官方智能体",
"myAgentsSection": "我的智能体",
"officialBadge": "官方",
"officialAgent1Name": "我智能体 运维助手",
"officialAgent1Desc": "服务器管理、SSH 执行、日志分析",
"officialAgent2Name": "安全审计助手",
"officialAgent2Desc": "漏洞扫描、权限审查、合规检查",
"officialAgent3Name": "数据库巡检",
"officialAgent3Desc": "慢查询分析、索引优化、备份验证",
"noOwnAgentsTitle": "还没有自己的智能体",
"noOwnAgentsDesc": "点击下方机器人按钮,告诉 我智能体\n\"帮我招募一个 OpenClaw 智能体\"",
"quickTipsHeader": "你可以这样说...",
"quickTip1": "💬 \"帮我招募一个监控 GitHub Actions 的智能体\"",
"quickTip2": "🔧 \"把我的 OpenClaw 配置导出为 JSON\"",
"quickTip3": "📊 \"分析我的服务器最近7天的负载情况\"",
"quickTip4": "🛡️ \"帮我设置每天凌晨2点自动备份数据库\"",
"myAgentsTitle": "我的智能体",
"myAgentsEmptyTitle": "招募你的专属智能体",
"myAgentsEmptyDesc": "通过与 我智能体 对话,你可以招募各种智能体:\nOpenClaw 编程助手、运维机器人、数据分析师...",
"myAgentsStep1Title": "点击下方机器人",
"myAgentsStep1Desc": "打开与 我智能体 的对话窗口",
"myAgentsStep2Title": "描述你想要的智能体",
"myAgentsStep2Desc": "例如:\"帮我招募一个 OpenClaw 编程助手\"",
"myAgentsStep3Title": "我智能体 自动部署",
"myAgentsStep3Desc": "部署完成后出现在这里,通过 Telegram/WhatsApp 等渠道与它对话",
"myAgentsTemplatesHeader": "热门模板(告诉 我智能体 你想要哪种)",
"summaryTotal": "总计 {count}",
"@summaryTotal": {
"placeholders": { "count": { "type": "int" } }
},
"summaryRunning": "运行中 {count}",
"@summaryRunning": {
"placeholders": { "count": { "type": "int" } }
},
"summaryStopped": "已停止 {count}",
"@summaryStopped": {
"placeholders": { "count": { "type": "int" } }
},
"statusRunning": "运行中",
"statusDeploying": "部署中",
"statusStopped": "已停止",
"statusError": "错误",
"dismissTitle": "解聘智能体",
"dismissConfirmContent": "确认要解聘「{name}」吗?\n\n解聘后将停止并删除该智能体容器此操作不可撤销。",
"@dismissConfirmContent": {
"placeholders": { "name": { "type": "String" } }
},
"dismissButton": "解聘",
"renameButton": "重命名",
"renameTitle": "重命名",
"renameHint": "输入新名称",
"dismissSuccessMessage": "已解聘「{name}」",
"@dismissSuccessMessage": {
"placeholders": { "name": { "type": "String" } }
},
"dismissErrorMessage": "解聘失败:{error}",
"@dismissErrorMessage": {
"placeholders": { "error": { "type": "String" } }
},
"renameSuccessMessage": "重命名成功",
"renameErrorMessage": "重命名失败:{error}",
"@renameErrorMessage": {
"placeholders": { "error": { "type": "String" } }
},
"loginPasswordTab": "密码登录",
"loginOtpTab": "验证码登录",
"emailLabel": "邮箱",
"emailHint": "user@example.com",
"emailRequiredError": "请输入邮箱地址",
"invalidEmailError": "请输入有效的邮箱地址",
"passwordLabel": "密码",
"passwordRequiredError": "请输入密码",
"phoneLabel": "手机号",
"phoneHint": "+86 138 0000 0000",
"phoneRequiredError": "请输入手机号",
"otpLabel": "验证码",
"otpHint": "6 位数字",
"otpRequiredError": "请输入验证码",
"sendingLabel": "发送中",
"getOtpButton": "获取验证码",
"enterPhoneFirstError": "请先输入手机号",
"loginButton": "登录",
"accountCreationNote": "账号由管理员在后台创建或通过邀请链接注册",
"chatNewConversationTooltip": "新对话",
"chatStopTooltip": "停止",
"chatVoiceCallTooltip": "语音通话",
"chatSelectFromAlbum": "从相册选择",
"chatMultiSelectSupport": "支持多选",
"chatTakePhoto": "拍照",
"chatSelectFile": "选择文件",
"chatImagesPdfLabel": "图片、PDF",
"chatThinkingLabel": "思考中...",
"chatNeedsApprovalLabel": "需要审批",
"chatExecutionFailedLabel": "执行失败",
"chatExecutionResultLabel": "执行结果",
"chatStandingOrderDraftLabel": "常驻指令草案",
"chatProcessingLabel": "处理中...",
"chatReplyingLabel": "回复中...",
"chatReplyLabel": "回复",
"chatStartConversationPrompt": "开始与 我智能体 对话",
"chatInputInstructionHint": "输入指令或拨打语音通话",
"chatAdditionalInstructionHint": "追加指令...",
"chatInstructionHint": "输入指令...",
"chatAddImageTooltip": "添加图片",
"chatInjectionTooltip": "追加指令",
"chatCollapseLabel": "收起",
"chatExpandLabel": "展开 ({lineCount} 行)",
"@chatExpandLabel": {
"placeholders": { "lineCount": { "type": "int" } }
},
"chatRecognizingLabel": "识别中…",
"chatSpeechRecognitionError": "语音识别失败,请重试",
"chatTargetsLabel": "目标: ",
"agentCallVoiceCallTitle": "语音通话",
"agentCallRingingStatus": "我智能体 语音通话",
"agentCallActiveStatus": "我智能体",
"agentCallConnectingStatus": "连接中...",
"agentCallEndedStatus": "通话结束",
"agentCallThinking": "思考中...",
"terminalTitle": "远程终端",
"terminalInitMessage": "我智能体 远程终端",
"terminalSelectServerMessage": "请选择服务器并点击连接。",
"terminalSelectServerFirst": "请先选择服务器",
"terminalConnectingMessage": "正在连接服务器",
"terminalConnectedLabel": "已连接",
"terminalConnectingLabel": "连接中...",
"terminalDisconnectedLabel": "未连接",
"terminalSelectServerHint": "选择服务器...",
"terminalNoAvailableServers": "暂无可用服务器",
"terminalLoadServersError": "加载服务器失败",
"terminalConnectButton": "连接",
"terminalDisconnectButton": "断开",
"terminalDisconnectMessage": "已断开连接",
"tasksPageTitle": "任务",
"opsTasksTab": "运维任务",
"standingOrdersTab": "常驻指令",
"noTasksTitle": "暂无任务",
"createNewTaskHint": "点击 + 创建新任务",
"noStandingOrdersTitle": "暂无常驻指令",
"standingOrdersHint": "通过 我智能体 对话新增常驻指令",
"createTaskTitle": "新建任务",
"taskTitleLabel": "标题",
"taskTitleHint": "例如: 重启 web-01 的 nginx",
"taskDescriptionLabel": "描述",
"taskDescriptionHint": "可选详情...",
"taskPriorityLabel": "优先级",
"taskServerOptionalLabel": "服务器(可选)",
"taskNoServerSelection": "不指定",
"createTaskButton": "创建任务",
"createTaskError": "创建任务失败: {error}",
"@createTaskError": {
"placeholders": { "error": { "type": "String" } }
},
"notificationInboxTitle": "站内消息",
"notificationMarkAllRead": "全部已读",
"notificationLoadingFailed": "加载失败",
"noMessagesTitle": "暂无消息",
"operationFailedError": "操作失败,请重试",
"linkLabel": "链接:",
"notificationPreferencesTitle": "通知偏好设置",
"noNotificationChannels": "暂无可配置的通知频道",
"notificationPreferencesInfo": "您可以选择接收哪些类型的通知。强制通知(如安全告警)无法关闭。",
"mandatoryNotificationsSection": "重要通知(不可关闭)",
"optionalNotificationsSection": "可选通知",
"savePreferencesButton": "保存偏好设置",
"requiredLabel": "必需",
"preferencesSavedMessage": "通知偏好已保存",
"saveFailedMessage": "保存失败: {error}",
"@saveFailedMessage": {
"placeholders": { "error": { "type": "String" } }
},
"referralScreenTitle": "邀请有礼",
"yourReferralCodeLabel": "你的推荐码",
"copyReferralCodeTooltip": "复制推荐码",
"copyInviteLinkButton": "复制邀请链接",
"shareButton": "分享",
"copiedToClipboard": "已复制到剪贴板",
"referralRecordsSection": "推荐记录",
"viewAllReferralsLink": "查看全部 >",
"pendingRewardsSection": "待领积分",
"viewAllRewardsLink": "查看全部 >",
"referredLabel": "已推荐",
"peopleUnit": "人",
"activatedLabel": "已激活",
"pendingCreditsLabel": "待领积分",
"rewardRulesTitle": "奖励规则",
"proReferralReward": "推荐 Pro 套餐:你获得 $15 积分,对方获得 $5 积分",
"enterpriseReferralReward": "推荐 Enterprise 套餐:你获得 $50 积分,对方获得 $20 积分",
"renewalBonusReward": "对方续订后,你持续获得每月付款额 10% 的积分,最长 12 个月",
"creditDeductionNote": "积分自动抵扣你的下期账单",
"noReferralsMessage": "暂无推荐记录,分享推荐码邀请好友吧",
"pendingPaymentStatus": "待付款",
"activeStatus": "已激活",
"rewardedStatus": "已奖励",
"expiredStatus": "已过期",
"registeredAt": "注册于",
"noPendingRewardsMessage": "暂无待领积分",
"noReferralRecordsMessage": "暂无推荐记录",
"noRewardRecordsMessage": "暂无奖励记录",
"pendingDeductionStatus": "待抵扣",
"billingTitle": "订阅与用量",
"upgradeButton": "升级套餐",
"upgradeDialogTitle": "升级套餐",
"upgradeDialogMessage": "请前往 Web 管理后台 → 账单 → 套餐 完成升级。",
"acknowledgeButton": "知道了",
"currentPlanLabel": "当前套餐",
"periodEndLabel": "当期结束:",
"tokenUsageLabel": "本月 Token 用量",
"unlimitedLabel": "不限量",
"billingStatusActive": "正常",
"billingStatusTrialing": "试用期",
"billingStatusPastDue": "待付款",
"billingStatusCancelled": "已取消",
"billingStatusExpired": "已过期",
"invoicePaidStatus": "已付款",
"invoiceUnpaidStatus": "待付款",
"serversPageTitle": "服务器",
"noServersTitle": "未找到服务器",
"noServersFiltered": "没有匹配当前筛选条件的服务器",
"allEnvironments": "全部",
"ipAddressLabel": "IP 地址",
"osLabel": "操作系统",
"cpuLabel": "CPU",
"memoryLabel": "内存",
"regionLabel": "区域",
"cloudProviderLabel": "云厂商",
"createdAtLabel": "创建时间",
"standingOrdersPageTitle": "常驻指令",
"standingOrdersEmptyHint": "配置后常驻指令将显示在此处",
"executionHistoryLabel": "执行历史 ({count})",
"@executionHistoryLabel": {
"placeholders": { "count": { "type": "int" } }
},
"unnamedOrderName": "未命名指令",
"neverExecuted": "从未执行",
"updateStatusError": "更新状态失败: {error}",
"@updateStatusError": {
"placeholders": { "error": { "type": "String" } }
},
"settingsPageTitle": "设置",
"appearanceThemeLabel": "外观主题",
"languageLabel": "语言",
"languageZh": "简体中文",
"languageZhTW": "繁體中文",
"languageEn": "English",
"selectLanguageTitle": "选择语言",
"pushNotificationsLabel": "推送通知",
"soundLabel": "提示音",
"hapticFeedbackLabel": "震动反馈",
"conversationEngineLabel": "对话引擎",
"ttsVoiceLabel": "语音音色",
"ttsStyleLabel": "语音风格",
"subscriptionLabel": "订阅与用量",
"changePasswordLabel": "修改密码",
"versionLabel": "版本",
"checkUpdateLabel": "检查更新",
"tenantLabel": "租户",
"logoutButton": "退出登录",
"selectThemeTitle": "选择主题",
"darkModeLabel": "深色模式",
"lightModeLabel": "浅色模式",
"followSystemLabel": "跟随系统",
"selectVoiceTitle": "选择语音音色",
"voiceCoralDesc": "女 · 温暖",
"voiceNovaDesc": "女 · 活泼",
"voiceSageDesc": "女 · 知性",
"voiceShimmerDesc": "女 · 柔和",
"voiceMarinDesc": "女 · 清澈",
"voiceAshDesc": "男 · 沉稳",
"voiceEchoDesc": "男 · 清朗",
"voiceOnyxDesc": "男 · 低沉",
"voiceVerseDesc": "男 · 磁性",
"voiceBalladDesc": "男 · 浑厚",
"voiceCedarDesc": "男 · 自然",
"voiceAlloyDesc": "中性",
"voiceFableDesc": "中性 · 叙事",
"selectEngineTitle": "选择对话引擎",
"agentSdkDesc": "支持工具审批、技能注入、会话恢复",
"claudeApiDesc": "直连 API响应更快",
"selectStyleTitle": "选择语音风格",
"defaultStyleLabel": "默认",
"customStyleLabel": "自定义风格",
"customStyleHint": "例如:用东北话说话,幽默风趣",
"resetToDefaultButton": "恢复默认",
"styleProfessionalName": "专业干练",
"styleProfessionalDesc": "用专业、简洁、干练的语气说话,不拖泥带水。",
"styleGentleName": "温柔耐心",
"styleGentleDesc": "用温柔、耐心的语气说话,像一个贴心的朋友。",
"styleRelaxedName": "轻松活泼",
"styleRelaxedDesc": "用轻松、活泼的语气说话,带一点幽默感。",
"styleFormalName": "严肃正式",
"styleFormalDesc": "用严肃、正式的语气说话,像在正式会议中发言。",
"styleScifiName": "科幻AI",
"styleScifiDesc": "用科幻电影中AI的语气说话冷静、理性、略带未来感。",
"editNameDialogTitle": "修改显示名称",
"displayNameLabel": "显示名称",
"displayNameHint": "输入新的显示名称",
"changePasswordTitle": "修改密码",
"currentPasswordLabel": "当前密码",
"newPasswordLabel": "新密码",
"confirmPasswordLabel": "确认新密码",
"passwordMismatchError": "两次输入的密码不一致",
"passwordMinLengthError": "新密码至少6个字符",
"confirmChangeButton": "确认修改",
"passwordChangedMessage": "密码已修改",
"nameUpdatedMessage": "名称已更新",
"updateFailedMessage": "更新失败",
"changeFailedMessage": "修改失败",
"logoutDialogTitle": "退出登录",
"logoutConfirmMessage": "确定要退出登录吗?",
"logoutConfirmButton": "退出",
"profileSubscriptionLabel": "订阅套餐与用量",
"profileFreePlanLabel": "Free",
"profileReferralLabel": "邀请有礼",
"profileReferralHint": "推荐赚积分",
"profileInSiteMessagesLabel": "站内消息",
"profileViewMessagesLabel": "查看消息",
"errorNetworkError": "无法连接到服务器,请检查网络",
"errorDataFormat": "数据格式异常",
"errorUnknown": "发生未知错误",
"errorConnectionTimeout": "连接超时,服务器无响应",
"errorSendTimeout": "发送请求超时,请检查网络",
"errorReceiveTimeout": "等待响应超时,请稍后重试",
"errorBadCertificate": "安全证书验证失败",
"errorRequestCancelled": "请求已取消",
"errorBadRequest": "请求参数错误",
"errorPermissionDenied": "没有权限执行此操作",
"errorNotFound": "请求的资源不存在",
"errorConflict": "数据冲突,请刷新后重试",
"errorInvalidData": "提交的数据不合法",
"errorTooManyRequests": "请求过于频繁,请稍后重试",
"errorInternalServer": "服务器内部错误,请稍后重试",
"errorBadGateway": "服务器网关错误,请稍后重试",
"errorServiceUnavailable": "服务器暂时不可用,请稍后重试",
"errorConnectionReset": "连接被服务器重置,请稍后重试",
"errorConnectionRefused": "服务器拒绝连接,请确认服务是否启动",
"errorConnectionClosed": "连接已关闭,请稍后重试",
"errorSocketException": "网络连接异常,请检查网络设置",
"errorTlsException": "安全连接失败,请检查网络环境",
"errorNetworkRequestFailed": "网络请求失败,请检查网络后重试"
}

View File

@ -0,0 +1,140 @@
{
"@@locale": "zh_TW",
"appTitle": "我智能體",
"appSubtitle": "伺服器叢集運維智能體",
"navMyAgents": "我的智能體",
"homeSubtitle": "我智能體 隨時為你服務",
"officialAgent1Name": "我智能體 運維助手",
"officialAgent1Desc": "伺服器管理、SSH 執行、日誌分析",
"myAgentsSection": "我的智能體",
"noOwnAgentsTitle": "還沒有自己的智能體",
"noOwnAgentsDesc": "點擊下方機器人按鈕,告訴 我智能體\n\"幫我招募一個 OpenClaw 智能體\"",
"quickTipsHeader": "你可以這樣說...",
"quickTip1": "💬 \"幫我招募一個監控 GitHub Actions 的智能體\"",
"quickTip2": "🔧 \"把我的 OpenClaw 配置匯出為 JSON\"",
"quickTip3": "📊 \"分析我的伺服器最近7天的負載情況\"",
"quickTip4": "🛡️ \"幫我設置每天凌晨2點自動備份資料庫\"",
"myAgentsTitle": "我的智能體",
"myAgentsEmptyTitle": "招募你的專屬智能體",
"myAgentsEmptyDesc": "透過與 我智能體 對話,你可以招募各種智能體:\nOpenClaw 程式助手、運維機器人、資料分析師...",
"myAgentsStep1Desc": "打開與 我智能體 的對話視窗",
"myAgentsStep2Desc": "例如:\"幫我招募一個 OpenClaw 程式助手\"",
"myAgentsStep3Title": "我智能體 自動部署",
"myAgentsStep3Desc": "部署完成後出現在這裡,透過 Telegram/WhatsApp 等渠道與它對話",
"myAgentsTemplatesHeader": "熱門模板(告訴 我智能體 你想要哪種)",
"statusRunning": "運行中",
"statusDeploying": "部署中",
"statusStopped": "已停止",
"dismissTitle": "解聘智能體",
"dismissConfirmContent": "確認要解聘「{name}」嗎?\n\n解聘後將停止並刪除該智能體容器此操作不可撤銷。",
"@dismissConfirmContent": {
"placeholders": { "name": { "type": "String" } }
},
"dismissSuccessMessage": "已解聘「{name}」",
"@dismissSuccessMessage": {
"placeholders": { "name": { "type": "String" } }
},
"languageZh": "簡體中文",
"languageZhTW": "繁體中文",
"languageEn": "English",
"selectLanguageTitle": "選擇語言",
"chatStartConversationPrompt": "開始與 我智能體 對話",
"chatStandingOrderDraftLabel": "常駐指令草稿",
"terminalInitMessage": "我智能體 遠端終端機",
"terminalTitle": "遠端終端機",
"tasksPageTitle": "任務",
"standingOrdersTab": "常駐指令",
"noStandingOrdersTitle": "暫無常駐指令",
"standingOrdersHint": "透過 我智能體 對話新增常駐指令",
"agentCallRingingStatus": "我智能體 語音通話",
"agentCallActiveStatus": "我智能體",
"settingsPageTitle": "設置",
"appearanceThemeLabel": "外觀主題",
"languageLabel": "語言",
"pushNotificationsLabel": "推送通知",
"soundLabel": "提示音",
"hapticFeedbackLabel": "震動反饋",
"conversationEngineLabel": "對話引擎",
"ttsVoiceLabel": "語音音色",
"ttsStyleLabel": "語音風格",
"subscriptionLabel": "訂閱與用量",
"changePasswordLabel": "修改密碼",
"tenantLabel": "租戶",
"logoutButton": "退出登入",
"selectThemeTitle": "選擇主題",
"darkModeLabel": "深色模式",
"lightModeLabel": "淺色模式",
"followSystemLabel": "跟隨系統",
"selectVoiceTitle": "選擇語音音色",
"selectEngineTitle": "選擇對話引擎",
"selectStyleTitle": "選擇語音風格",
"defaultStyleLabel": "預設",
"customStyleHint": "例如:用臺灣腔說話,親切自然",
"resetToDefaultButton": "恢復預設",
"editNameDialogTitle": "修改顯示名稱",
"displayNameLabel": "顯示名稱",
"displayNameHint": "輸入新的顯示名稱",
"changePasswordTitle": "修改密碼",
"currentPasswordLabel": "目前密碼",
"newPasswordLabel": "新密碼",
"confirmPasswordLabel": "確認新密碼",
"passwordMismatchError": "兩次輸入的密碼不一致",
"passwordMinLengthError": "新密碼至少6個字元",
"confirmChangeButton": "確認修改",
"passwordChangedMessage": "密碼已修改",
"logoutDialogTitle": "退出登入",
"logoutConfirmMessage": "確定要退出登入嗎?",
"logoutConfirmButton": "退出",
"billingTitle": "訂閱與用量",
"upgradeButton": "升級方案",
"upgradeDialogTitle": "升級方案",
"currentPlanLabel": "目前方案",
"tokenUsageLabel": "本月 Token 用量",
"serversPageTitle": "伺服器",
"noServersTitle": "未找到伺服器",
"ipAddressLabel": "IP 位址",
"cloudProviderLabel": "雲端廠商",
"createdAtLabel": "建立時間",
"notificationInboxTitle": "站內訊息",
"notificationMarkAllRead": "全部已讀",
"noMessagesTitle": "暫無訊息",
"notificationPreferencesTitle": "通知偏好設定",
"notificationPreferencesInfo": "您可以選擇接收哪些類型的通知。強制通知(如安全告警)無法關閉。",
"mandatoryNotificationsSection": "重要通知(不可關閉)",
"savePreferencesButton": "儲存偏好設定",
"preferencesSavedMessage": "通知偏好已儲存",
"referralScreenTitle": "邀請有禮",
"yourReferralCodeLabel": "你的推薦碼",
"copyReferralCodeTooltip": "複製推薦碼",
"copyInviteLinkButton": "複製邀請連結",
"referralRecordsSection": "推薦記錄",
"pendingRewardsSection": "待領積分",
"rewardRulesTitle": "獎勵規則",
"copiedToClipboard": "已複製到剪貼簿",
"profileSubscriptionLabel": "訂閱方案與用量",
"profileReferralLabel": "邀請有禮",
"profileReferralHint": "推薦賺積分",
"profileInSiteMessagesLabel": "站內訊息",
"profileViewMessagesLabel": "查看訊息"
}

View File

@ -454,6 +454,11 @@ packages:
url: "https://pub.dev"
source: hosted
version: "8.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_math_fork:
dependency: transitive
description:
@ -756,10 +761,10 @@ packages:
dependency: "direct main"
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.19.0"
version: "0.20.2"
io:
dependency: transitive
description:

View File

@ -9,6 +9,8 @@ environment:
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
cupertino_icons: ^1.0.8
# State Management
@ -58,7 +60,7 @@ dependencies:
xterm: ^4.0.0
# Utils
intl: ^0.19.0
intl: ^0.20.2
logger: ^2.2.0
uuid: ^4.3.0
url_launcher: ^6.2.0
@ -100,6 +102,7 @@ flutter_launcher_icons:
generate: false
flutter:
generate: true
uses-material-design: true
assets: