From c592cc78542f8f9089b186eeb521cba6bdba1060 Mon Sep 17 00:00:00 2001 From: qzl Date: Fri, 27 Mar 2026 14:05:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(apps):=20=E9=87=8D=E6=9E=84=20UI=20?= =?UTF-8?q?=E6=9E=B6=E6=9E=84=E4=B8=BA=20presentation=20=E5=B1=82=E5=B9=B6?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20l10n=20=E5=9B=BD=E9=99=85=E5=8C=96?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/AGENTS.md | 14 + apps/l10n.yaml | 5 + apps/lib/app/app.dart | 47 + apps/lib/{core => app}/di/injection.dart | 26 +- .../router/app_route_observer.dart | 0 apps/lib/{core => app}/router/app_router.dart | 56 +- apps/lib/{core => app}/router/app_routes.dart | 0 .../router/go_router_refresh_stream.dart | 0 .../startup/auth_session_bootstrapper.dart | 2 +- apps/lib/core/l10n/l10n.dart | 17 + .../lib/core/{api => network}/api_client.dart | 0 .../core/{api => network}/api_exception.dart | 101 +- .../{api => network}/api_interceptor.dart | 0 apps/lib/core/network/error_code_mapper.dart | 244 ++ .../core/{api => network}/i_api_client.dart | 0 .../utils/phone_display_formatter.dart | 0 .../utils/tool_name_localizer.dart | 26 +- .../{shared => core}/utils/validators.dart | 18 +- apps/lib/features/auth/data/auth_api.dart | 2 +- .../auth/presentation/cubits/login_cubit.dart | 11 +- .../screens/auth_boot_screen.dart | 0 .../screens/login_screen.dart | 36 +- .../widgets/auth_field.dart | 0 .../widgets/auth_page_scaffold.dart | 4 +- .../widgets/password_field.dart | 5 +- .../features/calendar/data/calendar_api.dart | 2 +- .../data/models/schedule_item_model.dart | 10 +- .../data/services/calendar_service.dart | 4 +- .../calendar_state_manager.dart | 0 .../calendar_time_utils.dart | 0 .../dayweek/day_event_layout_engine.dart | 0 .../dayweek/day_timeline_metrics.dart | 0 .../dayweek/day_view_scale.dart | 0 .../screens/calendar_dayweek_screen.dart | 26 +- .../screens/calendar_event_create_screen.dart | 0 .../screens/calendar_event_detail_screen.dart | 126 +- .../screens/calendar_event_edit_screen.dart | 11 +- .../screens/calendar_event_share_screen.dart | 13 +- .../screens/calendar_month_screen.dart | 42 +- .../utils/event_color_resolver.dart | 0 .../widgets/bottom_dock.dart | 0 .../widgets/calendar_share_dialog.dart | 56 +- .../widgets/create_event_sheet.dart | 149 +- .../widgets/date_time_picker_sheet.dart | 37 +- .../chat/data/models/tool_result.dart | 10 +- .../chat/data/services/ag_ui_service.dart | 6 +- .../presentation/bloc/ag_ui_event_label.dart | 20 + .../chat/presentation/bloc/agent_stage.dart | 10 +- .../chat/presentation/bloc/chat_bloc.dart | 11 +- .../data/friends_api.dart | 2 +- .../data/users}/models/user_response.dart | 0 .../data/users}/users_api.dart | 2 +- .../data/users}/users_repository.dart | 0 .../data/users}/users_repository_impl.dart | 0 .../screens/add_contact_screen.dart | 38 +- .../screens/contacts_screen.dart | 101 +- .../features/home/data/voice_recorder.dart | 10 +- .../home_keyboard_inset_calculator.dart | 0 .../home_message_viewport_controller.dart | 0 .../home_viewport_coordinator.dart | 0 .../navigation/home_return_policy.dart | 2 +- .../screens/home_screen.dart | 21 +- .../screens/home_screen_interactions.dart | 18 +- .../screens/home_sheet.dart | 5 +- .../widgets/home_attachment_strip.dart | 0 .../widgets/home_background_field.dart | 0 .../widgets/home_chat_item_renderer.dart | 20 +- .../widgets/home_composer_stack.dart | 27 +- .../widgets/home_conversation_chrome.dart | 18 +- .../widgets/home_floating_header.dart | 0 .../widgets/home_input_host.dart | 0 .../widgets/home_recording_overlay.dart | 5 +- .../widgets/home_unread_badge.dart | 3 +- .../lib/features/messages/data/inbox_api.dart | 2 +- .../screens/message_invite_detail_screen.dart | 101 +- .../screens/message_invite_list_screen.dart | 104 +- .../widgets/calendar_message_card.dart | 48 +- .../widgets/message_action_sheet.dart | 5 +- .../ios_notification_payload_bridge.dart | 2 +- .../services}/local_notification_service.dart | 22 +- .../reminder_notification_callbacks.dart | 2 +- .../domain}/models/reminder_action.dart | 0 .../domain}/models/reminder_payload.dart | 0 .../services}/reminder_action_executor.dart | 8 +- .../services}/reminder_queue_manager.dart | 2 +- .../widgets}/reminder_overlay.dart | 16 +- .../settings/data/models/memory_models.dart | 20 +- .../data/services/automation_jobs_api.dart | 2 +- .../data/services/memory_service.dart | 2 +- .../data/services/settings_user_cache.dart | 2 +- .../user_profile_cache_repository.dart | 2 +- .../features/settings/data/settings_api.dart | 2 +- .../screens/edit_profile_screen.dart | 77 +- .../screens/features_screen.dart | 31 +- .../screens/job_detail_screen.dart | 216 +- .../screens/memory_screen.dart | 57 +- .../screens/settings_screen.dart | 114 +- .../screens/user_memory_detail_screen.dart | 126 +- .../screens/user_memory_view_screen.dart | 139 +- .../screens/work_memory_detail_screen.dart | 112 +- .../screens/work_memory_view_screen.dart | 129 +- .../widgets/account_section_card.dart | 0 .../widgets/settings_page_scaffold.dart | 0 apps/lib/features/todo/data/todo_api.dart | 2 +- .../screens/todo_detail_screen.dart | 68 +- .../screens/todo_edit_screen.dart | 71 +- .../screens/todo_quadrants_screen.dart | 42 +- .../widgets/todo_drag_item.dart | 0 .../ui_schema/domain/models}/ui_schema.dart | 0 .../domain/models}/ui_schema/actions.dart | 0 .../domain/models}/ui_schema/builders.dart | 0 .../models}/ui_schema/common_types.dart | 0 .../domain/models}/ui_schema/document.dart | 0 .../domain/models}/ui_schema/enums.dart | 0 .../domain/models}/ui_schema/nodes.dart | 0 .../navigation/ui_schema_navigation.dart | 0 .../widgets/ui_schema_renderer.dart | 42 +- apps/lib/l10n/app_en.arb | 775 ++++ apps/lib/l10n/app_localizations.dart | 3445 +++++++++++++++++ apps/lib/l10n/app_localizations_en.dart | 1840 +++++++++ apps/lib/l10n/app_localizations_zh.dart | 1793 +++++++++ apps/lib/l10n/app_zh.arb | 775 ++++ apps/lib/main.dart | 230 +- .../forms/inputs.dart} | 21 +- .../widgets/app_pull_refresh_feedback.dart | 8 +- .../shared/widgets/app_selection_sheet.dart | 3 +- .../lib/shared/widgets/banner/app_banner.dart | 2 +- apps/lib/shared/widgets/chat_bubble.dart | 7 +- apps/lib/shared/widgets/confirm_sheet.dart | 12 +- .../widgets/destructive_action_sheet.dart | 3 +- .../shared/widgets/error_retry_surface.dart | 3 +- apps/lib/shared/widgets/message_composer.dart | 35 +- apps/lib/shared/widgets/toast/toast.dart | 2 +- .../widgets/toast/toast_type_config.dart | 72 +- apps/pubspec.yaml | 5 +- apps/test/core/api/api_exception_test.dart | 52 - apps/test/core/api/api_interceptor_test.dart | 143 - .../core/cache/cache_invalidator_test.dart | 11 - apps/test/core/cache/cache_policy_test.dart | 19 - .../cache/cache_refresh_coordinator_test.dart | 27 - .../core/cache/hybrid_cache_store_test.dart | 27 - .../ios_notification_payload_bridge_test.dart | 44 - apps/test/core/router/app_routes_test.dart | 16 - apps/test/core/schemas/ui_schema_test.dart | 105 - .../test/core/storage/token_storage_test.dart | 44 - .../auth/data/auth_repository_test.dart | 125 - .../auth/data/models/auth_models_test.dart | 55 - .../presentation/bloc/auth_bloc_test.dart | 152 - .../presentation/cubits/login_cubit_test.dart | 94 - .../services/calendar_repository_test.dart | 60 - .../models/reminder_payload_test.dart | 42 - .../reminder_action_executor_test.dart | 120 - .../reminder_notification_callbacks_test.dart | 142 - .../reminders/reminder_overlay_test.dart | 106 - .../reminder_queue_manager_test.dart | 60 - apps/test/features/chat/ag_ui_event_test.dart | 109 - .../data/services/ag_ui_service_test.dart | 346 -- .../agent_stage_mapping_test.dart | 34 - .../chat_bloc_attachment_sync_test.dart | 358 -- .../chat/ui_schema_navigation_test.dart | 29 - .../chat/ui_schema_renderer_test.dart | 277 -- .../home_keyboard_inset_calculator_test.dart | 36 - ...home_message_viewport_controller_test.dart | 197 - .../navigation/home_return_policy_test.dart | 35 - .../widgets/home_chat_item_renderer_test.dart | 42 - .../ui/widgets/home_screen_layout_test.dart | 452 --- .../models/automation_job_model_test.dart | 130 - .../services/settings_user_cache_test.dart | 69 - .../user_profile_cache_repository_test.dart | 47 - .../cubits/automation_jobs_cubit_test.dart | 167 - .../cubits/job_detail_cubit_test.dart | 244 -- .../ui/screens/settings_screen_test.dart | 153 - .../features/todo/quadrant_drag_test.dart | 176 - .../features/todo/todo_repository_test.dart | 57 - ...oid_manifest_notification_action_test.dart | 20 - ...p_delegate_notification_callback_test.dart | 22 - .../utils/phone_display_formatter_test.dart | 30 - .../utils/tool_name_localizer_test.dart | 20 - 178 files changed, 10748 insertions(+), 5764 deletions(-) create mode 100644 apps/l10n.yaml create mode 100644 apps/lib/app/app.dart rename apps/lib/{core => app}/di/injection.dart (88%) rename apps/lib/{core => app}/router/app_route_observer.dart (100%) rename apps/lib/{core => app}/router/app_router.dart (74%) rename apps/lib/{core => app}/router/app_routes.dart (100%) rename apps/lib/{core => app}/router/go_router_refresh_stream.dart (100%) rename apps/lib/{core => app}/startup/auth_session_bootstrapper.dart (93%) create mode 100644 apps/lib/core/l10n/l10n.dart rename apps/lib/core/{api => network}/api_client.dart (100%) rename apps/lib/core/{api => network}/api_exception.dart (50%) rename apps/lib/core/{api => network}/api_interceptor.dart (100%) create mode 100644 apps/lib/core/network/error_code_mapper.dart rename apps/lib/core/{api => network}/i_api_client.dart (100%) rename apps/lib/{shared => core}/utils/phone_display_formatter.dart (100%) rename apps/lib/{shared => core}/utils/tool_name_localizer.dart (55%) rename apps/lib/{shared => core}/utils/validators.dart (60%) rename apps/lib/features/auth/{ui => presentation}/screens/auth_boot_screen.dart (100%) rename apps/lib/features/auth/{ui => presentation}/screens/login_screen.dart (91%) rename apps/lib/features/auth/{ui => presentation}/widgets/auth_field.dart (100%) rename apps/lib/features/auth/{ui => presentation}/widgets/auth_page_scaffold.dart (99%) rename apps/lib/features/auth/{ui => presentation}/widgets/password_field.dart (87%) rename apps/lib/features/calendar/{ui => presentation}/calendar_state_manager.dart (100%) rename apps/lib/features/calendar/{ui => presentation}/calendar_time_utils.dart (100%) rename apps/lib/features/calendar/{ui => presentation}/dayweek/day_event_layout_engine.dart (100%) rename apps/lib/features/calendar/{ui => presentation}/dayweek/day_timeline_metrics.dart (100%) rename apps/lib/features/calendar/{ui => presentation}/dayweek/day_view_scale.dart (100%) rename apps/lib/features/calendar/{ui => presentation}/screens/calendar_dayweek_screen.dart (97%) rename apps/lib/features/calendar/{ui => presentation}/screens/calendar_event_create_screen.dart (100%) rename apps/lib/features/calendar/{ui => presentation}/screens/calendar_event_detail_screen.dart (83%) rename apps/lib/features/calendar/{ui => presentation}/screens/calendar_event_edit_screen.dart (85%) rename apps/lib/features/calendar/{ui => presentation}/screens/calendar_event_share_screen.dart (85%) rename apps/lib/features/calendar/{ui => presentation}/screens/calendar_month_screen.dart (93%) rename apps/lib/features/calendar/{ui => presentation}/utils/event_color_resolver.dart (100%) rename apps/lib/features/calendar/{ui => presentation}/widgets/bottom_dock.dart (100%) rename apps/lib/features/calendar/{ui => presentation}/widgets/calendar_share_dialog.dart (79%) rename apps/lib/features/calendar/{ui => presentation}/widgets/create_event_sheet.dart (85%) rename apps/lib/features/calendar/{ui => presentation}/widgets/date_time_picker_sheet.dart (93%) create mode 100644 apps/lib/features/chat/presentation/bloc/ag_ui_event_label.dart rename apps/lib/features/{friends => contacts}/data/friends_api.dart (98%) rename apps/lib/features/{users/data => contacts/data/users}/models/user_response.dart (100%) rename apps/lib/features/{users/data => contacts/data/users}/users_api.dart (97%) rename apps/lib/features/{users/data => contacts/data/users}/users_repository.dart (100%) rename apps/lib/features/{users/data => contacts/data/users}/users_repository_impl.dart (100%) rename apps/lib/features/contacts/{ui => presentation}/screens/add_contact_screen.dart (82%) rename apps/lib/features/contacts/{ui => presentation}/screens/contacts_screen.dart (90%) rename apps/lib/features/home/{ui => presentation}/controllers/home_keyboard_inset_calculator.dart (100%) rename apps/lib/features/home/{ui => presentation}/controllers/home_message_viewport_controller.dart (100%) rename apps/lib/features/home/{ui => presentation}/controllers/home_viewport_coordinator.dart (100%) rename apps/lib/features/home/{ui => presentation}/navigation/home_return_policy.dart (95%) rename apps/lib/features/home/{ui => presentation}/screens/home_screen.dart (97%) rename apps/lib/features/home/{ui => presentation}/screens/home_screen_interactions.dart (93%) rename apps/lib/features/home/{ui => presentation}/screens/home_sheet.dart (96%) rename apps/lib/features/home/{ui => presentation}/widgets/home_attachment_strip.dart (100%) rename apps/lib/features/home/{ui => presentation}/widgets/home_background_field.dart (100%) rename apps/lib/features/home/{ui => presentation}/widgets/home_chat_item_renderer.dart (94%) rename apps/lib/features/home/{ui => presentation}/widgets/home_composer_stack.dart (88%) rename apps/lib/features/home/{ui => presentation}/widgets/home_conversation_chrome.dart (83%) rename apps/lib/features/home/{ui => presentation}/widgets/home_floating_header.dart (100%) rename apps/lib/features/home/{ui => presentation}/widgets/home_input_host.dart (100%) rename apps/lib/features/home/{ui => presentation}/widgets/home_recording_overlay.dart (95%) rename apps/lib/features/home/{ui => presentation}/widgets/home_unread_badge.dart (92%) rename apps/lib/features/messages/{ui => presentation}/screens/message_invite_detail_screen.dart (81%) rename apps/lib/features/messages/{ui => presentation}/screens/message_invite_list_screen.dart (84%) rename apps/lib/features/messages/{ui => presentation}/widgets/calendar_message_card.dart (79%) rename apps/lib/features/messages/{ui => presentation}/widgets/message_action_sheet.dart (96%) rename apps/lib/{core/notifications => features/notification/data/services}/ios_notification_payload_bridge.dart (91%) rename apps/lib/{core/notifications => features/notification/data/services}/local_notification_service.dart (93%) rename apps/lib/{core/notifications => features/notification/data/services}/reminder_notification_callbacks.dart (98%) rename apps/lib/features/{calendar/reminders => notification/domain}/models/reminder_action.dart (100%) rename apps/lib/features/{calendar/reminders => notification/domain}/models/reminder_payload.dart (100%) rename apps/lib/features/{calendar/reminders => notification/domain/services}/reminder_action_executor.dart (89%) rename apps/lib/features/{calendar/reminders => notification/domain/services}/reminder_queue_manager.dart (94%) rename apps/lib/features/{calendar/reminders/ui => notification/presentation/widgets}/reminder_overlay.dart (90%) rename apps/lib/features/settings/{ui => presentation}/screens/edit_profile_screen.dart (85%) rename apps/lib/features/settings/{ui => presentation}/screens/features_screen.dart (86%) rename apps/lib/features/settings/{ui => presentation}/screens/job_detail_screen.dart (81%) rename apps/lib/features/settings/{ui => presentation}/screens/memory_screen.dart (90%) rename apps/lib/features/settings/{ui => presentation}/screens/settings_screen.dart (89%) rename apps/lib/features/settings/{ui => presentation}/screens/user_memory_detail_screen.dart (88%) rename apps/lib/features/settings/{ui => presentation}/screens/user_memory_view_screen.dart (78%) rename apps/lib/features/settings/{ui => presentation}/screens/work_memory_detail_screen.dart (88%) rename apps/lib/features/settings/{ui => presentation}/screens/work_memory_view_screen.dart (79%) rename apps/lib/features/settings/{ui => presentation}/widgets/account_section_card.dart (100%) rename apps/lib/features/settings/{ui => presentation}/widgets/settings_page_scaffold.dart (100%) rename apps/lib/features/todo/{ui => presentation}/screens/todo_detail_screen.dart (84%) rename apps/lib/features/todo/{ui => presentation}/screens/todo_edit_screen.dart (89%) rename apps/lib/features/todo/{ui => presentation}/screens/todo_quadrants_screen.dart (94%) rename apps/lib/features/todo/{ui => presentation}/widgets/todo_drag_item.dart (100%) rename apps/lib/{core/schemas => features/ui_schema/domain/models}/ui_schema.dart (100%) rename apps/lib/{core/schemas => features/ui_schema/domain/models}/ui_schema/actions.dart (100%) rename apps/lib/{core/schemas => features/ui_schema/domain/models}/ui_schema/builders.dart (100%) rename apps/lib/{core/schemas => features/ui_schema/domain/models}/ui_schema/common_types.dart (100%) rename apps/lib/{core/schemas => features/ui_schema/domain/models}/ui_schema/document.dart (100%) rename apps/lib/{core/schemas => features/ui_schema/domain/models}/ui_schema/enums.dart (100%) rename apps/lib/{core/schemas => features/ui_schema/domain/models}/ui_schema/nodes.dart (100%) rename apps/lib/features/{chat/ui => ui_schema/presentation}/navigation/ui_schema_navigation.dart (100%) rename apps/lib/features/{chat/ui => ui_schema/presentation}/widgets/ui_schema_renderer.dart (92%) create mode 100644 apps/lib/l10n/app_en.arb create mode 100644 apps/lib/l10n/app_localizations.dart create mode 100644 apps/lib/l10n/app_localizations_en.dart create mode 100644 apps/lib/l10n/app_localizations_zh.dart create mode 100644 apps/lib/l10n/app_zh.arb rename apps/lib/{core/form_inputs/form_inputs.dart => shared/forms/inputs.dart} (61%) delete mode 100644 apps/test/core/api/api_exception_test.dart delete mode 100644 apps/test/core/api/api_interceptor_test.dart delete mode 100644 apps/test/core/cache/cache_invalidator_test.dart delete mode 100644 apps/test/core/cache/cache_policy_test.dart delete mode 100644 apps/test/core/cache/cache_refresh_coordinator_test.dart delete mode 100644 apps/test/core/cache/hybrid_cache_store_test.dart delete mode 100644 apps/test/core/notifications/ios_notification_payload_bridge_test.dart delete mode 100644 apps/test/core/router/app_routes_test.dart delete mode 100644 apps/test/core/schemas/ui_schema_test.dart delete mode 100644 apps/test/core/storage/token_storage_test.dart delete mode 100644 apps/test/features/auth/data/auth_repository_test.dart delete mode 100644 apps/test/features/auth/data/models/auth_models_test.dart delete mode 100644 apps/test/features/auth/presentation/bloc/auth_bloc_test.dart delete mode 100644 apps/test/features/auth/presentation/cubits/login_cubit_test.dart delete mode 100644 apps/test/features/calendar/data/services/calendar_repository_test.dart delete mode 100644 apps/test/features/calendar/reminders/models/reminder_payload_test.dart delete mode 100644 apps/test/features/calendar/reminders/reminder_action_executor_test.dart delete mode 100644 apps/test/features/calendar/reminders/reminder_notification_callbacks_test.dart delete mode 100644 apps/test/features/calendar/reminders/reminder_overlay_test.dart delete mode 100644 apps/test/features/calendar/reminders/reminder_queue_manager_test.dart delete mode 100644 apps/test/features/chat/ag_ui_event_test.dart delete mode 100644 apps/test/features/chat/data/services/ag_ui_service_test.dart delete mode 100644 apps/test/features/chat/presentation/agent_stage_mapping_test.dart delete mode 100644 apps/test/features/chat/presentation/chat_bloc_attachment_sync_test.dart delete mode 100644 apps/test/features/chat/ui_schema_navigation_test.dart delete mode 100644 apps/test/features/chat/ui_schema_renderer_test.dart delete mode 100644 apps/test/features/home/ui/controllers/home_keyboard_inset_calculator_test.dart delete mode 100644 apps/test/features/home/ui/controllers/home_message_viewport_controller_test.dart delete mode 100644 apps/test/features/home/ui/navigation/home_return_policy_test.dart delete mode 100644 apps/test/features/home/ui/widgets/home_chat_item_renderer_test.dart delete mode 100644 apps/test/features/home/ui/widgets/home_screen_layout_test.dart delete mode 100644 apps/test/features/settings/data/models/automation_job_model_test.dart delete mode 100644 apps/test/features/settings/data/services/settings_user_cache_test.dart delete mode 100644 apps/test/features/settings/data/services/user_profile_cache_repository_test.dart delete mode 100644 apps/test/features/settings/presentation/cubits/automation_jobs_cubit_test.dart delete mode 100644 apps/test/features/settings/presentation/cubits/job_detail_cubit_test.dart delete mode 100644 apps/test/features/settings/ui/screens/settings_screen_test.dart delete mode 100644 apps/test/features/todo/quadrant_drag_test.dart delete mode 100644 apps/test/features/todo/todo_repository_test.dart delete mode 100644 apps/test/platform/android_manifest_notification_action_test.dart delete mode 100644 apps/test/platform/ios_app_delegate_notification_callback_test.dart delete mode 100644 apps/test/shared/utils/phone_display_formatter_test.dart delete mode 100644 apps/test/shared/utils/tool_name_localizer_test.dart diff --git a/apps/AGENTS.md b/apps/AGENTS.md index 5b66a4d..0fbffe7 100644 --- a/apps/AGENTS.md +++ b/apps/AGENTS.md @@ -8,6 +8,12 @@ This file governs `apps/**` (Flutter). Keep rules strict, short, and reusable. - If rules conflict, apply the stricter one. - Visual language source of truth: `apps/rules/visual_design_language.md`. +## Flutter Directory Contract (Must) + +- `apps/lib` only allows these second-level directories: `app/`, `core/`, `data/`, `features/`, `shared/`, `l10n/`. +- `apps/lib/main.dart` is the only allowed root entry file. +- Do not add new second-level directories under `apps/lib` without explicit approval. + ## UI Design System (Must) - Use design tokens only from `apps/lib/core/theme/design_tokens.dart` (`AppColors`, `AppSpacing`, `AppRadius`). @@ -33,6 +39,14 @@ This file governs `apps/**` (Flutter). Keep rules strict, short, and reusable. - Lifecycle events are mandatory: `RUN_STARTED` and exactly one of `RUN_FINISHED` or `RUN_ERROR`. - Text streaming flow must be `TEXT_MESSAGE_START -> TEXT_MESSAGE_CONTENT -> TEXT_MESSAGE_END`. +## HTTP Error Parse Contract (Must) + +- Frontend must parse backend errors as RFC7807: `type/title/status/detail/instance` + extension `code/params`. +- Error code registry single source of truth: `docs/protocols/common/http-error-codes.md`. +- Frontend mapping must be based on documented `code` only (`code -> l10n key`), not inferred from `detail` text. +- Any new/changed code requires protocol doc update first, then frontend mapping update. +- Unknown code fallback order: status-generic localized message -> safe generic localized message. + ## High-Risk Modules (Must) ### Auth diff --git a/apps/l10n.yaml b/apps/l10n.yaml new file mode 100644 index 0000000..7c401d5 --- /dev/null +++ b/apps/l10n.yaml @@ -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 diff --git a/apps/lib/app/app.dart b/apps/lib/app/app.dart new file mode 100644 index 0000000..01fcd17 --- /dev/null +++ b/apps/lib/app/app.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'di/injection.dart'; +import '../core/l10n/l10n.dart'; +import '../core/network/i_api_client.dart'; +import '../l10n/app_localizations.dart'; +import '../features/auth/presentation/bloc/auth_bloc.dart'; +import '../features/auth/presentation/bloc/auth_state.dart'; +import '../features/chat/presentation/bloc/chat_bloc.dart'; +import 'router/app_router.dart'; +import '../core/theme/app_theme.dart'; + +class LinksyApp extends StatelessWidget { + final AuthBloc authBloc; + + const LinksyApp({super.key, required this.authBloc}); + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider.value(value: authBloc), + BlocProvider( + create: (_) => ChatBloc(apiClient: sl()), + ), + ], + child: BlocListener( + listener: (context, state) { + // Handle auth state changes if needed + }, + child: MaterialApp.router( + onGenerateTitle: (context) => AppLocalizations.of(context).appTitle, + debugShowCheckedModeBanner: false, + theme: AppTheme.light, + locale: const Locale('zh'), + supportedLocales: AppLocalizations.supportedLocales, + localizationsDelegates: AppLocalizations.localizationsDelegates, + builder: (context, child) { + L10n.setLocale(Localizations.localeOf(context)); + return child ?? const SizedBox.shrink(); + }, + routerConfig: createAppRouter(authBloc), + ), + ), + ); + } +} diff --git a/apps/lib/core/di/injection.dart b/apps/lib/app/di/injection.dart similarity index 88% rename from apps/lib/core/di/injection.dart rename to apps/lib/app/di/injection.dart index 950cf60..77bb77b 100644 --- a/apps/lib/core/di/injection.dart +++ b/apps/lib/app/di/injection.dart @@ -2,15 +2,15 @@ import 'package:dio/dio.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:get_it/get_it.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import '../cache/cache_invalidator.dart'; -import '../cache/hybrid_cache_store.dart'; -import '../cache/memory_cache_store.dart'; -import '../cache/persistent_cache_store.dart'; -import '../api/api_client.dart'; -import '../api/i_api_client.dart'; -import '../storage/token_storage.dart'; -import '../config/env.dart'; -import '../notifications/local_notification_service.dart'; +import '../../core/cache/cache_invalidator.dart'; +import '../../core/cache/hybrid_cache_store.dart'; +import '../../core/cache/memory_cache_store.dart'; +import '../../core/cache/persistent_cache_store.dart'; +import '../../core/network/api_client.dart'; +import '../../core/network/i_api_client.dart'; +import '../../core/storage/token_storage.dart'; +import '../../core/config/env.dart'; +import '../../features/notification/data/services/local_notification_service.dart'; import '../../features/auth/data/auth_api.dart'; import '../../features/auth/data/auth_repository.dart'; import '../../features/auth/data/auth_repository_impl.dart'; @@ -19,16 +19,16 @@ import '../../features/auth/presentation/bloc/auth_event.dart'; import '../../features/calendar/data/calendar_api.dart'; import '../../features/calendar/data/services/calendar_repository.dart'; import '../../features/calendar/data/services/calendar_service.dart'; -import '../../features/calendar/reminders/reminder_action_executor.dart'; -import '../../features/calendar/ui/calendar_state_manager.dart'; -import '../../features/friends/data/friends_api.dart'; +import '../../features/notification/domain/services/reminder_action_executor.dart'; +import '../../features/calendar/presentation/calendar_state_manager.dart'; +import '../../features/contacts/data/friends_api.dart'; import '../../features/messages/data/inbox_api.dart'; import '../../features/settings/data/settings_api.dart'; import '../../features/settings/data/services/automation_jobs_api.dart'; import '../../features/settings/data/services/settings_user_cache.dart'; import '../../features/settings/data/services/user_profile_cache_repository.dart'; import '../../features/settings/data/services/memory_service.dart'; -import '../../features/users/data/users_api.dart'; +import '../../features/contacts/data/users/users_api.dart'; import '../../features/todo/data/todo_api.dart'; import '../../features/todo/data/todo_repository.dart'; diff --git a/apps/lib/core/router/app_route_observer.dart b/apps/lib/app/router/app_route_observer.dart similarity index 100% rename from apps/lib/core/router/app_route_observer.dart rename to apps/lib/app/router/app_route_observer.dart diff --git a/apps/lib/core/router/app_router.dart b/apps/lib/app/router/app_router.dart similarity index 74% rename from apps/lib/core/router/app_router.dart rename to apps/lib/app/router/app_router.dart index 70f1071..e8025b5 100644 --- a/apps/lib/core/router/app_router.dart +++ b/apps/lib/app/router/app_router.dart @@ -1,35 +1,35 @@ import 'package:go_router/go_router.dart'; import 'app_route_observer.dart'; -import '../../features/auth/presentation/bloc/auth_bloc.dart'; -import '../../features/auth/presentation/bloc/auth_state.dart'; +import '../../../features/auth/presentation/bloc/auth_bloc.dart'; +import '../../../features/auth/presentation/bloc/auth_state.dart'; import 'app_routes.dart'; import 'go_router_refresh_stream.dart'; -import '../../features/auth/ui/screens/auth_boot_screen.dart'; -import '../../features/auth/ui/screens/login_screen.dart'; -import '../../features/home/ui/screens/home_screen.dart'; -import '../../features/messages/ui/screens/message_invite_list_screen.dart'; -import '../../features/messages/ui/screens/message_invite_detail_screen.dart'; -import '../../features/contacts/ui/screens/contacts_screen.dart'; -import '../../features/contacts/ui/screens/add_contact_screen.dart'; -import '../../features/calendar/ui/screens/calendar_dayweek_screen.dart'; -import '../../features/calendar/ui/screens/calendar_month_screen.dart'; -import '../../features/calendar/ui/screens/calendar_event_detail_screen.dart'; -import '../../features/calendar/ui/screens/calendar_event_create_screen.dart'; -import '../../features/calendar/ui/screens/calendar_event_edit_screen.dart'; -import '../../features/calendar/ui/screens/calendar_event_share_screen.dart'; -import '../../features/calendar/ui/calendar_time_utils.dart'; -import '../../features/todo/ui/screens/todo_quadrants_screen.dart'; -import '../../features/todo/ui/screens/todo_detail_screen.dart'; -import '../../features/todo/ui/screens/todo_edit_screen.dart'; -import '../../features/settings/ui/screens/settings_screen.dart'; -import '../../features/settings/ui/screens/features_screen.dart'; -import '../../features/settings/ui/screens/job_detail_screen.dart'; -import '../../features/settings/ui/screens/memory_screen.dart'; -import '../../features/settings/ui/screens/user_memory_view_screen.dart'; -import '../../features/settings/ui/screens/work_memory_view_screen.dart'; -import '../../features/settings/ui/screens/user_memory_detail_screen.dart'; -import '../../features/settings/ui/screens/work_memory_detail_screen.dart'; -import '../../features/settings/ui/screens/edit_profile_screen.dart'; +import '../../../features/auth/presentation/screens/auth_boot_screen.dart'; +import '../../../features/auth/presentation/screens/login_screen.dart'; +import '../../../features/home/presentation/screens/home_screen.dart'; +import '../../../features/messages/presentation/screens/message_invite_list_screen.dart'; +import '../../../features/messages/presentation/screens/message_invite_detail_screen.dart'; +import '../../../features/contacts/presentation/screens/contacts_screen.dart'; +import '../../../features/contacts/presentation/screens/add_contact_screen.dart'; +import '../../../features/calendar/presentation/screens/calendar_dayweek_screen.dart'; +import '../../../features/calendar/presentation/screens/calendar_month_screen.dart'; +import '../../../features/calendar/presentation/screens/calendar_event_detail_screen.dart'; +import '../../../features/calendar/presentation/screens/calendar_event_create_screen.dart'; +import '../../../features/calendar/presentation/screens/calendar_event_edit_screen.dart'; +import '../../../features/calendar/presentation/screens/calendar_event_share_screen.dart'; +import '../../../features/calendar/presentation/calendar_time_utils.dart'; +import '../../../features/todo/presentation/screens/todo_quadrants_screen.dart'; +import '../../../features/todo/presentation/screens/todo_detail_screen.dart'; +import '../../../features/todo/presentation/screens/todo_edit_screen.dart'; +import '../../../features/settings/presentation/screens/settings_screen.dart'; +import '../../../features/settings/presentation/screens/features_screen.dart'; +import '../../../features/settings/presentation/screens/job_detail_screen.dart'; +import '../../../features/settings/presentation/screens/memory_screen.dart'; +import '../../../features/settings/presentation/screens/user_memory_view_screen.dart'; +import '../../../features/settings/presentation/screens/work_memory_view_screen.dart'; +import '../../../features/settings/presentation/screens/user_memory_detail_screen.dart'; +import '../../../features/settings/presentation/screens/work_memory_detail_screen.dart'; +import '../../../features/settings/presentation/screens/edit_profile_screen.dart'; final _homeSecondLevelRoutes = [ AppRoutes.shellHomeBranch, diff --git a/apps/lib/core/router/app_routes.dart b/apps/lib/app/router/app_routes.dart similarity index 100% rename from apps/lib/core/router/app_routes.dart rename to apps/lib/app/router/app_routes.dart diff --git a/apps/lib/core/router/go_router_refresh_stream.dart b/apps/lib/app/router/go_router_refresh_stream.dart similarity index 100% rename from apps/lib/core/router/go_router_refresh_stream.dart rename to apps/lib/app/router/go_router_refresh_stream.dart diff --git a/apps/lib/core/startup/auth_session_bootstrapper.dart b/apps/lib/app/startup/auth_session_bootstrapper.dart similarity index 93% rename from apps/lib/core/startup/auth_session_bootstrapper.dart rename to apps/lib/app/startup/auth_session_bootstrapper.dart index 003fa69..bd8248a 100644 --- a/apps/lib/core/startup/auth_session_bootstrapper.dart +++ b/apps/lib/app/startup/auth_session_bootstrapper.dart @@ -1,6 +1,6 @@ import '../../features/auth/presentation/bloc/auth_state.dart'; import '../../features/calendar/data/services/calendar_service.dart'; -import '../notifications/local_notification_service.dart'; +import '../../features/notification/data/services/local_notification_service.dart'; class AuthSessionBootstrapper { AuthSessionBootstrapper({ diff --git a/apps/lib/core/l10n/l10n.dart b/apps/lib/core/l10n/l10n.dart new file mode 100644 index 0000000..66bc920 --- /dev/null +++ b/apps/lib/core/l10n/l10n.dart @@ -0,0 +1,17 @@ +import 'package:flutter/widgets.dart'; + +import '../../l10n/app_localizations.dart'; + +class L10n { + static Locale _locale = const Locale('zh'); + + static void setLocale(Locale locale) { + _locale = locale; + } + + static AppLocalizations get current => lookupAppLocalizations(_locale); +} + +extension L10nContextX on BuildContext { + AppLocalizations get l10n => AppLocalizations.of(this); +} diff --git a/apps/lib/core/api/api_client.dart b/apps/lib/core/network/api_client.dart similarity index 100% rename from apps/lib/core/api/api_client.dart rename to apps/lib/core/network/api_client.dart diff --git a/apps/lib/core/api/api_exception.dart b/apps/lib/core/network/api_exception.dart similarity index 50% rename from apps/lib/core/api/api_exception.dart rename to apps/lib/core/network/api_exception.dart index d85b8d7..ce0d3e2 100644 --- a/apps/lib/core/api/api_exception.dart +++ b/apps/lib/core/network/api_exception.dart @@ -1,12 +1,21 @@ import 'dart:convert'; import 'package:dio/dio.dart'; +import '../l10n/l10n.dart'; +import 'error_code_mapper.dart'; abstract class ApiException implements Exception { final String message; final int? statusCode; + final String? errorCode; + final Map? errorParams; - const ApiException(this.message, {this.statusCode}); + const ApiException( + this.message, { + this.statusCode, + this.errorCode, + this.errorParams, + }); @override String toString() => message; @@ -19,6 +28,8 @@ abstract class ApiException implements Exception { final data = response?.data; String detail; + String? errorCode; + Map? errorParams; final decodedData = _normalizeErrorData(data); if (decodedData is Map) { @@ -27,26 +38,50 @@ abstract class ApiException implements Exception { decodedData['message'] ?? decodedData['error']) ?.toString() ?? - '请求失败'; + L10n.current.errorRequestFailed; + final code = decodedData['code']; + if (code is String && code.trim().isNotEmpty) { + errorCode = code; + } + final params = decodedData['params']; + if (params is Map) { + errorParams = params; + } else if (params is Map) { + errorParams = params.map( + (key, value) => MapEntry(key.toString(), value), + ); + } } else { detail = _networkErrorMessage(error); } - final localized = _localizeError(detail, statusCode); + final localized = _localizeError( + detail, + statusCode, + errorCode: errorCode, + errorParams: errorParams, + ); if (statusCode == 401) { - return UnauthorizedException(localized); + return UnauthorizedException(message: localized, errorCode: errorCode); } if (statusCode == 422) { return ValidationException( localized, errors: data, statusCode: statusCode, + errorCode: errorCode, + errorParams: errorParams, ); } - return ServerException(localized, statusCode: statusCode); + return ServerException( + localized, + statusCode: statusCode, + errorCode: errorCode, + errorParams: errorParams, + ); } - return const ServerException('网络错误'); + return ServerException(L10n.current.errorNetwork); } static Map? _normalizeErrorData(dynamic data) { @@ -72,52 +107,72 @@ abstract class ApiException implements Exception { return null; } - static String _localizeError(String detail, int? statusCode) { + static String _localizeError( + String detail, + int? statusCode, { + String? errorCode, + Map? errorParams, + }) { + final mapped = resolveErrorCodeMessage(errorCode, params: errorParams); + if (mapped != null && mapped.isNotEmpty) { + return mapped; + } if (statusCode == 403) { - return '没有权限执行此操作'; + return L10n.current.errorForbidden; } if (statusCode == 404) { - return '请求的资源不存在'; + return L10n.current.errorNotFound; } if (statusCode == 429) { - final normalized = detail.trim(); - if (normalized.isEmpty || normalized == '请求失败') { - return '请求过于频繁,请稍后再试'; - } - return detail; + return L10n.current.errorTooManyRequests; } if (statusCode != null && statusCode >= 500) { - return '服务器错误,请稍后再试'; + return L10n.current.errorServer; } - return detail; + return L10n.current.errorGenericSafe; } static String _networkErrorMessage(DioException error) { if (error.type == DioExceptionType.connectionTimeout || error.type == DioExceptionType.sendTimeout || error.type == DioExceptionType.receiveTimeout) { - return '网络超时,请确认手机与服务端在同一网络后重试'; + return L10n.current.errorNetworkTimeout; } if (error.type == DioExceptionType.connectionError || error.type == DioExceptionType.unknown) { - return '无法连接服务器。请在 iPhone 设置中为本应用开启“无线数据(WLAN与蜂窝网络)”,并确认本地网络权限已开启。'; + return L10n.current.errorNetworkUnavailable; } - return '请求失败'; + return L10n.current.errorRequestFailed; } } class ServerException extends ApiException { - const ServerException(super.message, {super.statusCode}); + const ServerException( + super.message, { + super.statusCode, + super.errorCode, + super.errorParams, + }); } class UnauthorizedException extends ApiException { - const UnauthorizedException([super.message = '请重新登录']) - : super(statusCode: 401); + UnauthorizedException({String? message, String? errorCode}) + : super( + message ?? L10n.current.errorReLogin, + statusCode: 401, + errorCode: errorCode, + ); } class ValidationException extends ApiException { final Map? errors; - const ValidationException(super.message, {this.errors, super.statusCode}); + const ValidationException( + super.message, { + this.errors, + super.statusCode, + super.errorCode, + super.errorParams, + }); } diff --git a/apps/lib/core/api/api_interceptor.dart b/apps/lib/core/network/api_interceptor.dart similarity index 100% rename from apps/lib/core/api/api_interceptor.dart rename to apps/lib/core/network/api_interceptor.dart diff --git a/apps/lib/core/network/error_code_mapper.dart b/apps/lib/core/network/error_code_mapper.dart new file mode 100644 index 0000000..27bc02a --- /dev/null +++ b/apps/lib/core/network/error_code_mapper.dart @@ -0,0 +1,244 @@ +import '../l10n/l10n.dart'; + +String? mapErrorCodeToL10nKey( + String? errorCode, { + Map? params, +}) { + if (errorCode == null || errorCode.isEmpty) { + return null; + } + + switch (errorCode) { + case 'AGENT_RUN_INPUT_INVALID': + return 'errorGenericSafe'; + case 'AGENT_RUN_MESSAGES_INVALID': + return 'errorGenericSafe'; + case 'AGENT_INVALID_LAST_EVENT_ID': + return 'errorAgentInvalidLastEventId'; + case 'AGENT_SSE_CONNECTION_LIMIT': + return 'errorAgentSseConnectionLimit'; + case 'AGENT_ATTACHMENT_EMPTY': + return 'errorAgentAttachmentEmpty'; + case 'AGENT_ATTACHMENT_TOO_LARGE': + return 'errorAgentAttachmentTooLarge'; + case 'AGENT_AUDIO_UNSUPPORTED_FORMAT': + return 'errorAgentAudioUnsupportedFormat'; + case 'AGENT_AUDIO_TOO_LARGE': + return 'errorAgentAudioTooLarge'; + case 'AGENT_AUDIO_EMPTY': + return 'errorAgentAudioEmpty'; + case 'AGENT_ASR_UNAVAILABLE': + return 'errorAgentAsrUnavailable'; + case 'AGENT_FORBIDDEN': + return 'errorForbidden'; + case 'AGENT_PAYLOAD_INVALID': + return 'errorGenericSafe'; + case 'AGENT_ATTACHMENTS_TOO_MANY': + return 'errorGenericSafe'; + case 'AGENT_SIGNED_IMAGE_URL_INVALID': + return 'errorGenericSafe'; + case 'AGENT_ATTACHMENT_STORAGE_UNAVAILABLE': + return 'errorServer'; + case 'AGENT_ATTACHMENT_UNSUPPORTED_TYPE': + return 'errorGenericSafe'; + case 'AGENT_ATTACHMENT_UPLOAD_FAILED': + return 'errorGenericSafe'; + case 'AGENT_ATTACHMENT_BUCKET_INVALID': + return 'errorGenericSafe'; + case 'AGENT_ATTACHMENT_PATH_SCOPE_INVALID': + return 'errorGenericSafe'; + case 'AGENT_SIGNED_URL_GENERATION_FAILED': + return 'errorGenericSafe'; + case 'AGENT_SESSION_ID_INVALID': + return 'errorGenericSafe'; + case 'AGENT_SESSION_NOT_FOUND': + return 'errorNotFound'; + case 'AGENT_USER_ID_INVALID': + return 'errorGenericSafe'; + case 'INVALID_BINARY_URL_HOST': + return 'errorAgentInvalidBinaryUrl'; + case 'INVALID_BINARY_URL_BUCKET': + return 'errorAgentInvalidBinaryUrl'; + case 'INVALID_BINARY_URL_PATH_SCOPE': + return 'errorAgentInvalidBinaryUrl'; + case 'AUTH_SERVICE_UNAVAILABLE': + return 'errorServer'; + case 'AUTH_TOO_MANY_REQUESTS': + return 'errorTooManyRequests'; + case 'AUTH_VERIFICATION_CODE_INVALID': + return 'errorGenericSafe'; + case 'AUTH_REFRESH_TOKEN_INVALID': + return 'errorReLogin'; + case 'AUTH_REFRESH_TOKEN_MISSING': + return 'errorReLogin'; + case 'AUTH_USER_NOT_FOUND': + return 'errorNotFound'; + case 'AUTH_UNAUTHORIZED': + return 'errorReLogin'; + case 'JWT_VERIFIER_NOT_CONFIGURED': + return 'errorServer'; + case 'AUTOMATION_JOB_LIMIT_EXCEEDED': + return 'errorGenericSafe'; + case 'AUTOMATION_SYSTEM_JOB_MODIFICATION_FORBIDDEN': + return 'errorForbidden'; + case 'AUTOMATION_JOB_NOT_FOUND': + return 'errorNotFound'; + case 'AUTOMATION_JOB_STORE_UNAVAILABLE': + return 'errorServer'; + case 'NOT_FOUND': + return 'errorNotFound'; + case 'LOOKUP_FAILED': + return 'errorServer'; + case 'INTERNAL_ERROR': + return 'errorServer'; + case 'MISSING_RUNTIME_ARGS': + return 'errorGenericSafe'; + case 'TOOL_PENDING_APPROVAL': + return 'errorGenericSafe'; + case 'TOOL_REJECTED': + return 'errorForbidden'; + case 'USER_STORE_UNAVAILABLE': + return 'errorServer'; + case 'USER_NOT_FOUND': + return 'errorNotFound'; + case 'USER_UPDATE_FIELDS_EMPTY': + return 'errorGenericSafe'; + case 'USER_AVATAR_UNSUPPORTED_TYPE': + return 'errorGenericSafe'; + case 'USER_AVATAR_TOO_LARGE': + return 'errorGenericSafe'; + case 'USER_AVATAR_EMPTY': + return 'errorGenericSafe'; + case 'USER_AVATAR_UPLOAD_FAILED': + return 'errorGenericSafe'; + case 'USER_AUTH_LOOKUP_UNAVAILABLE': + return 'errorServer'; + case 'TODO_SERVICE_UNAVAILABLE': + return 'errorServer'; + case 'TODO_NOT_FOUND': + return 'errorNotFound'; + case 'TODO_ACCESS_FORBIDDEN': + return 'errorForbidden'; + case 'TODO_REORDER_DUPLICATE_ID': + return 'errorGenericSafe'; + case 'TODO_STATUS_INVALID': + return 'errorGenericSafe'; + case 'TODO_PRIORITY_INVALID': + return 'errorGenericSafe'; + case 'SCHEDULE_ITEM_INVALID_TIME_RANGE': + return 'errorGenericSafe'; + case 'SCHEDULE_ITEM_STORE_UNAVAILABLE': + return 'errorServer'; + case 'SCHEDULE_ITEM_NOT_FOUND': + return 'errorNotFound'; + case 'SCHEDULE_ITEM_START_AT_TIMEZONE_REQUIRED': + return 'errorGenericSafe'; + case 'SCHEDULE_ITEM_PAGE_INVALID': + return 'errorGenericSafe'; + case 'SCHEDULE_ITEM_PAGE_SIZE_INVALID': + return 'errorGenericSafe'; + case 'SCHEDULE_ITEM_SHARE_FORBIDDEN': + return 'errorForbidden'; + case 'SCHEDULE_ITEM_SHARE_PERMISSION_EXCEEDED': + return 'errorGenericSafe'; + case 'SCHEDULE_ITEM_SUBSCRIPTION_ALREADY_ACTIVE': + return 'errorGenericSafe'; + case 'SCHEDULE_ITEM_INVITE_ALREADY_SUBSCRIBED': + return 'errorGenericSafe'; + case 'SCHEDULE_ITEM_INVITE_ALREADY_PENDING': + return 'errorGenericSafe'; + case 'SCHEDULE_ITEM_AUTH_LOOKUP_UNAVAILABLE': + return 'errorServer'; + case 'SCHEDULE_ITEM_PENDING_INVITE_NOT_FOUND': + return 'errorNotFound'; + case 'SCHEDULE_ITEM_ACCEPT_SUBSCRIPTION_FAILED': + return 'errorGenericSafe'; + case 'SCHEDULE_ITEM_REJECT_SUBSCRIPTION_FAILED': + return 'errorGenericSafe'; + case 'SCHEDULE_ITEM_DATETIME_TIMEZONE_REQUIRED': + return 'errorGenericSafe'; + case 'SCHEDULE_ITEM_DATETIME_REQUIRED': + return 'errorGenericSafe'; + case 'INBOX_MESSAGE_NOT_FOUND': + return 'errorNotFound'; + case 'INBOX_MESSAGE_STORE_UNAVAILABLE': + return 'errorServer'; + case 'MEMORIES_USER_NOT_FOUND': + return 'errorNotFound'; + case 'MEMORIES_WORK_NOT_FOUND': + return 'errorNotFound'; + case 'MEMORIES_SERVICE_UNAVAILABLE': + return 'errorServer'; + case 'FRIEND_REQUEST_SELF_NOT_ALLOWED': + return 'errorGenericSafe'; + case 'FRIEND_ALREADY_ACCEPTED': + return 'errorGenericSafe'; + case 'FRIEND_REQUEST_BLOCKED': + return 'errorGenericSafe'; + case 'FRIEND_REQUEST_ALREADY_SENT': + return 'errorGenericSafe'; + case 'FRIENDSHIP_SERVICE_UNAVAILABLE': + return 'errorServer'; + case 'FRIEND_REQUEST_NOT_FOUND': + return 'errorNotFound'; + case 'FRIEND_REQUEST_FORBIDDEN': + return 'errorForbidden'; + case 'FRIEND_REQUEST_NOT_PENDING': + return 'errorGenericSafe'; + case 'FRIEND_INBOX_MESSAGE_NOT_FOUND': + return 'errorNotFound'; + case 'FRIENDSHIP_DATA_INVALID': + return 'errorGenericSafe'; + case 'FRIENDSHIP_NOT_FOUND': + return 'errorNotFound'; + case 'FRIENDSHIP_REMOVE_REQUIRES_ACCEPTED': + return 'errorGenericSafe'; + default: + return null; + } +} + +String? resolveErrorCodeMessage( + String? errorCode, { + Map? params, +}) { + final key = mapErrorCodeToL10nKey(errorCode, params: params); + if (key == null) { + return null; + } + + switch (key) { + case 'errorAgentSseConnectionLimit': + return L10n.current.errorAgentSseConnectionLimit; + case 'errorAgentAttachmentEmpty': + return L10n.current.errorAgentAttachmentEmpty; + case 'errorAgentAttachmentTooLarge': + return L10n.current.errorAgentAttachmentTooLarge; + case 'errorAgentAudioEmpty': + return L10n.current.errorAgentAudioEmpty; + case 'errorAgentAudioTooLarge': + return L10n.current.errorAgentAudioTooLarge; + case 'errorAgentAudioUnsupportedFormat': + return L10n.current.errorAgentAudioUnsupportedFormat; + case 'errorAgentAsrUnavailable': + return L10n.current.errorAgentAsrUnavailable; + case 'errorAgentInvalidLastEventId': + return L10n.current.errorAgentInvalidLastEventId; + case 'errorAgentInvalidBinaryUrl': + return L10n.current.errorAgentInvalidBinaryUrl; + case 'errorForbidden': + return L10n.current.errorForbidden; + case 'errorNotFound': + return L10n.current.errorNotFound; + case 'errorTooManyRequests': + return L10n.current.errorTooManyRequests; + case 'errorServer': + return L10n.current.errorServer; + case 'errorGenericSafe': + return L10n.current.errorGenericSafe; + case 'errorReLogin': + return L10n.current.errorReLogin; + default: + return null; + } +} diff --git a/apps/lib/core/api/i_api_client.dart b/apps/lib/core/network/i_api_client.dart similarity index 100% rename from apps/lib/core/api/i_api_client.dart rename to apps/lib/core/network/i_api_client.dart diff --git a/apps/lib/shared/utils/phone_display_formatter.dart b/apps/lib/core/utils/phone_display_formatter.dart similarity index 100% rename from apps/lib/shared/utils/phone_display_formatter.dart rename to apps/lib/core/utils/phone_display_formatter.dart diff --git a/apps/lib/shared/utils/tool_name_localizer.dart b/apps/lib/core/utils/tool_name_localizer.dart similarity index 55% rename from apps/lib/shared/utils/tool_name_localizer.dart rename to apps/lib/core/utils/tool_name_localizer.dart index 9238565..5cc0097 100644 --- a/apps/lib/shared/utils/tool_name_localizer.dart +++ b/apps/lib/core/utils/tool_name_localizer.dart @@ -1,11 +1,4 @@ -const Map _toolNameZhMap = { - 'calendar.read': '读取日程', - 'calendar.write': '写入日程', - 'calendar.share': '共享日程', - 'user.lookup': '查找联系人', - 'memory.write': '写入记忆', - 'memory.forget': '清理记忆', -}; +import '../l10n/l10n.dart'; const Map _toolNameAliases = { 'calendar_read': 'calendar.read', @@ -31,5 +24,20 @@ String localizeToolName(String rawName) { return rawName; } final canonical = _toolNameAliases[normalized] ?? normalized; - return _toolNameZhMap[canonical] ?? rawName; + switch (canonical) { + case 'calendar.read': + return L10n.current.toolCalendarRead; + case 'calendar.write': + return L10n.current.toolCalendarWrite; + case 'calendar.share': + return L10n.current.toolCalendarShare; + case 'user.lookup': + return L10n.current.toolUserLookup; + case 'memory.write': + return L10n.current.toolMemoryWrite; + case 'memory.forget': + return L10n.current.toolMemoryForget; + default: + return rawName; + } } diff --git a/apps/lib/shared/utils/validators.dart b/apps/lib/core/utils/validators.dart similarity index 60% rename from apps/lib/shared/utils/validators.dart rename to apps/lib/core/utils/validators.dart index 782dc06..af69adb 100644 --- a/apps/lib/shared/utils/validators.dart +++ b/apps/lib/core/utils/validators.dart @@ -1,40 +1,44 @@ +import '../l10n/l10n.dart'; + class Validators { Validators._(); static String? phone(String? value) { if (value == null || value.isEmpty) { - return '请输入手机号'; + return L10n.current.validatorPhoneRequired; } final phoneRegex = RegExp(r'^\+861[3-9]\d{9}$'); if (!phoneRegex.hasMatch(value)) { - return '请输入有效的 +86 手机号'; + return L10n.current.validatorPhoneInvalid86; } return null; } static String? password(String? value) { if (value == null || value.isEmpty) { - return '请输入密码'; + return L10n.current.validatorPasswordRequired; } if (value.length < 8) { - return '密码至少需要8位'; + return L10n.current.validatorPasswordMin8; } return null; } static String? required(String? value, [String? fieldName]) { if (value == null || value.isEmpty) { - return '请输入${fieldName ?? '内容'}'; + return L10n.current.validatorRequired( + fieldName ?? L10n.current.commonUnknown, + ); } return null; } static String? nickname(String? value) { if (value == null || value.isEmpty) { - return '请输入昵称'; + return L10n.current.validatorNicknameRequired; } if (value.length < 2) { - return '昵称至少需要2个字符'; + return L10n.current.validatorNicknameMin2; } return null; } diff --git a/apps/lib/features/auth/data/auth_api.dart b/apps/lib/features/auth/data/auth_api.dart index 1eb4522..26e3d3f 100644 --- a/apps/lib/features/auth/data/auth_api.dart +++ b/apps/lib/features/auth/data/auth_api.dart @@ -1,4 +1,4 @@ -import 'package:social_app/core/api/i_api_client.dart'; +import 'package:social_app/core/network/i_api_client.dart'; import 'models/signup_request.dart'; import 'models/login_request.dart'; import 'models/auth_response.dart'; diff --git a/apps/lib/features/auth/presentation/cubits/login_cubit.dart b/apps/lib/features/auth/presentation/cubits/login_cubit.dart index 9e828c4..c3cf144 100644 --- a/apps/lib/features/auth/presentation/cubits/login_cubit.dart +++ b/apps/lib/features/auth/presentation/cubits/login_cubit.dart @@ -3,10 +3,11 @@ import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:formz/formz.dart'; import 'package:equatable/equatable.dart'; -import '../../../../core/api/api_exception.dart'; +import '../../../../core/network/api_exception.dart'; +import '../../../../core/l10n/l10n.dart'; import '../../data/auth_repository.dart'; import '../../data/models/auth_response.dart'; -import '../../../../core/form_inputs/form_inputs.dart'; +import '../../../../shared/forms/inputs.dart'; class LoginState extends Equatable { static const defaultDialCode = '+86'; @@ -121,7 +122,7 @@ class LoginCubit extends Cubit { Future sendCode() async { if (!state.phone.isValid) { - emit(state.copyWith(errorMessage: '请输入有效手机号')); + emit(state.copyWith(errorMessage: L10n.current.authInvalidPhone)); return false; } if (!state.canSendCode) { @@ -152,7 +153,9 @@ class LoginCubit extends Cubit { if (isClosed) { return false; } - final message = e is ApiException ? e.message : '验证码发送失败'; + final message = e is ApiException + ? e.message + : L10n.current.authSendCodeFailed; emit(state.copyWith(isSendingCode: false, errorMessage: message)); return false; } diff --git a/apps/lib/features/auth/ui/screens/auth_boot_screen.dart b/apps/lib/features/auth/presentation/screens/auth_boot_screen.dart similarity index 100% rename from apps/lib/features/auth/ui/screens/auth_boot_screen.dart rename to apps/lib/features/auth/presentation/screens/auth_boot_screen.dart diff --git a/apps/lib/features/auth/ui/screens/login_screen.dart b/apps/lib/features/auth/presentation/screens/login_screen.dart similarity index 91% rename from apps/lib/features/auth/ui/screens/login_screen.dart rename to apps/lib/features/auth/presentation/screens/login_screen.dart index 9065c5a..837f1ee 100644 --- a/apps/lib/features/auth/ui/screens/login_screen.dart +++ b/apps/lib/features/auth/presentation/screens/login_screen.dart @@ -4,8 +4,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:formz/formz.dart'; import 'package:go_router/go_router.dart'; -import '../../../../core/di/injection.dart'; -import '../../../../core/router/app_routes.dart'; +import '../../../../app/di/injection.dart'; +import '../../../../app/router/app_routes.dart'; +import '../../../../core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/banner/app_banner.dart'; @@ -85,12 +86,13 @@ class _LoginViewState extends State { } Future _showAgreementDialog() async { + final l10n = context.l10n; return await showConfirmSheet( context, - title: '请先同意协议', - message: '在使用我们的服务之前,请先阅读并同意《用户协议》和《隐私政策》。\n\n只有您同意上述协议,我们才能为您提供服务。', - confirmText: '确认', - cancelText: '取消', + title: l10n.authAgreementTitle, + message: l10n.authAgreementMessage, + confirmText: l10n.commonConfirm, + cancelText: l10n.commonCancel, ); } @@ -101,7 +103,7 @@ class _LoginViewState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ Semantics( - label: '同意用户协议与隐私政策', + label: context.l10n.authAgreementSemantics, checked: _agreedToTerms, button: true, child: InkWell( @@ -143,17 +145,17 @@ class _LoginViewState extends State { text: TextSpan( style: const TextStyle(fontSize: 13, color: AppColors.slate600), children: [ - const TextSpan(text: '我已同意'), + TextSpan(text: context.l10n.authAgreementPrefix), TextSpan( - text: '《用户协议》', + text: context.l10n.authAgreementTerms, style: const TextStyle( color: AppColors.blue600, decoration: TextDecoration.underline, ), ), - const TextSpan(text: '与'), + TextSpan(text: context.l10n.authAgreementAnd), TextSpan( - text: '《隐私政策》', + text: context.l10n.authAgreementPrivacy, style: const TextStyle( color: AppColors.blue600, decoration: TextDecoration.underline, @@ -213,7 +215,7 @@ class _LoginViewState extends State { CrossAxisAlignment.stretch, children: [ AuthField( - hint: '输入手机号', + hint: context.l10n.authPhoneHint, controller: _phoneController, onChanged: (value) { context.read().phoneChanged( @@ -237,7 +239,7 @@ class _LoginViewState extends State { ), SizedBox(height: AppSpacing.lg), AuthField( - hint: '输入验证码', + hint: context.l10n.authCodeHint, controller: _codeController, onChanged: (value) { context.read().codeChanged( @@ -256,7 +258,7 @@ class _LoginViewState extends State { child: LinkButton( text: state.resendCooldownSeconds > 0 ? '${state.resendCooldownSeconds}s' - : '发送验证码', + : context.l10n.authSendCode, onTap: state.canSendCode ? _handleSendCode : null, @@ -276,8 +278,8 @@ class _LoginViewState extends State { ? ToastType.error : ToastType.warning, title: state.errorMessage != null - ? '登录失败' - : '请检查输入', + ? context.l10n.authLoginFailed + : context.l10n.authCheckInput, ), ), ], @@ -285,7 +287,7 @@ class _LoginViewState extends State { ), SizedBox(height: AppSpacing.xxl), AppButton( - text: '登录/注册', + text: context.l10n.authLoginOrRegister, onPressed: state.status == FormzSubmissionStatus.inProgress diff --git a/apps/lib/features/auth/ui/widgets/auth_field.dart b/apps/lib/features/auth/presentation/widgets/auth_field.dart similarity index 100% rename from apps/lib/features/auth/ui/widgets/auth_field.dart rename to apps/lib/features/auth/presentation/widgets/auth_field.dart diff --git a/apps/lib/features/auth/ui/widgets/auth_page_scaffold.dart b/apps/lib/features/auth/presentation/widgets/auth_page_scaffold.dart similarity index 99% rename from apps/lib/features/auth/ui/widgets/auth_page_scaffold.dart rename to apps/lib/features/auth/presentation/widgets/auth_page_scaffold.dart index a3316d0..65de6c7 100644 --- a/apps/lib/features/auth/ui/widgets/auth_page_scaffold.dart +++ b/apps/lib/features/auth/presentation/widgets/auth_page_scaffold.dart @@ -146,8 +146,8 @@ class AuthHeroHeader extends StatelessWidget { borderRadius: BorderRadius.circular(AppRadius.full), child: Image.asset( 'assets/images/logo.png', - width: 58, - height: 58, + width: 88, + height: 88, fit: BoxFit.cover, ), ), diff --git a/apps/lib/features/auth/ui/widgets/password_field.dart b/apps/lib/features/auth/presentation/widgets/password_field.dart similarity index 87% rename from apps/lib/features/auth/ui/widgets/password_field.dart rename to apps/lib/features/auth/presentation/widgets/password_field.dart index c4a7edb..9427c45 100644 --- a/apps/lib/features/auth/ui/widgets/password_field.dart +++ b/apps/lib/features/auth/presentation/widgets/password_field.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import '../../../../core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; import 'auth_field.dart'; @@ -40,7 +41,9 @@ class _PasswordFieldState extends State { onChanged: widget.onChanged, suffixIcon: IconButton( onPressed: _toggleVisibility, - tooltip: _obscured ? '显示密码' : '隐藏密码', + tooltip: _obscured + ? context.l10n.authShowPassword + : context.l10n.authHidePassword, icon: Icon( _obscured ? Icons.visibility_off_rounded : Icons.visibility_rounded, color: AppColors.authInputIcon, diff --git a/apps/lib/features/calendar/data/calendar_api.dart b/apps/lib/features/calendar/data/calendar_api.dart index 3a2e2d2..16b2bb2 100644 --- a/apps/lib/features/calendar/data/calendar_api.dart +++ b/apps/lib/features/calendar/data/calendar_api.dart @@ -1,4 +1,4 @@ -import 'package:social_app/core/api/i_api_client.dart'; +import 'package:social_app/core/network/i_api_client.dart'; import 'models/schedule_item_model.dart'; diff --git a/apps/lib/features/calendar/data/models/schedule_item_model.dart b/apps/lib/features/calendar/data/models/schedule_item_model.dart index 95f15d8..5c3795b 100644 --- a/apps/lib/features/calendar/data/models/schedule_item_model.dart +++ b/apps/lib/features/calendar/data/models/schedule_item_model.dart @@ -20,12 +20,12 @@ class ScheduleItemModel { final DateTime createdAt; final DateTime updatedAt; - static const int PERMISSION_VIEW = 1; - static const int PERMISSION_INVITE = 2; - static const int PERMISSION_EDIT = 4; + static const int permissionView = 1; + static const int permissionInvite = 2; + static const int permissionEdit = 4; - bool get canEdit => isOwner || (permission & PERMISSION_EDIT) != 0; - bool get canInvite => isOwner || (permission & PERMISSION_INVITE) != 0; + bool get canEdit => isOwner || (permission & permissionEdit) != 0; + bool get canInvite => isOwner || (permission & permissionInvite) != 0; bool get canDelete => isOwner; ScheduleItemModel({ diff --git a/apps/lib/features/calendar/data/services/calendar_service.dart b/apps/lib/features/calendar/data/services/calendar_service.dart index 11045ce..61fade5 100644 --- a/apps/lib/features/calendar/data/services/calendar_service.dart +++ b/apps/lib/features/calendar/data/services/calendar_service.dart @@ -1,6 +1,6 @@ -import 'package:social_app/core/api/i_api_client.dart'; +import 'package:social_app/core/network/i_api_client.dart'; import 'package:social_app/core/cache/cache_invalidator.dart'; -import 'package:social_app/core/di/injection.dart'; +import 'package:social_app/app/di/injection.dart'; import '../calendar_api.dart'; import '../models/schedule_item_model.dart'; diff --git a/apps/lib/features/calendar/ui/calendar_state_manager.dart b/apps/lib/features/calendar/presentation/calendar_state_manager.dart similarity index 100% rename from apps/lib/features/calendar/ui/calendar_state_manager.dart rename to apps/lib/features/calendar/presentation/calendar_state_manager.dart diff --git a/apps/lib/features/calendar/ui/calendar_time_utils.dart b/apps/lib/features/calendar/presentation/calendar_time_utils.dart similarity index 100% rename from apps/lib/features/calendar/ui/calendar_time_utils.dart rename to apps/lib/features/calendar/presentation/calendar_time_utils.dart diff --git a/apps/lib/features/calendar/ui/dayweek/day_event_layout_engine.dart b/apps/lib/features/calendar/presentation/dayweek/day_event_layout_engine.dart similarity index 100% rename from apps/lib/features/calendar/ui/dayweek/day_event_layout_engine.dart rename to apps/lib/features/calendar/presentation/dayweek/day_event_layout_engine.dart diff --git a/apps/lib/features/calendar/ui/dayweek/day_timeline_metrics.dart b/apps/lib/features/calendar/presentation/dayweek/day_timeline_metrics.dart similarity index 100% rename from apps/lib/features/calendar/ui/dayweek/day_timeline_metrics.dart rename to apps/lib/features/calendar/presentation/dayweek/day_timeline_metrics.dart diff --git a/apps/lib/features/calendar/ui/dayweek/day_view_scale.dart b/apps/lib/features/calendar/presentation/dayweek/day_view_scale.dart similarity index 100% rename from apps/lib/features/calendar/ui/dayweek/day_view_scale.dart rename to apps/lib/features/calendar/presentation/dayweek/day_view_scale.dart diff --git a/apps/lib/features/calendar/ui/screens/calendar_dayweek_screen.dart b/apps/lib/features/calendar/presentation/screens/calendar_dayweek_screen.dart similarity index 97% rename from apps/lib/features/calendar/ui/screens/calendar_dayweek_screen.dart rename to apps/lib/features/calendar/presentation/screens/calendar_dayweek_screen.dart index 650b6cc..bd1510f 100644 --- a/apps/lib/features/calendar/ui/screens/calendar_dayweek_screen.dart +++ b/apps/lib/features/calendar/presentation/screens/calendar_dayweek_screen.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:lucide_icons/lucide_icons.dart'; -import '../../../../core/router/app_routes.dart'; -import '../../../home/ui/navigation/home_return_policy.dart'; +import '../../../../app/router/app_routes.dart'; +import '../../../home/presentation/navigation/home_return_policy.dart'; -import '../../../../core/di/injection.dart'; +import '../../../../app/di/injection.dart'; +import '../../../../core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_pressable.dart'; import '../../data/models/schedule_item_model.dart'; @@ -36,7 +37,6 @@ class _CalendarDayWeekScreenState extends State static const double _dayItemWidth = 44; static const double _dayItemGap = 12; static const double _minEventTapHeight = 32; - static const List _dayNames = ['日', '一', '二', '三', '四', '五', '六']; final DayEventLayoutEngine _layoutEngine = const DayEventLayoutEngine(); final Map _activePointers = {}; @@ -224,7 +224,10 @@ class _CalendarDayWeekScreenState extends State } Widget _buildHeader() { - final monthLabel = '${_selectedDate.year}年${_selectedDate.month}月'; + final monthLabel = context.l10n.calendarDayWeekMonthYearLabel( + _selectedDate.year, + _selectedDate.month, + ); final isNotToday = !isSameDay(_selectedDate, DateTime.now()); return SizedBox( @@ -298,10 +301,10 @@ class _CalendarDayWeekScreenState extends State borderRadius: BorderRadius.circular(AppRadius.xl), border: Border.all(color: AppColors.messageBtnBorder), ), - child: const Center( + child: Center( child: Text( - '今天', - style: TextStyle( + context.l10n.calendarToday, + style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.slate700, @@ -448,7 +451,7 @@ class _CalendarDayWeekScreenState extends State mainAxisSize: MainAxisSize.min, children: [ Text( - _dayNames[date.weekday % 7], + _weekdayLabel(date), style: TextStyle( fontSize: 11, color: isWeekend ? AppColors.slate400 : AppColors.slate600, @@ -481,6 +484,11 @@ class _CalendarDayWeekScreenState extends State ); } + String _weekdayLabel(DateTime date) { + const labels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + return labels[date.weekday % 7]; + } + Widget _buildTimelineBoard() { final now = DateTime.now(); final showCurrent = shouldShowCurrentMarker(_selectedDate, now); diff --git a/apps/lib/features/calendar/ui/screens/calendar_event_create_screen.dart b/apps/lib/features/calendar/presentation/screens/calendar_event_create_screen.dart similarity index 100% rename from apps/lib/features/calendar/ui/screens/calendar_event_create_screen.dart rename to apps/lib/features/calendar/presentation/screens/calendar_event_create_screen.dart diff --git a/apps/lib/features/calendar/ui/screens/calendar_event_detail_screen.dart b/apps/lib/features/calendar/presentation/screens/calendar_event_detail_screen.dart similarity index 83% rename from apps/lib/features/calendar/ui/screens/calendar_event_detail_screen.dart rename to apps/lib/features/calendar/presentation/screens/calendar_event_detail_screen.dart index 7e4883d..f29bd15 100644 --- a/apps/lib/features/calendar/ui/screens/calendar_event_detail_screen.dart +++ b/apps/lib/features/calendar/presentation/screens/calendar_event_detail_screen.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; import 'package:lucide_icons/lucide_icons.dart'; import 'package:go_router/go_router.dart'; -import '../../../../core/di/injection.dart'; -import '../../../../core/router/app_routes.dart'; -import '../../../../core/notifications/local_notification_service.dart'; +import 'package:social_app/core/l10n/l10n.dart'; +import '../../../../app/di/injection.dart'; +import '../../../../app/router/app_routes.dart'; +import '../../../../features/notification/data/services/local_notification_service.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_loading_indicator.dart'; import '../../../../shared/widgets/back_title_page_header.dart'; @@ -72,7 +73,7 @@ class _CalendarEventDetailScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const BackTitlePageHeader(title: '日程详情'), + BackTitlePageHeader(title: context.l10n.calendarDetailTitle), Expanded( child: Center( child: Padding( @@ -83,8 +84,8 @@ class _CalendarEventDetailScreenState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - const Text( - '未找到该日程', + Text( + context.l10n.calendarDetailNotFoundTitle, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, @@ -92,8 +93,8 @@ class _CalendarEventDetailScreenState extends State { ), ), const SizedBox(height: AppSpacing.sm), - const Text( - '可能已被删除,或你没有访问权限。', + Text( + context.l10n.calendarDetailNotFoundDesc, textAlign: TextAlign.center, style: TextStyle( fontSize: 13, @@ -156,7 +157,7 @@ class _CalendarEventDetailScreenState extends State { Widget _buildHeader(ScheduleItemModel event) { return BackTitlePageHeader( - title: '日程详情', + title: context.l10n.calendarDetailTitle, onBack: () => context.pop(), trailing: _buildHeaderActions(event), ); @@ -168,7 +169,7 @@ class _CalendarEventDetailScreenState extends State { items.add( DetailHeaderActionItem<_CalendarHeaderAction>( value: _CalendarHeaderAction.edit, - label: '编辑', + label: context.l10n.commonEdit, icon: LucideIcons.pencil, ), ); @@ -177,7 +178,7 @@ class _CalendarEventDetailScreenState extends State { items.add( DetailHeaderActionItem<_CalendarHeaderAction>( value: _CalendarHeaderAction.delete, - label: '删除', + label: context.l10n.commonDelete, icon: LucideIcons.trash2, isDestructive: true, ), @@ -187,7 +188,7 @@ class _CalendarEventDetailScreenState extends State { items.add( DetailHeaderActionItem<_CalendarHeaderAction>( value: _CalendarHeaderAction.share, - label: '分享', + label: context.l10n.commonShare, icon: LucideIcons.share2, ), ); @@ -197,7 +198,7 @@ class _CalendarEventDetailScreenState extends State { items.add( DetailHeaderActionItem<_CalendarHeaderAction>( value: _CalendarHeaderAction.archive, - label: '归档', + label: context.l10n.commonArchive, icon: LucideIcons.archive, enabled: !isExpired, ), @@ -313,8 +314,8 @@ class _CalendarEventDetailScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - '时间安排', + Text( + context.l10n.calendarDetailTimeArrangement, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, @@ -341,8 +342,12 @@ class _CalendarEventDetailScreenState extends State { Widget _buildMetaSurface(ScheduleItemModel event) { final startAt = event.startAt; - final dateStr = - '${startAt.year}年${startAt.month}月${startAt.day}日 ${_getWeekday(startAt.weekday)}'; + final dateStr = context.l10n.calendarDetailDateLabel( + startAt.year, + startAt.month, + startAt.day, + _getWeekday(startAt.weekday), + ); final color = resolveEventColor( status: event.status, colorHex: event.metadata?.color, @@ -358,8 +363,8 @@ class _CalendarEventDetailScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - '基础信息', + Text( + context.l10n.calendarDetailBasicInfo, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, @@ -367,21 +372,21 @@ class _CalendarEventDetailScreenState extends State { ), ), const SizedBox(height: AppSpacing.md), - _buildDetailRow('日期', dateStr), + _buildDetailRow(context.l10n.calendarDetailDate, dateStr), const SizedBox(height: AppSpacing.md), _buildDetailRow( - '提醒', + context.l10n.calendarDetailReminder, _formatReminderText(event.metadata?.reminderMinutes), ), const SizedBox(height: AppSpacing.md), Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - const SizedBox( + SizedBox( width: AppSpacing.xxl * 3, child: Text( - '颜色', - style: TextStyle( + context.l10n.calendarDetailColor, + style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.slate500, @@ -421,8 +426,8 @@ class _CalendarEventDetailScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - '补充信息', + Text( + context.l10n.calendarDetailExtraInfo, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, @@ -431,16 +436,22 @@ class _CalendarEventDetailScreenState extends State { ), if (event.metadata?.location?.trim().isNotEmpty ?? false) ...[ const SizedBox(height: AppSpacing.md), - _buildDetailRow('地点', event.metadata!.location!.trim()), + _buildDetailRow( + context.l10n.calendarDetailLocation, + event.metadata!.location!.trim(), + ), ], if (event.description?.trim().isNotEmpty ?? false) ...[ const SizedBox(height: AppSpacing.md), - _buildDetailRow('描述', event.description!.trim()), + _buildDetailRow( + context.l10n.calendarDetailDescription, + event.description!.trim(), + ), ], if (event.metadata?.notes?.trim().isNotEmpty ?? false) ...[ const SizedBox(height: AppSpacing.md), _buildDetailRow( - '备注', + context.l10n.calendarDetailNotes, event.metadata!.notes!.trim(), multiline: true, ), @@ -452,16 +463,25 @@ class _CalendarEventDetailScreenState extends State { String _formatReminderText(int? reminderMinutes) { if (reminderMinutes == null) { - return '无'; + return context.l10n.calendarDetailReminderNone; } if (reminderMinutes == 0) { - return '准时提醒'; + return context.l10n.calendarDetailReminderOnTime; } - return '开始前$reminderMinutes分钟'; + return context.l10n.calendarDetailReminderBeforeMinutes(reminderMinutes); } String _getWeekday(int weekday) { - const weekdays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']; + final l10n = context.l10n; + final weekdays = [ + l10n.calendarWeekdayMon, + l10n.calendarWeekdayTue, + l10n.calendarWeekdayWed, + l10n.calendarWeekdayThu, + l10n.calendarWeekdayFri, + l10n.calendarWeekdaySat, + l10n.calendarWeekdaySun, + ]; return weekdays[weekday - 1]; } @@ -472,9 +492,9 @@ class _CalendarEventDetailScreenState extends State { Future _showDeleteConfirmation() async { final confirmed = await showDestructiveActionSheet( context, - title: '删除日程', - message: '确定要删除这个日程吗?', - confirmText: '确认删除', + title: context.l10n.calendarDetailDeleteTitle, + message: context.l10n.calendarDetailDeleteMessage, + confirmText: context.l10n.calendarDetailDeleteConfirm, ); if (!confirmed) { return; @@ -492,9 +512,9 @@ class _CalendarEventDetailScreenState extends State { Future _archiveEvent() async { final confirmed = await showDestructiveActionSheet( context, - title: '归档日程', - message: '归档后此日程将标记为过期,确定要归档吗?', - confirmText: '确认归档', + title: context.l10n.calendarDetailArchiveTitle, + message: context.l10n.calendarDetailArchiveMessage, + confirmText: context.l10n.calendarDetailArchiveConfirm, ); if (!confirmed) { return; @@ -506,14 +526,22 @@ class _CalendarEventDetailScreenState extends State { } } catch (e) { if (mounted) { - Toast.show(context, '归档失败', type: ToastType.error); + Toast.show( + context, + context.l10n.calendarDetailArchiveFailed, + type: ToastType.error, + ); } } } String _formatRangeLabel(DateTime startAt, DateTime? endAt) { - final startLabel = - '${startAt.month}月${startAt.day}日 ${_getWeekday(startAt.weekday)} ${_formatTime(startAt)}'; + final startLabel = context.l10n.calendarDetailDateTimeShort( + startAt.month, + startAt.day, + _getWeekday(startAt.weekday), + _formatTime(startAt), + ); if (endAt == null) { return startLabel; } @@ -524,9 +552,13 @@ class _CalendarEventDetailScreenState extends State { if (isSameDay) { return '$startLabel - ${_formatTime(endAt)}'; } - final endLabel = - '${endAt.month}月${endAt.day}日 ${_getWeekday(endAt.weekday)} ${_formatTime(endAt)}'; - return '开始: $startLabel\n结束: $endLabel'; + final endLabel = context.l10n.calendarDetailDateTimeShort( + endAt.month, + endAt.day, + _getWeekday(endAt.weekday), + _formatTime(endAt), + ); + return context.l10n.calendarDetailRangeWithStartEnd(startLabel, endLabel); } Widget _buildStatusBadge(ScheduleStatus status) { @@ -548,7 +580,9 @@ class _CalendarEventDetailScreenState extends State { ), ), child: Text( - isArchived ? '已过期' : '启用', + isArchived + ? context.l10n.calendarDetailStatusExpired + : context.l10n.settingsJobStatusEnabled, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w700, diff --git a/apps/lib/features/calendar/ui/screens/calendar_event_edit_screen.dart b/apps/lib/features/calendar/presentation/screens/calendar_event_edit_screen.dart similarity index 85% rename from apps/lib/features/calendar/ui/screens/calendar_event_edit_screen.dart rename to apps/lib/features/calendar/presentation/screens/calendar_event_edit_screen.dart index e43901f..644c285 100644 --- a/apps/lib/features/calendar/ui/screens/calendar_event_edit_screen.dart +++ b/apps/lib/features/calendar/presentation/screens/calendar_event_edit_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; -import '../../../../core/di/injection.dart'; +import '../../../../app/di/injection.dart'; +import '../../../../core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_loading_indicator.dart'; import '../widgets/create_event_sheet.dart'; @@ -50,8 +51,12 @@ class _CalendarEventEditScreenState extends State { } if (_event == null) { - return const Scaffold( - body: SafeArea(child: Center(child: Text('日程不存在或无权限'))), + return Scaffold( + body: SafeArea( + child: Center( + child: Text(context.l10n.calendarEventNoAccessOrMissing), + ), + ), ); } diff --git a/apps/lib/features/calendar/ui/screens/calendar_event_share_screen.dart b/apps/lib/features/calendar/presentation/screens/calendar_event_share_screen.dart similarity index 85% rename from apps/lib/features/calendar/ui/screens/calendar_event_share_screen.dart rename to apps/lib/features/calendar/presentation/screens/calendar_event_share_screen.dart index 5c85d06..1bed1d6 100644 --- a/apps/lib/features/calendar/ui/screens/calendar_event_share_screen.dart +++ b/apps/lib/features/calendar/presentation/screens/calendar_event_share_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; -import '../../../../core/di/injection.dart'; +import '../../../../app/di/injection.dart'; +import '../../../../core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_loading_indicator.dart'; import '../../../../shared/widgets/back_title_page_header.dart'; @@ -52,8 +53,12 @@ class _CalendarEventShareScreenState extends State { final event = _event; if (event == null) { - return const Scaffold( - body: SafeArea(child: Center(child: Text('日程不存在或无权限'))), + return Scaffold( + body: SafeArea( + child: Center( + child: Text(context.l10n.calendarEventNoAccessOrMissing), + ), + ), ); } @@ -63,7 +68,7 @@ class _CalendarEventShareScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const BackTitlePageHeader(title: '分享日程'), + BackTitlePageHeader(title: context.l10n.calendarShareTitle), Expanded( child: CalendarShareDialog( eventId: event.id, diff --git a/apps/lib/features/calendar/ui/screens/calendar_month_screen.dart b/apps/lib/features/calendar/presentation/screens/calendar_month_screen.dart similarity index 93% rename from apps/lib/features/calendar/ui/screens/calendar_month_screen.dart rename to apps/lib/features/calendar/presentation/screens/calendar_month_screen.dart index e9fcec0..9f298ee 100644 --- a/apps/lib/features/calendar/ui/screens/calendar_month_screen.dart +++ b/apps/lib/features/calendar/presentation/screens/calendar_month_screen.dart @@ -2,11 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:go_router/go_router.dart'; import 'package:lucide_icons/lucide_icons.dart'; -import '../../../../core/di/injection.dart'; -import '../../../../core/router/app_routes.dart'; +import 'package:social_app/core/l10n/l10n.dart'; +import '../../../../app/di/injection.dart'; +import '../../../../app/router/app_routes.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_pressable.dart'; -import '../../../home/ui/navigation/home_return_policy.dart'; +import '../../../home/presentation/navigation/home_return_policy.dart'; import '../calendar_state_manager.dart'; import '../calendar_time_utils.dart'; import '../utils/event_color_resolver.dart'; @@ -107,6 +108,7 @@ class _CalendarMonthScreenState extends State } Widget _buildHeader() { + final l10n = context.l10n; final today = DateTime.now(); final isNotToday = !isSameDay(_selectedDate, today); @@ -132,7 +134,7 @@ class _CalendarMonthScreenState extends State switchInCurve: Curves.easeOut, switchOutCurve: Curves.easeOut, child: Text( - '${_currentMonth.month}月', + l10n.calendarMonthHeader(_currentMonth.month), key: ValueKey(_currentMonth.month), style: const TextStyle( fontSize: 22, @@ -163,9 +165,9 @@ class _CalendarMonthScreenState extends State borderRadius: BorderRadius.circular(AppRadius.xl), border: Border.all(color: AppColors.messageBtnBorder), ), - child: const Center( + child: Center( child: Text( - '今天', + l10n.calendarMonthToday, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, @@ -234,7 +236,16 @@ class _CalendarMonthScreenState extends State } Widget _buildWeekdayHeader() { - const weekdays = ['日', '一', '二', '三', '四', '五', '六']; + final l10n = context.l10n; + final List weekdays = [ + l10n.calendarMonthWeekdaySunShort, + l10n.calendarMonthWeekdayMonShort, + l10n.calendarMonthWeekdayTueShort, + l10n.calendarMonthWeekdayWedShort, + l10n.calendarMonthWeekdayThuShort, + l10n.calendarMonthWeekdayFriShort, + l10n.calendarMonthWeekdaySatShort, + ]; return SizedBox( height: 40, child: Padding( @@ -452,6 +463,7 @@ class _CalendarMonthScreenState extends State } void _showMonthPicker() { + final l10n = context.l10n; var selectedYear = _currentMonth.year; var selectedMonth = _currentMonth.month; @@ -472,7 +484,7 @@ class _CalendarMonthScreenState extends State children: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('取消'), + child: Text(l10n.commonCancel), ), TextButton( onPressed: () { @@ -492,7 +504,7 @@ class _CalendarMonthScreenState extends State _calendarManager.setSelectedDate(_selectedDate); _loadMonthEvents(); }, - child: const Text('确定'), + child: Text(l10n.commonConfirm), ), ], ), @@ -512,7 +524,11 @@ class _CalendarMonthScreenState extends State }); }, children: List.generate(20, (index) { - return Center(child: Text('${2020 + index}年')); + return Center( + child: Text( + l10n.calendarMonthYearLabel(2020 + index), + ), + ); }), ), ), @@ -528,7 +544,11 @@ class _CalendarMonthScreenState extends State }); }, children: List.generate(12, (index) { - return Center(child: Text('${index + 1}月')); + return Center( + child: Text( + l10n.calendarMonthHeader(index + 1), + ), + ); }), ), ), diff --git a/apps/lib/features/calendar/ui/utils/event_color_resolver.dart b/apps/lib/features/calendar/presentation/utils/event_color_resolver.dart similarity index 100% rename from apps/lib/features/calendar/ui/utils/event_color_resolver.dart rename to apps/lib/features/calendar/presentation/utils/event_color_resolver.dart diff --git a/apps/lib/features/calendar/ui/widgets/bottom_dock.dart b/apps/lib/features/calendar/presentation/widgets/bottom_dock.dart similarity index 100% rename from apps/lib/features/calendar/ui/widgets/bottom_dock.dart rename to apps/lib/features/calendar/presentation/widgets/bottom_dock.dart diff --git a/apps/lib/features/calendar/ui/widgets/calendar_share_dialog.dart b/apps/lib/features/calendar/presentation/widgets/calendar_share_dialog.dart similarity index 79% rename from apps/lib/features/calendar/ui/widgets/calendar_share_dialog.dart rename to apps/lib/features/calendar/presentation/widgets/calendar_share_dialog.dart index 1139620..b0fbaf8 100644 --- a/apps/lib/features/calendar/ui/widgets/calendar_share_dialog.dart +++ b/apps/lib/features/calendar/presentation/widgets/calendar_share_dialog.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart' hide BackButton; +import 'package:social_app/core/l10n/l10n.dart'; -import '../../../../core/di/injection.dart'; +import '../../../../app/di/injection.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/toast/toast.dart'; @@ -47,7 +48,7 @@ class CalendarShareDialog extends StatefulWidget { class _CalendarShareDialogState extends State { final _phoneController = TextEditingController(); - bool _permissionView = true; + final bool _permissionView = true; bool _permissionEdit = false; bool _permissionInvite = false; bool _isLoading = false; @@ -59,9 +60,14 @@ class _CalendarShareDialogState extends State { } Future _handleShare() async { + final l10n = context.l10n; final phone = _phoneController.text.trim(); if (phone.isEmpty) { - Toast.show(context, '请输入手机号', type: ToastType.error); + Toast.show( + context, + l10n.calendarSharePhoneRequired, + type: ToastType.error, + ); return; } @@ -77,12 +83,20 @@ class _CalendarShareDialogState extends State { invite: _permissionInvite, ); if (mounted) { - Toast.show(context, '邀请已发送', type: ToastType.success); + Toast.show( + context, + l10n.calendarShareInviteSent, + type: ToastType.success, + ); Navigator.of(context).pop(); } } catch (e) { if (mounted) { - Toast.show(context, '发送邀请失败', type: ToastType.error); + Toast.show( + context, + l10n.calendarShareInviteFailed, + type: ToastType.error, + ); } } finally { if (mounted) { @@ -93,6 +107,8 @@ class _CalendarShareDialogState extends State { @override Widget build(BuildContext context) { + final l10n = context.l10n; + return Container( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, @@ -113,8 +129,8 @@ class _CalendarShareDialogState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( - '分享日历', + Text( + l10n.calendarShareTitle, style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), ), IconButton( @@ -129,8 +145,8 @@ class _CalendarShareDialogState extends State { TextField( controller: _phoneController, decoration: InputDecoration( - labelText: '手机号', - hintText: '输入对方的 +86 手机号', + labelText: l10n.calendarSharePhoneLabel, + hintText: l10n.calendarSharePhoneHint, border: OutlineInputBorder( borderRadius: BorderRadius.circular(AppRadius.md), ), @@ -138,20 +154,28 @@ class _CalendarShareDialogState extends State { keyboardType: TextInputType.phone, ), const SizedBox(height: AppSpacing.lg), - const Text('权限设置', style: TextStyle(fontWeight: FontWeight.w600)), + Text( + l10n.calendarSharePermissionTitle, + style: const TextStyle(fontWeight: FontWeight.w600), + ), const SizedBox(height: AppSpacing.sm), - _buildPermissionSwitch('查看', '可以查看此日历事件(必选)', true, null), _buildPermissionSwitch( - '编辑', - '可以编辑此日历事件', + l10n.calendarSharePermissionView, + l10n.calendarSharePermissionViewDesc, + true, + null, + ), + _buildPermissionSwitch( + l10n.calendarSharePermissionEdit, + l10n.calendarSharePermissionEditDesc, _permissionEdit, widget.canEdit ? (v) => setState(() => _permissionEdit = v) : null, ), _buildPermissionSwitch( - '邀请', - '可以邀请其他人', + l10n.calendarSharePermissionInvite, + l10n.calendarSharePermissionInviteDesc, _permissionInvite, widget.canInvite ? (v) => setState(() => _permissionInvite = v) @@ -159,7 +183,7 @@ class _CalendarShareDialogState extends State { ), const SizedBox(height: AppSpacing.lg), AppButton( - text: '发送邀请', + text: l10n.calendarShareSendInvite, onPressed: _isLoading ? null : _handleShare, isLoading: _isLoading, ), diff --git a/apps/lib/features/calendar/ui/widgets/create_event_sheet.dart b/apps/lib/features/calendar/presentation/widgets/create_event_sheet.dart similarity index 85% rename from apps/lib/features/calendar/ui/widgets/create_event_sheet.dart rename to apps/lib/features/calendar/presentation/widgets/create_event_sheet.dart index 9242869..54bfabc 100644 --- a/apps/lib/features/calendar/ui/widgets/create_event_sheet.dart +++ b/apps/lib/features/calendar/presentation/widgets/create_event_sheet.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:lucide_icons/lucide_icons.dart'; -import '../../../../core/di/injection.dart'; -import '../../../../core/notifications/local_notification_service.dart'; +import 'package:social_app/core/l10n/l10n.dart'; +import '../../../../app/di/injection.dart'; +import '../../../../features/notification/data/services/local_notification_service.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_loading_indicator.dart'; import '../../../../shared/widgets/app_selection_sheet.dart'; @@ -204,7 +205,9 @@ class _CreateEventSheetState extends State Widget _buildPageHeader() { return BackTitlePageHeader( - title: _isEditing ? '编辑日程' : '新建日程', + title: _isEditing + ? context.l10n.calendarCreateEditTitle + : context.l10n.calendarCreateNewTitle, onBack: () => Navigator.of(context).pop(), trailing: ValueListenableBuilder( valueListenable: _titleController, @@ -226,7 +229,7 @@ class _CreateEventSheetState extends State trackColor: AppColors.blue200, ) : Text( - '保存', + context.l10n.commonSave, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, @@ -266,7 +269,9 @@ class _CreateEventSheetState extends State ), ), Text( - _isEditing ? '编辑日程' : '新建日程', + _isEditing + ? context.l10n.calendarCreateEditTitle + : context.l10n.calendarCreateNewTitle, style: const TextStyle( fontSize: 17, fontWeight: FontWeight.w600, @@ -295,7 +300,7 @@ class _CreateEventSheetState extends State trackColor: AppColors.blue200, ) : Text( - '保存', + context.l10n.commonSave, style: TextStyle( fontSize: 17, fontWeight: FontWeight.w600, @@ -323,9 +328,9 @@ class _CreateEventSheetState extends State labelColor: AppColors.blue600, unselectedLabelColor: AppColors.slate600, indicatorColor: AppColors.blue600, - tabs: const [ - Tab(text: '基础'), - Tab(text: '进阶'), + tabs: [ + Tab(text: context.l10n.calendarCreateTabBasic), + Tab(text: context.l10n.calendarCreateTabAdvanced), ], ), ); @@ -345,38 +350,47 @@ class _CreateEventSheetState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildTextField('标题', _titleController, '请输入日程标题'), - const SizedBox(height: 20), - _buildDateTimePicker('开始', _startDate, _startTime, (date, time) { - setState(() { - _startDate = date; - _startTime = time; - if (_endDate != null && _endTime != null) { - final endDateTime = DateTime( - _endDate!.year, - _endDate!.month, - _endDate!.day, - _endTime!.hour, - _endTime!.minute, - ); - final startDateTime = DateTime( - date.year, - date.month, - date.day, - time.hour, - time.minute, - ); - if (endDateTime.isBefore(startDateTime) || - endDateTime.isAtSameMomentAs(startDateTime)) { - _endDate = date; - _endTime = time.add(const Duration(hours: 1)); - } - } - }); - }), + _buildTextField( + context.l10n.calendarCreateFieldTitle, + _titleController, + context.l10n.calendarCreateFieldTitleHint, + ), const SizedBox(height: 20), _buildDateTimePicker( - '结束', + context.l10n.calendarCreateFieldStart, + _startDate, + _startTime, + (date, time) { + setState(() { + _startDate = date; + _startTime = time; + if (_endDate != null && _endTime != null) { + final endDateTime = DateTime( + _endDate!.year, + _endDate!.month, + _endDate!.day, + _endTime!.hour, + _endTime!.minute, + ); + final startDateTime = DateTime( + date.year, + date.month, + date.day, + time.hour, + time.minute, + ); + if (endDateTime.isBefore(startDateTime) || + endDateTime.isAtSameMomentAs(startDateTime)) { + _endDate = date; + _endTime = time.add(const Duration(hours: 1)); + } + } + }); + }, + ), + const SizedBox(height: 20), + _buildDateTimePicker( + context.l10n.calendarCreateFieldEnd, _endDate ?? _startDate, _endTime ?? _startTime, (date, time) { @@ -426,15 +440,28 @@ class _CreateEventSheetState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildTextField('描述', _descriptionController, '请输入描述'), + _buildTextField( + context.l10n.calendarCreateFieldDescription, + _descriptionController, + context.l10n.calendarCreateFieldDescriptionHint, + ), const SizedBox(height: 20), - _buildTextField('地点', _locationController, '请输入地点'), + _buildTextField( + context.l10n.calendarCreateFieldLocation, + _locationController, + context.l10n.calendarCreateFieldLocationHint, + ), const SizedBox(height: 20), _buildReminderPicker(), const SizedBox(height: 20), _buildColorPicker(), const SizedBox(height: 20), - _buildTextField('备注', _notesController, '请输入备注', maxLines: 3), + _buildTextField( + context.l10n.calendarDetailNotes, + _notesController, + context.l10n.calendarCreateFieldNotesHint, + maxLines: 3, + ), ], ), ); @@ -468,7 +495,7 @@ class _CreateEventSheetState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - label + (isOptional ? '(可选)' : ''), + isOptional ? context.l10n.calendarCreateOptionalField(label) : label, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, @@ -525,7 +552,13 @@ class _CreateEventSheetState extends State } String _formatDateTimeLabel(DateTime date, DateTime time) { - return '${date.year}年${date.month}月${date.day}日 ${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}'; + return context.l10n.calendarCreateDateTimeLabel( + date.year, + date.month, + date.day, + time.hour.toString().padLeft(2, '0'), + time.minute.toString().padLeft(2, '0'), + ); } Future<(DateTime, DateTime)?> _pickDateTime( @@ -550,8 +583,8 @@ class _CreateEventSheetState extends State return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - '颜色', + Text( + context.l10n.calendarDetailColor, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, @@ -591,19 +624,19 @@ class _CreateEventSheetState extends State Widget _buildReminderPicker() { String labelOf(int? value) { if (value == null) { - return '无提醒'; + return context.l10n.calendarCreateReminderNone; } if (value == 0) { - return '准时提醒'; + return context.l10n.calendarDetailReminderOnTime; } - return '开始前$value分钟'; + return context.l10n.calendarDetailReminderBeforeMinutes(value); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - '提醒时间', + Text( + context.l10n.calendarCreateReminderTime, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, @@ -616,7 +649,7 @@ class _CreateEventSheetState extends State final options = _buildReminderOptions(); final selected = await showAppSelectionSheet( context, - title: '选择提醒时间', + title: context.l10n.calendarCreatePickReminderTime, items: options .map((v) => AppSelectionItem(value: v, label: labelOf(v))) .toList(), @@ -745,7 +778,11 @@ class _CreateEventSheetState extends State await notificationService.upsertEventReminder(saved); } catch (_) { if (mounted) { - Toast.show(context, '提醒创建失败,请检查通知权限', type: ToastType.warning); + Toast.show( + context, + context.l10n.calendarCreateReminderPermissionFailed, + type: ToastType.warning, + ); } } @@ -755,7 +792,11 @@ class _CreateEventSheetState extends State } } catch (e) { if (mounted) { - Toast.show(context, '保存失败: $e', type: ToastType.error); + Toast.show( + context, + context.l10n.todoSaveFailed('$e'), + type: ToastType.error, + ); } } finally { if (mounted) { diff --git a/apps/lib/features/calendar/ui/widgets/date_time_picker_sheet.dart b/apps/lib/features/calendar/presentation/widgets/date_time_picker_sheet.dart similarity index 93% rename from apps/lib/features/calendar/ui/widgets/date_time_picker_sheet.dart rename to apps/lib/features/calendar/presentation/widgets/date_time_picker_sheet.dart index b861e07..d8eef02 100644 --- a/apps/lib/features/calendar/ui/widgets/date_time_picker_sheet.dart +++ b/apps/lib/features/calendar/presentation/widgets/date_time_picker_sheet.dart @@ -1,4 +1,5 @@ import 'package:flutter/cupertino.dart'; +import 'package:social_app/core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; @@ -130,6 +131,8 @@ class _DateTimePickerSheetState extends State { @override Widget build(BuildContext context) { + final l10n = context.l10n; + return Container( height: 420, decoration: const BoxDecoration( @@ -148,7 +151,7 @@ class _DateTimePickerSheetState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _buildPickerLabel('日期'), + _buildPickerLabel(l10n.calendarDateTimePickerDateLabel), Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.center, @@ -165,8 +168,8 @@ class _DateTimePickerSheetState extends State { }); }, (v) => '$v'), ), - const Text( - '年', + Text( + l10n.calendarDateTimePickerYearUnit, style: TextStyle( fontSize: 14, color: AppColors.slate600, @@ -186,8 +189,8 @@ class _DateTimePickerSheetState extends State { }); }, (v) => '$v'), ), - const Text( - '月', + Text( + l10n.calendarDateTimePickerMonthUnit, style: TextStyle( fontSize: 14, color: AppColors.slate600, @@ -201,8 +204,8 @@ class _DateTimePickerSheetState extends State { (v) => '$v', ), ), - const Text( - '日', + Text( + l10n.calendarDateTimePickerDayUnit, style: TextStyle( fontSize: 14, color: AppColors.slate600, @@ -220,7 +223,7 @@ class _DateTimePickerSheetState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _buildPickerLabel('时间'), + _buildPickerLabel(l10n.calendarDateTimePickerTimeLabel), Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -285,6 +288,8 @@ class _DateTimePickerSheetState extends State { } Widget _buildHeader() { + final l10n = context.l10n; + return Container( height: 56, padding: const EdgeInsets.symmetric(horizontal: 16), @@ -297,13 +302,13 @@ class _DateTimePickerSheetState extends State { children: [ GestureDetector( onTap: () => Navigator.pop(context), - child: const Text( - '取消', - style: TextStyle(fontSize: 17, color: AppColors.slate600), + child: Text( + l10n.commonCancel, + style: const TextStyle(fontSize: 17, color: AppColors.slate600), ), ), - const Text( - '选择时间', + Text( + l10n.calendarDateTimePickerTitle, style: TextStyle( fontSize: 17, fontWeight: FontWeight.w600, @@ -317,9 +322,9 @@ class _DateTimePickerSheetState extends State { DateTime(2000, 1, 1, _selectedHour, _selectedMinute), )); }, - child: const Text( - '确定', - style: TextStyle( + child: Text( + l10n.commonConfirm, + style: const TextStyle( fontSize: 17, fontWeight: FontWeight.w600, color: AppColors.blue600, diff --git a/apps/lib/features/chat/data/models/tool_result.dart b/apps/lib/features/chat/data/models/tool_result.dart index 4e051a9..f493482 100644 --- a/apps/lib/features/chat/data/models/tool_result.dart +++ b/apps/lib/features/chat/data/models/tool_result.dart @@ -2,10 +2,10 @@ import 'package:json_annotation/json_annotation.dart'; part 'tool_result.g.dart'; -/// Schema 版本常量 +/// Default schema version. const _defaultSchemaVersion = 'v1'; -/// 工具执行结果(给 AI 的原始数据) +/// Raw tool execution result used by the assistant runtime. @JsonSerializable() class ToolResult { final String? eventId; @@ -20,7 +20,7 @@ class ToolResult { Map toJson() => _$ToolResultToJson(this); } -/// UI 卡片 Schema(给 UI 渲染) +/// UI card schema consumed by frontend renderers. @JsonSerializable() class UiCard { @JsonKey(name: 'type') @@ -44,7 +44,7 @@ class UiCard { Map toJson() => _$UiCardToJson(this); } -/// 卡片操作按钮 +/// Action button metadata for a UI card. @JsonSerializable() class CardAction { final String type; @@ -65,7 +65,7 @@ class CardAction { Map toJson() => _$CardActionToJson(this); } -/// 日历卡片数据 +/// Calendar card payload data. @JsonSerializable() class CalendarCardData { final String id; diff --git a/apps/lib/features/chat/data/services/ag_ui_service.dart b/apps/lib/features/chat/data/services/ag_ui_service.dart index 24177dc..acf940e 100644 --- a/apps/lib/features/chat/data/services/ag_ui_service.dart +++ b/apps/lib/features/chat/data/services/ag_ui_service.dart @@ -5,7 +5,7 @@ import 'dart:typed_data'; import 'package:dio/dio.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:social_app/core/api/i_api_client.dart'; +import 'package:social_app/core/network/i_api_client.dart'; import '../models/ag_ui_event.dart'; @@ -155,8 +155,8 @@ class AgUiService { } bool hasEarlierHistory(DateTime fromDate) { - // 历史是否还有更多由后端 history snapshot 的 hasMore 驱动。 - // 参数保留是为了兼容 ChatBloc 现有调用签名。 + // Whether earlier history exists is driven by backend snapshot.hasMore. + // Keep the parameter for compatibility with the current ChatBloc signature. final _ = fromDate; return _hasMoreHistory; } diff --git a/apps/lib/features/chat/presentation/bloc/ag_ui_event_label.dart b/apps/lib/features/chat/presentation/bloc/ag_ui_event_label.dart new file mode 100644 index 0000000..19aad14 --- /dev/null +++ b/apps/lib/features/chat/presentation/bloc/ag_ui_event_label.dart @@ -0,0 +1,20 @@ +import '../../../../core/l10n/l10n.dart'; +import '../../data/models/ag_ui_event.dart'; + +String agUiEventLabel(AgUiEventType type) { + final l10n = L10n.current; + return switch (type) { + AgUiEventType.runStarted => l10n.agUiEventRunStarted, + AgUiEventType.runFinished => l10n.agUiEventRunFinished, + AgUiEventType.runError => l10n.agUiEventRunError, + AgUiEventType.stepStarted => l10n.agUiEventStepStarted, + AgUiEventType.stepFinished => l10n.agUiEventStepFinished, + AgUiEventType.textMessageEnd => l10n.agUiEventTextMessageEnd, + AgUiEventType.toolCallStart => l10n.agUiEventToolCallStart, + AgUiEventType.toolCallArgs => l10n.agUiEventToolCallArgs, + AgUiEventType.toolCallEnd => l10n.agUiEventToolCallEnd, + AgUiEventType.toolCallResult => l10n.agUiEventToolCallResult, + AgUiEventType.toolCallError => l10n.agUiEventToolCallError, + AgUiEventType.unknown => l10n.agUiEventUnknown, + }; +} diff --git a/apps/lib/features/chat/presentation/bloc/agent_stage.dart b/apps/lib/features/chat/presentation/bloc/agent_stage.dart index b4073bb..5cbfd3c 100644 --- a/apps/lib/features/chat/presentation/bloc/agent_stage.dart +++ b/apps/lib/features/chat/presentation/bloc/agent_stage.dart @@ -1,3 +1,5 @@ +import '../../../../core/l10n/l10n.dart'; + enum AgentStage { routing, execution, memory } AgentStage? stageFromStepName(String value) { @@ -15,9 +17,9 @@ AgentStage? stageFromStepName(String value) { String stageLabel(AgentStage? stage) { return switch (stage) { - AgentStage.routing => '意图识别中', - AgentStage.execution => '任务执行中', - AgentStage.memory => '记忆提取中', - null => '任务处理中', + AgentStage.routing => L10n.current.agentStageRouting, + AgentStage.execution => L10n.current.agentStageExecution, + AgentStage.memory => L10n.current.agentStageMemory, + null => L10n.current.agentStageProcessing, }; } diff --git a/apps/lib/features/chat/presentation/bloc/chat_bloc.dart b/apps/lib/features/chat/presentation/bloc/chat_bloc.dart index 61b75e3..1af8a26 100644 --- a/apps/lib/features/chat/presentation/bloc/chat_bloc.dart +++ b/apps/lib/features/chat/presentation/bloc/chat_bloc.dart @@ -2,7 +2,8 @@ import 'dart:typed_data'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:social_app/core/api/i_api_client.dart'; +import 'package:social_app/core/network/i_api_client.dart'; +import 'package:social_app/core/l10n/l10n.dart'; import '../../data/models/ag_ui_event.dart'; import '../../data/models/chat_list_item.dart'; @@ -130,7 +131,9 @@ class ChatBloc extends Cubit { ).copyWith( items: _markActiveToolCallsFailed( state.items, - reason: isCanceledByUser ? '本次运行已取消' : '本次运行已失败', + reason: isCanceledByUser + ? L10n.current.chatRunCanceled + : L10n.current.chatRunFailed, ), ), ); @@ -428,7 +431,9 @@ class ChatBloc extends Cubit { isCancelling: false, currentStage: null, error: sseClosedBeforeTerminal - ? (recoveredFromHistory ? null : '连接中断,请重试') + ? (recoveredFromHistory + ? null + : L10n.current.chatSseInterruptedRetry) : error.toString(), ), ); diff --git a/apps/lib/features/friends/data/friends_api.dart b/apps/lib/features/contacts/data/friends_api.dart similarity index 98% rename from apps/lib/features/friends/data/friends_api.dart rename to apps/lib/features/contacts/data/friends_api.dart index 02fe182..6f30a0b 100644 --- a/apps/lib/features/friends/data/friends_api.dart +++ b/apps/lib/features/contacts/data/friends_api.dart @@ -1,4 +1,4 @@ -import 'package:social_app/core/api/i_api_client.dart'; +import 'package:social_app/core/network/i_api_client.dart'; class FriendsApi { final IApiClient _client; diff --git a/apps/lib/features/users/data/models/user_response.dart b/apps/lib/features/contacts/data/users/models/user_response.dart similarity index 100% rename from apps/lib/features/users/data/models/user_response.dart rename to apps/lib/features/contacts/data/users/models/user_response.dart diff --git a/apps/lib/features/users/data/users_api.dart b/apps/lib/features/contacts/data/users/users_api.dart similarity index 97% rename from apps/lib/features/users/data/users_api.dart rename to apps/lib/features/contacts/data/users/users_api.dart index e93c4bc..8ca60aa 100644 --- a/apps/lib/features/users/data/users_api.dart +++ b/apps/lib/features/contacts/data/users/users_api.dart @@ -1,6 +1,6 @@ import 'dart:io'; import 'package:dio/dio.dart'; -import 'package:social_app/core/api/i_api_client.dart'; +import 'package:social_app/core/network/i_api_client.dart'; import 'models/user_response.dart'; class UserBasicInfo { diff --git a/apps/lib/features/users/data/users_repository.dart b/apps/lib/features/contacts/data/users/users_repository.dart similarity index 100% rename from apps/lib/features/users/data/users_repository.dart rename to apps/lib/features/contacts/data/users/users_repository.dart diff --git a/apps/lib/features/users/data/users_repository_impl.dart b/apps/lib/features/contacts/data/users/users_repository_impl.dart similarity index 100% rename from apps/lib/features/users/data/users_repository_impl.dart rename to apps/lib/features/contacts/data/users/users_repository_impl.dart diff --git a/apps/lib/features/contacts/ui/screens/add_contact_screen.dart b/apps/lib/features/contacts/presentation/screens/add_contact_screen.dart similarity index 82% rename from apps/lib/features/contacts/ui/screens/add_contact_screen.dart rename to apps/lib/features/contacts/presentation/screens/add_contact_screen.dart index acccdc8..bf31324 100644 --- a/apps/lib/features/contacts/ui/screens/add_contact_screen.dart +++ b/apps/lib/features/contacts/presentation/screens/add_contact_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import '../../../../core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/back_title_page_header.dart'; import '../../../../shared/widgets/app_input.dart'; @@ -42,7 +43,9 @@ class _AddContactScreenState extends State { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ BackTitlePageHeader( - title: isEditing ? '编辑联系人' : '添加联系人', + title: isEditing + ? context.l10n.contactEditTitle + : context.l10n.contactAddTitle, onBack: () => context.pop(), trailing: _buildConfirmButton(), ), @@ -121,18 +124,22 @@ class _AddContactScreenState extends State { ), child: Column( children: [ - AppInput(label: '昵称', hint: '请输入昵称', controller: _nameController), + AppInput( + label: context.l10n.contactNickname, + hint: context.l10n.contactNicknameHint, + controller: _nameController, + ), const SizedBox(height: 14), AppInput( - label: '手机号', - hint: '+86 请输入 11 位手机号', + label: context.l10n.contactPhone, + hint: context.l10n.contactPhoneHint, controller: _phoneController, keyboardType: TextInputType.phone, ), const SizedBox(height: 14), AppInput( - label: '备注', - hint: '请输入备注', + label: context.l10n.contactRemark, + hint: context.l10n.contactRemarkHint, controller: _remarkController, maxLines: 3, ), @@ -145,7 +152,7 @@ class _AddContactScreenState extends State { return Padding( padding: const EdgeInsets.only(bottom: AppSpacing.sm), child: LinkButton( - text: '删除联系人', + text: context.l10n.contactDelete, onTap: _handleDelete, foregroundColor: AppColors.red600, ), @@ -157,7 +164,11 @@ class _AddContactScreenState extends State { final phone = _phoneController.text.trim(); if (name.isEmpty || phone.isEmpty) { - Toast.show(context, '请填写昵称和手机号', type: ToastType.warning); + Toast.show( + context, + context.l10n.contactFillRequired, + type: ToastType.warning, + ); return; } @@ -169,12 +180,12 @@ class _AddContactScreenState extends State { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('删除联系人'), - content: const Text('确定要删除此联系人吗?'), + title: Text(context.l10n.contactDeleteConfirmTitle), + content: Text(context.l10n.contactDeleteConfirmMessage), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('取消'), + child: Text(context.l10n.commonCancel), ), TextButton( onPressed: () { @@ -182,7 +193,10 @@ class _AddContactScreenState extends State { // TODO: Implement delete logic context.pop(); }, - child: const Text('删除', style: TextStyle(color: AppColors.red600)), + child: Text( + context.l10n.commonDelete, + style: const TextStyle(color: AppColors.red600), + ), ), ], ), diff --git a/apps/lib/features/contacts/ui/screens/contacts_screen.dart b/apps/lib/features/contacts/presentation/screens/contacts_screen.dart similarity index 90% rename from apps/lib/features/contacts/ui/screens/contacts_screen.dart rename to apps/lib/features/contacts/presentation/screens/contacts_screen.dart index cbf9333..d1b2cab 100644 --- a/apps/lib/features/contacts/ui/screens/contacts_screen.dart +++ b/apps/lib/features/contacts/presentation/screens/contacts_screen.dart @@ -1,14 +1,15 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import '../../../../core/di/injection.dart'; +import '../../../../app/di/injection.dart'; +import '../../../../core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_loading_indicator.dart'; import '../../../../shared/widgets/toast/index.dart'; import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/back_title_page_header.dart'; -import '../../../friends/data/friends_api.dart'; -import '../../../users/data/models/user_response.dart'; -import '../../../users/data/users_api.dart'; +import '../../../contacts/data/friends_api.dart'; +import '../../../contacts/data/users/models/user_response.dart'; +import '../../../contacts/data/users/users_api.dart'; class ContactsScreen extends StatefulWidget { const ContactsScreen({super.key}); @@ -67,7 +68,11 @@ class _ContactsScreenState extends State { final query = _searchController.text.trim(); if (query.isEmpty) { - Toast.show(context, '请输入用户名或手机号', type: ToastType.warning); + Toast.show( + context, + context.l10n.contactsSearchEmptyQuery, + type: ToastType.warning, + ); return; } @@ -92,7 +97,11 @@ class _ContactsScreenState extends State { setState(() { _isSearching = false; }); - Toast.show(context, '搜索失败,请稍后重试', type: ToastType.error); + Toast.show( + context, + context.l10n.contactsSearchFailed, + type: ToastType.error, + ); } } } @@ -113,14 +122,22 @@ class _ContactsScreenState extends State { _sentRequestIds.add(targetUserId); _sendingRequestUserId = null; }); - Toast.show(context, '好友请求已发送', type: ToastType.success); + Toast.show( + context, + context.l10n.contactsFriendRequestSent, + type: ToastType.success, + ); } } catch (e) { if (mounted) { setState(() { _sendingRequestUserId = null; }); - Toast.show(context, '发送失败,请稍后重试', type: ToastType.error); + Toast.show( + context, + context.l10n.contactsSendFailed, + type: ToastType.error, + ); } } } @@ -170,7 +187,7 @@ class _ContactsScreenState extends State { ), const SizedBox(height: AppSpacing.lg), Text( - '添加 ${user.username}', + context.l10n.contactsAddSheetTitle(user.username), style: const TextStyle( fontSize: 20, fontWeight: FontWeight.w600, @@ -178,8 +195,8 @@ class _ContactsScreenState extends State { ), ), const SizedBox(height: AppSpacing.sm), - const Text( - '发送一条验证信息,方便对方确认你的身份', + Text( + context.l10n.contactsAddSheetDesc, style: TextStyle(fontSize: 13, color: AppColors.slate500), ), const SizedBox(height: AppSpacing.lg), @@ -194,8 +211,8 @@ class _ContactsScreenState extends State { maxLines: 3, minLines: 2, maxLength: 200, - decoration: const InputDecoration( - hintText: '你好,我是...', + decoration: InputDecoration( + hintText: context.l10n.contactsAddSheetMessageHint, hintStyle: TextStyle( fontSize: 13, color: AppColors.slate400, @@ -215,7 +232,7 @@ class _ContactsScreenState extends State { children: [ Expanded( child: AppButton( - text: '取消', + text: context.l10n.commonCancel, isOutlined: true, onPressed: () => Navigator.pop(sheetContext), ), @@ -261,7 +278,10 @@ class _ContactsScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - BackTitlePageHeader(title: '联系人', onBack: () => context.pop()), + BackTitlePageHeader( + title: context.l10n.contactsTitle, + onBack: () => context.pop(), + ), Expanded( child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), @@ -272,12 +292,12 @@ class _ContactsScreenState extends State { _buildSearchResults(), const SizedBox(height: 16), if (_pendingRequests.isNotEmpty) ...[ - _buildSectionTitle('新的联系人'), + _buildSectionTitle(context.l10n.contactsSectionNew), const SizedBox(height: 8), _buildPendingRequestCard(_pendingRequests), const SizedBox(height: 16), ], - _buildSectionTitle('全部联系人'), + _buildSectionTitle(context.l10n.contactsSectionAll), const SizedBox(height: 8), if (_isLoading) const Center( @@ -314,8 +334,8 @@ class _ContactsScreenState extends State { child: TextField( controller: _searchController, focusNode: _searchFocusNode, - decoration: const InputDecoration( - hintText: '输入用户名或手机号', + decoration: InputDecoration( + hintText: context.l10n.contactsSearchHint, hintStyle: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, @@ -404,9 +424,9 @@ class _ContactsScreenState extends State { else if (_searchResults.isEmpty) Container( padding: const EdgeInsets.all(20), - child: const Center( + child: Center( child: Text( - '未找到该用户', + context.l10n.contactsSearchNoUser, style: TextStyle(fontSize: 14, color: AppColors.slate500), ), ), @@ -479,8 +499,8 @@ class _ContactsScreenState extends State { color: AppColors.slate300, borderRadius: BorderRadius.circular(8), ), - child: const Text( - '已是好友', + child: Text( + context.l10n.contactsStatusAlreadyFriend, style: TextStyle(fontSize: 12, color: AppColors.slate500), ), ); @@ -493,8 +513,8 @@ class _ContactsScreenState extends State { color: AppColors.slate300, borderRadius: BorderRadius.circular(8), ), - child: const Text( - '已发送', + child: Text( + context.l10n.contactsStatusSent, style: TextStyle(fontSize: 12, color: AppColors.slate500), ), ); @@ -512,14 +532,14 @@ class _ContactsScreenState extends State { borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFFD7E6FF)), ), - child: const Row( + child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.person_add, size: 14, color: AppColors.blue500), - SizedBox(width: 4), + const Icon(Icons.person_add, size: 14, color: AppColors.blue500), + const SizedBox(width: 4), Text( - '添加', - style: TextStyle( + context.l10n.contactsAdd, + style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: AppColors.blue500, @@ -544,8 +564,8 @@ class _ContactsScreenState extends State { children: [ const Icon(Icons.person_outline, size: 48, color: AppColors.slate400), const SizedBox(height: 12), - const Text( - '暂无联系人', + Text( + context.l10n.contactsEmptyTitle, style: TextStyle( fontSize: 15, fontWeight: FontWeight.w500, @@ -553,8 +573,8 @@ class _ContactsScreenState extends State { ), ), const SizedBox(height: 4), - const Text( - '搜索手机号添加好友开始聊天吧', + Text( + context.l10n.contactsEmptyDesc, style: TextStyle(fontSize: 13, color: AppColors.slate400), ), ], @@ -616,8 +636,8 @@ class _ContactsScreenState extends State { ), ), const SizedBox(height: 2), - const Text( - '等待对方确认', + Text( + context.l10n.contactsPendingConfirm, style: TextStyle(fontSize: 12, color: AppColors.slate500), ), ], @@ -780,9 +800,12 @@ class _ContactsScreenState extends State { trackColor: AppColors.blue300, withContainer: false, ) - : const Text( - '发送', - style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + : Text( + context.l10n.contactsSend, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), ), ), ); diff --git a/apps/lib/features/home/data/voice_recorder.dart b/apps/lib/features/home/data/voice_recorder.dart index 1957ada..337679e 100644 --- a/apps/lib/features/home/data/voice_recorder.dart +++ b/apps/lib/features/home/data/voice_recorder.dart @@ -3,6 +3,8 @@ import 'dart:io'; import 'package:flutter/services.dart'; import 'package:record/record.dart'; +import '../../../core/l10n/l10n.dart'; + abstract class VoiceRecorder { Future start(); Future stop(); @@ -22,10 +24,10 @@ class RecordVoiceRecorder implements VoiceRecorder { try { hasPermission = await _recorder.hasPermission(); } on MissingPluginException catch (_) { - throw StateError('录音组件未加载,请完全重启 App 后重试'); + throw StateError(L10n.current.homeRecorderPluginUnavailable); } if (!hasPermission) { - throw StateError('录音权限未授权'); + throw StateError(L10n.current.homeRecorderPermissionDenied); } final fileName = @@ -42,7 +44,7 @@ class RecordVoiceRecorder implements VoiceRecorder { path: path, ); } on MissingPluginException catch (_) { - throw StateError('录音组件未加载,请完全重启 App 后重试'); + throw StateError(L10n.current.homeRecorderPluginUnavailable); } } @@ -52,7 +54,7 @@ class RecordVoiceRecorder implements VoiceRecorder { try { stoppedPath = await _recorder.stop(); } on MissingPluginException catch (_) { - throw StateError('录音组件未加载,请完全重启 App 后重试'); + throw StateError(L10n.current.homeRecorderPluginUnavailable); } return stoppedPath ?? _currentPath; } diff --git a/apps/lib/features/home/ui/controllers/home_keyboard_inset_calculator.dart b/apps/lib/features/home/presentation/controllers/home_keyboard_inset_calculator.dart similarity index 100% rename from apps/lib/features/home/ui/controllers/home_keyboard_inset_calculator.dart rename to apps/lib/features/home/presentation/controllers/home_keyboard_inset_calculator.dart diff --git a/apps/lib/features/home/ui/controllers/home_message_viewport_controller.dart b/apps/lib/features/home/presentation/controllers/home_message_viewport_controller.dart similarity index 100% rename from apps/lib/features/home/ui/controllers/home_message_viewport_controller.dart rename to apps/lib/features/home/presentation/controllers/home_message_viewport_controller.dart diff --git a/apps/lib/features/home/ui/controllers/home_viewport_coordinator.dart b/apps/lib/features/home/presentation/controllers/home_viewport_coordinator.dart similarity index 100% rename from apps/lib/features/home/ui/controllers/home_viewport_coordinator.dart rename to apps/lib/features/home/presentation/controllers/home_viewport_coordinator.dart diff --git a/apps/lib/features/home/ui/navigation/home_return_policy.dart b/apps/lib/features/home/presentation/navigation/home_return_policy.dart similarity index 95% rename from apps/lib/features/home/ui/navigation/home_return_policy.dart rename to apps/lib/features/home/presentation/navigation/home_return_policy.dart index 1c4e219..75737ca 100644 --- a/apps/lib/features/home/ui/navigation/home_return_policy.dart +++ b/apps/lib/features/home/presentation/navigation/home_return_policy.dart @@ -1,7 +1,7 @@ import 'package:flutter/widgets.dart'; import 'package:go_router/go_router.dart'; -import '../../../../core/router/app_routes.dart'; +import '../../../../app/router/app_routes.dart'; enum HomeReturnAction { pop, goHome, goHomeForDock } diff --git a/apps/lib/features/home/ui/screens/home_screen.dart b/apps/lib/features/home/presentation/screens/home_screen.dart similarity index 97% rename from apps/lib/features/home/ui/screens/home_screen.dart rename to apps/lib/features/home/presentation/screens/home_screen.dart index fce7278..998a76f 100644 --- a/apps/lib/features/home/ui/screens/home_screen.dart +++ b/apps/lib/features/home/presentation/screens/home_screen.dart @@ -5,10 +5,11 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; -import '../../../../core/api/api_exception.dart'; -import '../../../../core/di/injection.dart'; -import '../../../../core/router/app_route_observer.dart'; -import '../../../../core/router/app_routes.dart'; +import '../../../../core/network/api_exception.dart'; +import '../../../../app/di/injection.dart'; +import '../../../../app/router/app_route_observer.dart'; +import '../../../../app/router/app_routes.dart'; +import '../../../../core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../chat/presentation/bloc/agent_stage.dart'; import '../../../chat/presentation/bloc/chat_bloc.dart'; @@ -32,7 +33,7 @@ import '../widgets/home_unread_badge.dart'; part 'home_screen_interactions.dart'; -/// 布局常量 +/// Layout constants. const _defaultPadding = 20.0; const _itemSpacing = 16.0; const _cancelThreshold = -(AppSpacing.xxl + AppSpacing.xxl); @@ -46,7 +47,7 @@ const homeConversationStageKey = ValueKey('home_conversation_stage'); const homeBottomInputStackKey = ValueKey('home_bottom_input_stack'); const homeEmptyStateAmbientKey = ValueKey('home_empty_state_ambient'); -/// 颜色常量 +/// Color constants. const _chatBgColor = AppColors.slate50; class HomeScreen extends StatefulWidget { @@ -331,7 +332,7 @@ class _HomeScreenState extends State padding: const EdgeInsets.only( bottom: _itemSpacing, ), - child: HomeChatItemRenderer.build(item), + child: HomeChatItemRenderer.build(context, item), ), ], ); @@ -394,7 +395,11 @@ class _HomeScreenState extends State if (hasEarlierHistory) { await _loadMoreHistoryPreservingViewport(chatBloc); } else { - Toast.show(context, '没有更早的历史记录了', type: ToastType.info); + Toast.show( + context, + context.l10n.homeNoEarlierHistory, + type: ToastType.info, + ); } _applyViewportDecision( _dispatchViewportEvent( diff --git a/apps/lib/features/home/ui/screens/home_screen_interactions.dart b/apps/lib/features/home/presentation/screens/home_screen_interactions.dart similarity index 93% rename from apps/lib/features/home/ui/screens/home_screen_interactions.dart rename to apps/lib/features/home/presentation/screens/home_screen_interactions.dart index c97df08..d16ac72 100644 --- a/apps/lib/features/home/ui/screens/home_screen_interactions.dart +++ b/apps/lib/features/home/presentation/screens/home_screen_interactions.dart @@ -23,7 +23,11 @@ extension _HomeScreenInteractions on _HomeScreenState { _isCancelGestureActive = false; }); if (showToast) { - Toast.show(context, '已取消', type: ToastType.info); + Toast.show( + context, + context.l10n.homeRecordingCanceled, + type: ToastType.info, + ); } } @@ -75,7 +79,7 @@ extension _HomeScreenInteractions on _HomeScreenState { return; } if (canceled) { - Toast.show(context, '已请求停止', type: ToastType.info); + Toast.show(context, context.l10n.homeStopRequested, type: ToastType.info); } } @@ -145,7 +149,7 @@ extension _HomeScreenInteractions on _HomeScreenState { _isCancelGestureActive = false; }); if (audioPath == null || audioPath.isEmpty) { - throw StateError('录音失败,请重试'); + throw StateError(context.l10n.errorGenericSafe); } final transcript = await _transcribeAudio(audioPath); if (!mounted) { @@ -153,7 +157,11 @@ extension _HomeScreenInteractions on _HomeScreenState { } final normalizedTranscript = transcript.trim(); if (normalizedTranscript.isEmpty) { - Toast.show(context, '未识别到有效语音,请靠近麦克风并连续说话后重试', type: ToastType.error); + Toast.show( + context, + context.l10n.homeNoValidSpeech, + type: ToastType.error, + ); return; } _messageController.text = normalizedTranscript; @@ -196,7 +204,7 @@ extension _HomeScreenInteractions on _HomeScreenState { } final raw = error.toString(); if (raw.startsWith('Instance of')) { - return '请求失败,请稍后重试'; + return context.l10n.errorGenericSafe; } return raw.replaceFirst('Bad state: ', ''); } diff --git a/apps/lib/features/home/ui/screens/home_sheet.dart b/apps/lib/features/home/presentation/screens/home_sheet.dart similarity index 96% rename from apps/lib/features/home/ui/screens/home_sheet.dart rename to apps/lib/features/home/presentation/screens/home_sheet.dart index a27b46b..78d0f38 100644 --- a/apps/lib/features/home/ui/screens/home_sheet.dart +++ b/apps/lib/features/home/presentation/screens/home_sheet.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:lucide_icons/lucide_icons.dart'; +import '../../../../core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; class HomeSheet extends StatelessWidget { @@ -57,14 +58,14 @@ class HomeSheet extends StatelessWidget { _buildOptionCard( context: context, icon: LucideIcons.camera, - label: '拍照', + label: context.l10n.homeSheetTakePhoto, onTap: () => _handleCameraTap(context), ), const SizedBox(width: 24), _buildOptionCard( context: context, icon: LucideIcons.image, - label: '相册', + label: context.l10n.homeSheetPhotoLibrary, onTap: () => _handlePhotoTap(context), ), ], diff --git a/apps/lib/features/home/ui/widgets/home_attachment_strip.dart b/apps/lib/features/home/presentation/widgets/home_attachment_strip.dart similarity index 100% rename from apps/lib/features/home/ui/widgets/home_attachment_strip.dart rename to apps/lib/features/home/presentation/widgets/home_attachment_strip.dart diff --git a/apps/lib/features/home/ui/widgets/home_background_field.dart b/apps/lib/features/home/presentation/widgets/home_background_field.dart similarity index 100% rename from apps/lib/features/home/ui/widgets/home_background_field.dart rename to apps/lib/features/home/presentation/widgets/home_background_field.dart diff --git a/apps/lib/features/home/ui/widgets/home_chat_item_renderer.dart b/apps/lib/features/home/presentation/widgets/home_chat_item_renderer.dart similarity index 94% rename from apps/lib/features/home/ui/widgets/home_chat_item_renderer.dart rename to apps/lib/features/home/presentation/widgets/home_chat_item_renderer.dart index 4a599ad..83ce090 100644 --- a/apps/lib/features/home/ui/widgets/home_chat_item_renderer.dart +++ b/apps/lib/features/home/presentation/widgets/home_chat_item_renderer.dart @@ -3,11 +3,12 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:lucide_icons/lucide_icons.dart'; +import '../../../../core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; -import '../../../../shared/utils/tool_name_localizer.dart'; +import '../../../../core/utils/tool_name_localizer.dart'; import '../../../../shared/widgets/app_loading_indicator.dart'; import '../../../chat/data/models/chat_list_item.dart'; -import '../../../chat/ui/widgets/ui_schema_renderer.dart'; +import '../../../ui_schema/presentation/widgets/ui_schema_renderer.dart'; const _messagePaddingH = 13.0; const _messagePaddingV = 9.0; @@ -19,12 +20,12 @@ const _toolResultWidthFactor = 0.9; const _iconSize = 24.0; class HomeChatItemRenderer { - static Widget build(ChatListItem item) { + static Widget build(BuildContext context, ChatListItem item) { switch (item.type) { case ChatItemType.message: return _buildMessageItem(item as TextMessageItem); case ChatItemType.toolCall: - return _buildToolCallItem(item as ToolCallItem); + return _buildToolCallItem(context, item as ToolCallItem); case ChatItemType.toolResult: return _buildToolResultItem(item as ToolResultItem); } @@ -198,25 +199,26 @@ class HomeChatItemRenderer { ); } - static Widget _buildToolCallItem(ToolCallItem item) { + static Widget _buildToolCallItem(BuildContext context, ToolCallItem item) { + final l10n = context.l10n; final (statusText, statusColor, statusIcon) = switch (item.status) { ToolCallStatus.pending => ( - '工具准备中', + l10n.homeToolPreparing, AppColors.slate500, LucideIcons.clock, ), ToolCallStatus.executing => ( - '任务执行中', + l10n.homeToolExecuting, AppColors.blue600, LucideIcons.loader, ), ToolCallStatus.error => ( - item.errorMessage ?? '执行失败', + item.errorMessage ?? l10n.homeToolExecutionFailed, AppColors.red600, LucideIcons.alertCircle, ), ToolCallStatus.completed => ( - '已完成', + l10n.homeToolCompleted, AppColors.emerald600, LucideIcons.checkCircle, ), diff --git a/apps/lib/features/home/ui/widgets/home_composer_stack.dart b/apps/lib/features/home/presentation/widgets/home_composer_stack.dart similarity index 88% rename from apps/lib/features/home/ui/widgets/home_composer_stack.dart rename to apps/lib/features/home/presentation/widgets/home_composer_stack.dart index 8dbcc6c..e49f401 100644 --- a/apps/lib/features/home/ui/widgets/home_composer_stack.dart +++ b/apps/lib/features/home/presentation/widgets/home_composer_stack.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; +import '../../../../core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_loading_indicator.dart'; import '../../../../shared/widgets/message_composer.dart'; @@ -94,12 +95,14 @@ class HomeComposerStack extends StatelessWidget { onHoldToSpeakEnd: onHoldToSpeakEnd, onHoldToSpeakMoveUpdate: onHoldToSpeakMoveUpdate, onHoldToSpeakCancel: onHoldToSpeakCancel, - textInputChild: _buildTextInputContent(), + textInputChild: _buildTextInputContent(context), recordingAnimation: const SizedBox.shrink(), - recordingText: isCancelGestureActive ? '松手取消' : '松手发送', + recordingText: isCancelGestureActive + ? context.l10n.homeRecordingReleaseCancel + : context.l10n.homeRecordingReleaseSend, recordingHintText: isCancelGestureActive - ? '松开取消' - : '松开发送,上滑取消', + ? context.l10n.homeRecordingHintReleaseCancel + : context.l10n.homeRecordingHintReleaseSend, showRecordingInlineFeedback: false, ); }, @@ -111,9 +114,9 @@ class HomeComposerStack extends StatelessWidget { ); } - Widget _buildTextInputContent() { + Widget _buildTextInputContent(BuildContext context) { if (isTranscribing) { - return _buildTranscribingIndicator(); + return _buildTranscribingIndicator(context); } return SizedBox.expand( child: Align( @@ -129,8 +132,8 @@ class HomeComposerStack extends StatelessWidget { color: AppColors.slate900, ), textAlignVertical: TextAlignVertical.center, - decoration: const InputDecoration( - hintText: '输入消息...', + decoration: InputDecoration( + hintText: context.l10n.homeInputHint, hintStyle: TextStyle( fontSize: AppSpacing.lg, height: 1, @@ -152,7 +155,7 @@ class HomeComposerStack extends StatelessWidget { ); } - Widget _buildTranscribingIndicator() { + Widget _buildTranscribingIndicator(BuildContext context) { return Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -170,10 +173,10 @@ class HomeComposerStack extends StatelessWidget { const SizedBox(width: AppSpacing.sm), _buildWaveDots(), const SizedBox(width: AppSpacing.sm), - const Expanded( + Expanded( child: Text( - '语音识别中...', - style: TextStyle( + context.l10n.homeTranscribing, + style: const TextStyle( fontSize: 14, color: AppColors.blue600, fontWeight: FontWeight.w600, diff --git a/apps/lib/features/home/ui/widgets/home_conversation_chrome.dart b/apps/lib/features/home/presentation/widgets/home_conversation_chrome.dart similarity index 83% rename from apps/lib/features/home/ui/widgets/home_conversation_chrome.dart rename to apps/lib/features/home/presentation/widgets/home_conversation_chrome.dart index df2ca6f..2b38853 100644 --- a/apps/lib/features/home/ui/widgets/home_conversation_chrome.dart +++ b/apps/lib/features/home/presentation/widgets/home_conversation_chrome.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import '../../../../core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_loading_indicator.dart'; @@ -45,11 +46,16 @@ class HomeDateDivider extends StatelessWidget { @override Widget build(BuildContext context) { final now = DateTime.now(); - final weekdays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']; + const weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; final weekday = weekdays[date.weekday - 1]; final label = date.year == now.year - ? '${date.month}月${date.day}日 $weekday' - : '${date.year}年${date.month}月${date.day}日 $weekday'; + ? context.l10n.homeDateLabelNoYear(date.month, date.day, weekday) + : context.l10n.homeDateLabelWithYear( + date.year, + date.month, + date.day, + weekday, + ); return Container( padding: const EdgeInsets.symmetric(vertical: 12), @@ -87,9 +93,9 @@ class HomeLoadMoreButton extends StatelessWidget { color: AppColors.slate400, trackColor: AppColors.slate200, ) - : const Text( - '查看历史', - style: TextStyle(fontSize: 12, color: AppColors.slate400), + : Text( + context.l10n.homeViewHistory, + style: const TextStyle(fontSize: 12, color: AppColors.slate400), ), ), ); diff --git a/apps/lib/features/home/ui/widgets/home_floating_header.dart b/apps/lib/features/home/presentation/widgets/home_floating_header.dart similarity index 100% rename from apps/lib/features/home/ui/widgets/home_floating_header.dart rename to apps/lib/features/home/presentation/widgets/home_floating_header.dart diff --git a/apps/lib/features/home/ui/widgets/home_input_host.dart b/apps/lib/features/home/presentation/widgets/home_input_host.dart similarity index 100% rename from apps/lib/features/home/ui/widgets/home_input_host.dart rename to apps/lib/features/home/presentation/widgets/home_input_host.dart diff --git a/apps/lib/features/home/ui/widgets/home_recording_overlay.dart b/apps/lib/features/home/presentation/widgets/home_recording_overlay.dart similarity index 95% rename from apps/lib/features/home/ui/widgets/home_recording_overlay.dart rename to apps/lib/features/home/presentation/widgets/home_recording_overlay.dart index b977bde..bbc34d8 100644 --- a/apps/lib/features/home/ui/widgets/home_recording_overlay.dart +++ b/apps/lib/features/home/presentation/widgets/home_recording_overlay.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import '../../../../core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; const _recordingCancelTopColor = AppColors.warningBackground; @@ -30,7 +31,9 @@ class HomeRecordingOverlay extends StatelessWidget { final labelColor = isCancel ? _recordingCancelLabelColor : _recordingActiveLabelColor; - final label = isCancel ? '松手取消' : '松手发送,上移取消'; + final label = isCancel + ? context.l10n.homeRecordingReleaseCancel + : context.l10n.homeRecordingHintReleaseSend; return IgnorePointer( child: Align( diff --git a/apps/lib/features/home/ui/widgets/home_unread_badge.dart b/apps/lib/features/home/presentation/widgets/home_unread_badge.dart similarity index 92% rename from apps/lib/features/home/ui/widgets/home_unread_badge.dart rename to apps/lib/features/home/presentation/widgets/home_unread_badge.dart index bbb2ea3..4b1356d 100644 --- a/apps/lib/features/home/ui/widgets/home_unread_badge.dart +++ b/apps/lib/features/home/presentation/widgets/home_unread_badge.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import '../../../../core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_pressable.dart'; @@ -30,7 +31,7 @@ class HomeUnreadBadge extends StatelessWidget { ], ), child: Text( - '有$count条新消息', + context.l10n.homeUnreadMessages(count), style: const TextStyle( color: AppColors.white, fontSize: 12, diff --git a/apps/lib/features/messages/data/inbox_api.dart b/apps/lib/features/messages/data/inbox_api.dart index a0a5a38..449d0f8 100644 --- a/apps/lib/features/messages/data/inbox_api.dart +++ b/apps/lib/features/messages/data/inbox_api.dart @@ -1,4 +1,4 @@ -import 'package:social_app/core/api/i_api_client.dart'; +import 'package:social_app/core/network/i_api_client.dart'; class InboxApi { final IApiClient _client; diff --git a/apps/lib/features/messages/ui/screens/message_invite_detail_screen.dart b/apps/lib/features/messages/presentation/screens/message_invite_detail_screen.dart similarity index 81% rename from apps/lib/features/messages/ui/screens/message_invite_detail_screen.dart rename to apps/lib/features/messages/presentation/screens/message_invite_detail_screen.dart index ff603a5..e4f43b9 100644 --- a/apps/lib/features/messages/ui/screens/message_invite_detail_screen.dart +++ b/apps/lib/features/messages/presentation/screens/message_invite_detail_screen.dart @@ -1,14 +1,16 @@ import 'package:flutter/material.dart' hide BackButton; import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; -import '../../../../core/di/injection.dart'; +import '../../../../app/di/injection.dart'; +import '../../../../core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_loading_indicator.dart'; import '../../../../shared/widgets/page_header.dart'; import '../../../../shared/widgets/toast/toast.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; import '../../../calendar/data/calendar_api.dart'; -import '../../../users/data/users_api.dart'; +import '../../../contacts/data/users/users_api.dart'; import '../../data/inbox_api.dart'; class MessageInviteDetailScreen extends StatefulWidget { @@ -64,7 +66,7 @@ class _MessageInviteDetailScreenState extends State { } } if (message == null) { - throw StateError('邀请不存在或已失效'); + throw StateError(L10n.current.messagesInviteDetailNotFound); } String? calendarTitle; @@ -121,13 +123,21 @@ class _MessageInviteDetailScreenState extends State { if (!mounted) { return; } - Toast.show(context, '已接受邀请', type: ToastType.success); + Toast.show( + context, + context.l10n.messagesInviteAcceptedToast, + type: ToastType.success, + ); await _loadDetail(); } catch (_) { if (!mounted) { return; } - Toast.show(context, '操作失败,请稍后重试', type: ToastType.error); + Toast.show( + context, + context.l10n.messagesInviteOperationFailed, + type: ToastType.error, + ); } finally { if (mounted) { setState(() => _submitting = false); @@ -149,13 +159,21 @@ class _MessageInviteDetailScreenState extends State { if (!mounted) { return; } - Toast.show(context, '已拒绝邀请', type: ToastType.success); + Toast.show( + context, + context.l10n.messagesInviteRejectedToast, + type: ToastType.success, + ); await _loadDetail(); } catch (_) { if (!mounted) { return; } - Toast.show(context, '操作失败,请稍后重试', type: ToastType.error); + Toast.show( + context, + context.l10n.messagesInviteOperationFailed, + type: ToastType.error, + ); } finally { if (mounted) { setState(() => _submitting = false); @@ -212,18 +230,21 @@ class _MessageInviteDetailScreenState extends State { Widget _buildSummaryCard() { final message = _message; final statusText = message == null - ? '未知' + ? context.l10n.commonUnknown : switch (message.status) { - InboxMessageStatus.pending => '待处理', - InboxMessageStatus.accepted => '已接受', - InboxMessageStatus.rejected => '已拒绝', - InboxMessageStatus.dismissed => '已处理', + InboxMessageStatus.pending => context.l10n.messagesStatusPending, + InboxMessageStatus.accepted => + context.l10n.messagesInviteStatusAccepted, + InboxMessageStatus.rejected => + context.l10n.messagesInviteStatusRejected, + InboxMessageStatus.dismissed => + context.l10n.messagesInviteStatusHandled, }; final createdAt = message?.createdAt; final createdAtText = createdAt == null - ? '未知' - : '${createdAt.year}-${createdAt.month.toString().padLeft(2, '0')}-${createdAt.day.toString().padLeft(2, '0')} ${createdAt.hour.toString().padLeft(2, '0')}:${createdAt.minute.toString().padLeft(2, '0')}'; + ? context.l10n.commonUnknown + : DateFormat.yMd(context.l10n.localeName).add_Hm().format(createdAt); return Container( width: double.infinity, @@ -236,8 +257,8 @@ class _MessageInviteDetailScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - '日历邀请详情', + Text( + context.l10n.messagesInviteDetailTitle, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, @@ -246,7 +267,9 @@ class _MessageInviteDetailScreenState extends State { ), const SizedBox(height: 10), Text( - '事件:${_calendarTitle ?? '未命名日程'}', + context.l10n.messagesInviteEvent( + _calendarTitle ?? context.l10n.messagesInviteUnnamedEvent, + ), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, @@ -255,7 +278,9 @@ class _MessageInviteDetailScreenState extends State { ), const SizedBox(height: 10), Text( - '邀请人:${_senderName ?? '未知用户'}', + context.l10n.messagesInviteSender( + _senderName ?? context.l10n.messagesInviteUnknownUser, + ), style: const TextStyle( fontSize: 13, fontWeight: FontWeight.normal, @@ -264,7 +289,7 @@ class _MessageInviteDetailScreenState extends State { ), const SizedBox(height: 10), Text( - '消息时间:$createdAtText', + context.l10n.messagesInviteTime(createdAtText), style: const TextStyle( fontSize: 13, fontWeight: FontWeight.normal, @@ -273,7 +298,7 @@ class _MessageInviteDetailScreenState extends State { ), const SizedBox(height: 10), Text( - '状态:$statusText', + context.l10n.messagesInviteStatus(statusText), style: const TextStyle( fontSize: 13, fontWeight: FontWeight.normal, @@ -282,7 +307,7 @@ class _MessageInviteDetailScreenState extends State { ), const SizedBox(height: 10), Text( - '邀请ID:${widget.inviteId}', + context.l10n.messagesInviteId(widget.inviteId), style: const TextStyle( fontSize: 13, fontWeight: FontWeight.normal, @@ -303,14 +328,14 @@ class _MessageInviteDetailScreenState extends State { borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.messageTipBorder), ), - child: const Row( + child: Row( children: [ - Icon(Icons.info_outline, size: 14, color: AppColors.slate500), - SizedBox(width: 8), + const Icon(Icons.info_outline, size: 14, color: AppColors.slate500), + const SizedBox(width: 8), Expanded( child: Text( - '同意后将加入该日历事件,拒绝后该邀请会被标记为已处理', - style: TextStyle( + context.l10n.messagesInviteTip, + style: const TextStyle( fontSize: 12, fontWeight: FontWeight.normal, color: AppColors.slate500, @@ -332,8 +357,8 @@ class _MessageInviteDetailScreenState extends State { borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.messageCardBorder), ), - child: const Text( - '该邀请已处理,无需重复操作', + child: Text( + context.l10n.messagesInviteAlreadyHandled, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, @@ -357,10 +382,10 @@ class _MessageInviteDetailScreenState extends State { borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.messageRejectBorder), ), - child: const Row( + child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( + const Text( '×', style: TextStyle( fontSize: 15, @@ -368,10 +393,10 @@ class _MessageInviteDetailScreenState extends State { color: AppColors.red400, ), ), - SizedBox(width: 6), + const SizedBox(width: 6), Text( - '拒绝', - style: TextStyle( + context.l10n.messagesReject, + style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.red400, @@ -392,10 +417,10 @@ class _MessageInviteDetailScreenState extends State { borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.messageAcceptBorder), ), - child: const Row( + child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( + const Text( '√', style: TextStyle( fontSize: 15, @@ -403,10 +428,10 @@ class _MessageInviteDetailScreenState extends State { color: AppColors.blue600, ), ), - SizedBox(width: 6), + const SizedBox(width: 6), Text( - '同意', - style: TextStyle( + context.l10n.messagesAccept, + style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.blue600, diff --git a/apps/lib/features/messages/ui/screens/message_invite_list_screen.dart b/apps/lib/features/messages/presentation/screens/message_invite_list_screen.dart similarity index 84% rename from apps/lib/features/messages/ui/screens/message_invite_list_screen.dart rename to apps/lib/features/messages/presentation/screens/message_invite_list_screen.dart index ed5b449..4195a68 100644 --- a/apps/lib/features/messages/ui/screens/message_invite_list_screen.dart +++ b/apps/lib/features/messages/presentation/screens/message_invite_list_screen.dart @@ -1,17 +1,18 @@ import 'package:flutter/material.dart' hide BackButton; import 'package:go_router/go_router.dart'; -import '../../../../core/di/injection.dart'; -import '../../../../core/router/app_routes.dart'; +import '../../../../app/di/injection.dart'; +import '../../../../app/router/app_routes.dart'; +import '../../../../core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_loading_indicator.dart'; import '../../../../shared/widgets/app_pull_refresh_feedback.dart'; import '../../../../shared/widgets/page_header.dart'; import '../../../../shared/widgets/toast/toast.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; -import '../../../friends/data/friends_api.dart'; +import '../../../contacts/data/friends_api.dart'; import '../../data/inbox_api.dart'; -import '../../ui/widgets/message_action_sheet.dart'; +import '../../presentation/widgets/message_action_sheet.dart'; class MessageWithFriend { final InboxMessageResponse message; @@ -108,7 +109,11 @@ class _MessageInviteListScreenState extends State { _isLoading = false; _isPullRefreshing = false; }); - Toast.show(context, '消息加载失败,请稍后重试', type: ToastType.error); + Toast.show( + context, + context.l10n.messagesLoadFailed, + type: ToastType.error, + ); } } @@ -148,7 +153,11 @@ class _MessageInviteListScreenState extends State { return; case InboxMessageType.friendRequest: if (item.friendRequest == null) { - Toast.show(context, '发送者信息加载失败,请下拉重试', type: ToastType.error); + Toast.show( + context, + context.l10n.messagesSenderLoadFailed, + type: ToastType.error, + ); return; } _showFriendRequestSheet(item, isReadOnly: message.isRead); @@ -171,14 +180,16 @@ class _MessageInviteListScreenState extends State { final friendRequest = item.friendRequest; if (friendRequest == null) return; - final title = '${friendRequest.sender.username} 请求添加您为好友'; + final title = context.l10n.messagesFriendRequestTitle( + friendRequest.sender.username, + ); final description = message.content?['message'] as String?; final statusText = isReadOnly ? (friendRequest.status == 'accepted' - ? '已接受' + ? context.l10n.messagesInviteStatusAccepted : friendRequest.status == 'rejected' - ? '已拒绝' - : '已处理') + ? context.l10n.messagesInviteStatusRejected + : context.l10n.messagesInviteStatusHandled) : null; showModalBottomSheet( @@ -213,7 +224,11 @@ class _MessageInviteListScreenState extends State { final message = item.message; final friendshipId = message.friendshipId; if (friendshipId == null) { - Toast.show(context, '好友请求数据缺失', type: ToastType.error); + Toast.show( + context, + context.l10n.messagesFriendRequestMissing, + type: ToastType.error, + ); return; } @@ -221,19 +236,31 @@ class _MessageInviteListScreenState extends State { if (accept) { await _friendsApi.acceptRequest(friendshipId); if (mounted) { - Toast.show(context, '已接受好友请求', type: ToastType.success); + Toast.show( + context, + context.l10n.messagesAcceptedFriendRequest, + type: ToastType.success, + ); } } else { await _friendsApi.declineRequest(friendshipId); if (mounted) { - Toast.show(context, '已拒绝好友请求', type: ToastType.success); + Toast.show( + context, + context.l10n.messagesRejectedFriendRequest, + type: ToastType.success, + ); } } await _inboxApi.markAsRead(message.id); await _loadMessages(); } catch (e) { if (mounted) { - Toast.show(context, '处理失败,请稍后重试', type: ToastType.error); + Toast.show( + context, + context.l10n.messagesActionFailed, + type: ToastType.error, + ); } } } @@ -289,9 +316,21 @@ class _MessageInviteListScreenState extends State { ), child: Row( children: [ - Expanded(child: _buildTab(0, '未读', Icons.mark_email_unread_outlined)), + Expanded( + child: _buildTab( + 0, + context.l10n.messagesTabUnread, + Icons.mark_email_unread_outlined, + ), + ), const SizedBox(width: 4), - Expanded(child: _buildTab(1, '已读', Icons.mark_email_read_outlined)), + Expanded( + child: _buildTab( + 1, + context.l10n.messagesTabRead, + Icons.mark_email_read_outlined, + ), + ), ], ), ); @@ -412,7 +451,9 @@ class _MessageInviteListScreenState extends State { ), const SizedBox(height: 16), Text( - isUnread ? '暂无未读消息' : '暂无已读消息', + isUnread + ? context.l10n.messagesEmptyUnreadTitle + : context.l10n.messagesEmptyReadTitle, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, @@ -421,7 +462,9 @@ class _MessageInviteListScreenState extends State { ), const SizedBox(height: 8), Text( - isUnread ? '有新消息时会在这里显示' : '处理过的消息会显示在这里', + isUnread + ? context.l10n.messagesEmptyUnreadDesc + : context.l10n.messagesEmptyReadDesc, style: const TextStyle(fontSize: 13, color: AppColors.slate400), ), ], @@ -506,15 +549,17 @@ class _MessageCard extends StatelessWidget { String _title() { if (message.messageType == InboxMessageType.friendRequest) { if (friendRequest == null) { - return '好友请求信息加载失败'; + return L10n.current.messagesFriendRequestLoadFailed; } - return '${friendRequest!.sender.username} 请求添加您为好友'; + return L10n.current.messagesFriendRequestTitle( + friendRequest!.sender.username, + ); } if (message.messageType == InboxMessageType.calendar) { final data = message.content; - return data?['title'] as String? ?? '日历邀请'; + return data?['title'] as String? ?? L10n.current.messagesCalendarInvite; } - return '系统消息'; + return L10n.current.messagesSystemMessage; } String _content() { @@ -523,23 +568,24 @@ class _MessageCard extends StatelessWidget { if (message.content != null) { data = message.content; } - if (data == null) return '点击查看详情'; + if (data == null) return L10n.current.messagesTapToView; final type = data['type'] as String?; if (type == 'invite') { final status = message.status.value; if (status == 'pending') { - return '邀请您加入日历'; + return L10n.current.messagesInviteJoinCalendar; } else if (status == 'accepted') { - return '已接受日历邀请'; + return L10n.current.messagesInviteAccepted; } else if (status == 'rejected') { - return '已拒绝日历邀请'; + return L10n.current.messagesInviteRejected; } } else if (type == 'update') { - return '更新了日历事件'; + return L10n.current.messagesCalendarUpdated; } - return '点击查看详情'; + return L10n.current.messagesTapToView; } - return message.content?['message'] as String? ?? '点击查看详情'; + return message.content?['message'] as String? ?? + L10n.current.messagesTapToView; } } diff --git a/apps/lib/features/messages/ui/widgets/calendar_message_card.dart b/apps/lib/features/messages/presentation/widgets/calendar_message_card.dart similarity index 79% rename from apps/lib/features/messages/ui/widgets/calendar_message_card.dart rename to apps/lib/features/messages/presentation/widgets/calendar_message_card.dart index 0b7a46a..6cddaa4 100644 --- a/apps/lib/features/messages/ui/widgets/calendar_message_card.dart +++ b/apps/lib/features/messages/presentation/widgets/calendar_message_card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:social_app/core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_button.dart'; @@ -23,6 +24,8 @@ class CalendarInviteCard extends StatelessWidget { @override Widget build(BuildContext context) { + final l10n = context.l10n; + return Container( margin: const EdgeInsets.symmetric( horizontal: AppSpacing.md, @@ -52,17 +55,22 @@ class CalendarInviteCard extends StatelessWidget { ), ), const SizedBox(width: AppSpacing.sm), - const Expanded( + Expanded( child: Text( - '日历邀请', - style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14), + l10n.messagesCalendarCardInviteTitle, + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + ), ), ), ], ), const SizedBox(height: AppSpacing.sm), Text( - eventTitle != null ? '邀请你访问 "$eventTitle"' : '邀请你访问日历', + eventTitle != null + ? l10n.messagesCalendarCardInviteWithTitle(eventTitle!) + : l10n.messagesCalendarCardInviteWithoutTitle, style: const TextStyle(fontSize: 14, color: AppColors.slate700), ), const SizedBox(height: AppSpacing.md), @@ -70,14 +78,17 @@ class CalendarInviteCard extends StatelessWidget { children: [ Expanded( child: AppButton( - text: '拒绝', + text: l10n.messagesReject, isOutlined: true, onPressed: onReject, ), ), const SizedBox(width: AppSpacing.sm), Expanded( - child: AppButton(text: '接受', onPressed: onAccept), + child: AppButton( + text: l10n.messagesAccept, + onPressed: onAccept, + ), ), ], ), @@ -100,6 +111,8 @@ class CalendarUpdateCard extends StatelessWidget { @override Widget build(BuildContext context) { + final l10n = context.l10n; + return GestureDetector( onTap: onTap, child: Container( @@ -133,7 +146,9 @@ class CalendarUpdateCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - eventTitle != null ? '$eventTitle 已更新' : '日历事件已更新', + eventTitle != null + ? l10n.messagesCalendarCardUpdatedWithTitle(eventTitle!) + : l10n.messagesCalendarCardUpdatedWithoutTitle, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, @@ -141,7 +156,7 @@ class CalendarUpdateCard extends StatelessWidget { ), const SizedBox(height: 2), Text( - _formatTime(message.createdAt), + _formatTime(context, message.createdAt), style: const TextStyle( fontSize: 12, color: AppColors.slate500, @@ -157,17 +172,18 @@ class CalendarUpdateCard extends StatelessWidget { ); } - String _formatTime(DateTime time) { + String _formatTime(BuildContext context, DateTime time) { + final l10n = context.l10n; final now = DateTime.now(); final diff = now.difference(time); if (diff.inMinutes < 60) { - return '${diff.inMinutes}分钟前'; + return l10n.messagesCalendarCardTimeMinutesAgo(diff.inMinutes); } else if (diff.inHours < 24) { - return '${diff.inHours}小时前'; + return l10n.messagesCalendarCardTimeHoursAgo(diff.inHours); } else if (diff.inDays < 7) { - return '${diff.inDays}天前'; + return l10n.messagesCalendarCardTimeDaysAgo(diff.inDays); } else { - return '${time.month}月${time.day}日'; + return l10n.messagesCalendarCardTimeDate(time.month, time.day); } } } @@ -184,6 +200,8 @@ class CalendarDeleteCard extends StatelessWidget { @override Widget build(BuildContext context) { + final l10n = context.l10n; + return Container( margin: const EdgeInsets.symmetric( horizontal: AppSpacing.md, @@ -212,7 +230,9 @@ class CalendarDeleteCard extends StatelessWidget { const SizedBox(width: AppSpacing.sm), Expanded( child: Text( - eventTitle != null ? '$eventTitle 已删除' : '日历事件已删除', + eventTitle != null + ? l10n.messagesCalendarCardDeletedWithTitle(eventTitle!) + : l10n.messagesCalendarCardDeletedWithoutTitle, style: const TextStyle(fontSize: 14, color: AppColors.slate500), ), ), diff --git a/apps/lib/features/messages/ui/widgets/message_action_sheet.dart b/apps/lib/features/messages/presentation/widgets/message_action_sheet.dart similarity index 96% rename from apps/lib/features/messages/ui/widgets/message_action_sheet.dart rename to apps/lib/features/messages/presentation/widgets/message_action_sheet.dart index f53f467..6a3a6ef 100644 --- a/apps/lib/features/messages/ui/widgets/message_action_sheet.dart +++ b/apps/lib/features/messages/presentation/widgets/message_action_sheet.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import '../../../../core/theme/design_tokens.dart'; +import '../../../../core/l10n/l10n.dart'; import '../../../../shared/widgets/app_button.dart'; class MessageActionSheet extends StatelessWidget { @@ -98,7 +99,7 @@ class MessageActionSheet extends StatelessWidget { children: [ Expanded( child: AppButton( - text: '拒绝', + text: context.l10n.messagesReject, isOutlined: true, onPressed: () { Navigator.pop(context); @@ -109,7 +110,7 @@ class MessageActionSheet extends StatelessWidget { const SizedBox(width: AppSpacing.md), Expanded( child: AppButton( - text: '接受', + text: context.l10n.messagesAccept, onPressed: () { Navigator.pop(context); onAccept?.call(); diff --git a/apps/lib/core/notifications/ios_notification_payload_bridge.dart b/apps/lib/features/notification/data/services/ios_notification_payload_bridge.dart similarity index 91% rename from apps/lib/core/notifications/ios_notification_payload_bridge.dart rename to apps/lib/features/notification/data/services/ios_notification_payload_bridge.dart index 3c2563e..d598381 100644 --- a/apps/lib/core/notifications/ios_notification_payload_bridge.dart +++ b/apps/lib/features/notification/data/services/ios_notification_payload_bridge.dart @@ -1,6 +1,6 @@ import 'dart:convert'; import 'package:shared_preferences/shared_preferences.dart'; -import '../../features/calendar/reminders/models/reminder_payload.dart'; +import '../../domain/models/reminder_payload.dart'; class IOSNotificationPayloadBridge { static const String _key = 'pending_notification_payload'; diff --git a/apps/lib/core/notifications/local_notification_service.dart b/apps/lib/features/notification/data/services/local_notification_service.dart similarity index 93% rename from apps/lib/core/notifications/local_notification_service.dart rename to apps/lib/features/notification/data/services/local_notification_service.dart index 67674fa..81ba293 100644 --- a/apps/lib/core/notifications/local_notification_service.dart +++ b/apps/lib/features/notification/data/services/local_notification_service.dart @@ -5,9 +5,10 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:timezone/data/latest.dart' as tz_data; import 'package:timezone/timezone.dart' as tz; +import '../../../../core/l10n/l10n.dart'; import 'reminder_notification_callbacks.dart'; -import '../../features/calendar/data/models/schedule_item_model.dart'; -import '../../features/calendar/reminders/models/reminder_payload.dart'; +import '../../../calendar/data/models/schedule_item_model.dart'; +import '../../domain/models/reminder_payload.dart'; class LocalNotificationService { final FlutterLocalNotificationsPlugin _plugin; @@ -146,8 +147,8 @@ class LocalNotificationService { return NotificationDetails( android: AndroidNotificationDetails( 'calendar_alarm_channel_v2', - '日程闹钟提醒', - channelDescription: '日程到点闹钟式提醒通知', + L10n.current.notificationChannelName, + channelDescription: L10n.current.notificationChannelDescription, importance: Importance.max, priority: Priority.max, category: AndroidNotificationCategory.alarm, @@ -270,18 +271,19 @@ class LocalNotificationService { String _buildReminderBody(ScheduleItemModel event, int reminderMinutes) { final when = reminderMinutes == 0 - ? '日程现在开始' - : '日程即将开始(提前$reminderMinutes分钟)'; + ? L10n.current.notificationStartsNow + : L10n.current.notificationStartsInMinutes(reminderMinutes); final location = event.metadata?.location; final notes = event.metadata?.notes; final buffer = StringBuffer(when); if (location != null && location.isNotEmpty) { - buffer.write('\n地点:$location'); + buffer.write('\n${L10n.current.notificationLocation(location)}'); } if (notes != null && notes.isNotEmpty) { - buffer.write( - '\n备注:${notes.length > 30 ? '${notes.substring(0, 30)}...' : notes}', - ); + final preview = notes.length > 30 + ? '${notes.substring(0, 30)}...' + : notes; + buffer.write('\n${L10n.current.notificationNotes(preview)}'); } return buffer.toString(); } diff --git a/apps/lib/core/notifications/reminder_notification_callbacks.dart b/apps/lib/features/notification/data/services/reminder_notification_callbacks.dart similarity index 98% rename from apps/lib/core/notifications/reminder_notification_callbacks.dart rename to apps/lib/features/notification/data/services/reminder_notification_callbacks.dart index 9d97d58..afbc38f 100644 --- a/apps/lib/core/notifications/reminder_notification_callbacks.dart +++ b/apps/lib/features/notification/data/services/reminder_notification_callbacks.dart @@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import '../../features/calendar/reminders/models/reminder_payload.dart'; +import '../../domain/models/reminder_payload.dart'; typedef ReminderNotificationResponseHandler = Future Function(NotificationResponse response); diff --git a/apps/lib/features/calendar/reminders/models/reminder_action.dart b/apps/lib/features/notification/domain/models/reminder_action.dart similarity index 100% rename from apps/lib/features/calendar/reminders/models/reminder_action.dart rename to apps/lib/features/notification/domain/models/reminder_action.dart diff --git a/apps/lib/features/calendar/reminders/models/reminder_payload.dart b/apps/lib/features/notification/domain/models/reminder_payload.dart similarity index 100% rename from apps/lib/features/calendar/reminders/models/reminder_payload.dart rename to apps/lib/features/notification/domain/models/reminder_payload.dart diff --git a/apps/lib/features/calendar/reminders/reminder_action_executor.dart b/apps/lib/features/notification/domain/services/reminder_action_executor.dart similarity index 89% rename from apps/lib/features/calendar/reminders/reminder_action_executor.dart rename to apps/lib/features/notification/domain/services/reminder_action_executor.dart index 16f9af5..f34f6cb 100644 --- a/apps/lib/features/calendar/reminders/reminder_action_executor.dart +++ b/apps/lib/features/notification/domain/services/reminder_action_executor.dart @@ -1,7 +1,7 @@ -import '../data/services/calendar_service.dart'; -import '../../../core/notifications/local_notification_service.dart'; -import 'models/reminder_action.dart'; -import 'models/reminder_payload.dart'; +import '../../../calendar/data/services/calendar_service.dart'; +import '../../data/services/local_notification_service.dart'; +import '../models/reminder_action.dart'; +import '../models/reminder_payload.dart'; class ReminderActionExecutor { final CalendarService _calendarService; diff --git a/apps/lib/features/calendar/reminders/reminder_queue_manager.dart b/apps/lib/features/notification/domain/services/reminder_queue_manager.dart similarity index 94% rename from apps/lib/features/calendar/reminders/reminder_queue_manager.dart rename to apps/lib/features/notification/domain/services/reminder_queue_manager.dart index 5636516..f7df247 100644 --- a/apps/lib/features/calendar/reminders/reminder_queue_manager.dart +++ b/apps/lib/features/notification/domain/services/reminder_queue_manager.dart @@ -1,4 +1,4 @@ -import 'models/reminder_payload.dart'; +import '../models/reminder_payload.dart'; class ReminderQueueManager { ReminderPayload? _currentPayload; diff --git a/apps/lib/features/calendar/reminders/ui/reminder_overlay.dart b/apps/lib/features/notification/presentation/widgets/reminder_overlay.dart similarity index 90% rename from apps/lib/features/calendar/reminders/ui/reminder_overlay.dart rename to apps/lib/features/notification/presentation/widgets/reminder_overlay.dart index c2cb2b7..ffccbd3 100644 --- a/apps/lib/features/calendar/reminders/ui/reminder_overlay.dart +++ b/apps/lib/features/notification/presentation/widgets/reminder_overlay.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import '../../../../core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_button.dart'; -import '../../reminders/reminder_queue_manager.dart'; -import '../../reminders/models/reminder_payload.dart'; +import '../../domain/services/reminder_queue_manager.dart'; +import '../../domain/models/reminder_payload.dart'; class ReminderOverlay extends StatefulWidget { const ReminderOverlay({ @@ -65,7 +66,7 @@ class _ReminderOverlayState extends State { mainAxisSize: MainAxisSize.min, children: [ _SnoozeOption( - label: '5 分钟', + label: context.l10n.notificationSnoozeMinutes(5), onTap: () { _hideSnoozeOptions(); _handleSnooze(5); @@ -73,7 +74,7 @@ class _ReminderOverlayState extends State { ), const Divider(height: 1, color: AppColors.borderSecondary), _SnoozeOption( - label: '15 分钟', + label: context.l10n.notificationSnoozeMinutes(15), onTap: () { _hideSnoozeOptions(); _handleSnooze(15); @@ -138,14 +139,17 @@ class _ReminderOverlayState extends State { children: [ Expanded( child: AppButton( - text: '稍后提醒', + text: context.l10n.notificationSnoozeLater, isOutlined: true, onPressed: _showSnoozeDropdown, ), ), const SizedBox(width: AppSpacing.md), Expanded( - child: AppButton(text: '完成', onPressed: _handleComplete), + child: AppButton( + text: context.l10n.commonDone, + onPressed: _handleComplete, + ), ), ], ), diff --git a/apps/lib/features/settings/data/models/memory_models.dart b/apps/lib/features/settings/data/models/memory_models.dart index 38a6ca1..1a71e61 100644 --- a/apps/lib/features/settings/data/models/memory_models.dart +++ b/apps/lib/features/settings/data/models/memory_models.dart @@ -1,3 +1,5 @@ +import '../../../../core/l10n/l10n.dart'; + class PersonMeta { final String? source; final double? confidence; @@ -546,15 +548,15 @@ class UserMemoryContent { parts.add(occupation!); } if (people.isNotEmpty) { - parts.add('${people.length} 位联系人'); + parts.add(L10n.current.memorySummaryContactsCount(people.length)); } if (places.isNotEmpty) { - parts.add('${places.length} 个地点'); + parts.add(L10n.current.memorySummaryPlacesCount(places.length)); } if (interests.isNotEmpty) { - parts.add('${interests.length} 个兴趣'); + parts.add(L10n.current.memorySummaryInterestsCount(interests.length)); } - return parts.isEmpty ? '暂无信息' : parts.join(' · '); + return parts.isEmpty ? L10n.current.memoryNoInfo : parts.join(' · '); } } @@ -916,15 +918,17 @@ class WorkProfileContent { parts.add(occupation!); } if (expertise.isNotEmpty) { - parts.add('${expertise.length} 项专长'); + parts.add(L10n.current.memorySummaryExpertiseCount(expertise.length)); } if (currentProjects.isNotEmpty) { - parts.add('${currentProjects.length} 个项目'); + parts.add( + L10n.current.memorySummaryProjectsCount(currentProjects.length), + ); } if (teamMembers.isNotEmpty) { - parts.add('${teamMembers.length} 位团队成员'); + parts.add(L10n.current.memorySummaryTeamMembersCount(teamMembers.length)); } - return parts.isEmpty ? '暂无信息' : parts.join(' · '); + return parts.isEmpty ? L10n.current.memoryNoInfo : parts.join(' · '); } } diff --git a/apps/lib/features/settings/data/services/automation_jobs_api.dart b/apps/lib/features/settings/data/services/automation_jobs_api.dart index 590e58e..62de2b3 100644 --- a/apps/lib/features/settings/data/services/automation_jobs_api.dart +++ b/apps/lib/features/settings/data/services/automation_jobs_api.dart @@ -1,4 +1,4 @@ -import 'package:social_app/core/api/i_api_client.dart'; +import 'package:social_app/core/network/i_api_client.dart'; import '../models/automation_job_model.dart'; class AutomationJobsApi { diff --git a/apps/lib/features/settings/data/services/memory_service.dart b/apps/lib/features/settings/data/services/memory_service.dart index 534c3c2..5f02e8c 100644 --- a/apps/lib/features/settings/data/services/memory_service.dart +++ b/apps/lib/features/settings/data/services/memory_service.dart @@ -1,4 +1,4 @@ -import 'package:social_app/core/api/i_api_client.dart'; +import 'package:social_app/core/network/i_api_client.dart'; import '../models/memory_models.dart'; class MemoryService { diff --git a/apps/lib/features/settings/data/services/settings_user_cache.dart b/apps/lib/features/settings/data/services/settings_user_cache.dart index 6fbceb4..0ea05c3 100644 --- a/apps/lib/features/settings/data/services/settings_user_cache.dart +++ b/apps/lib/features/settings/data/services/settings_user_cache.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import '../../../users/data/models/user_response.dart'; +import '../../../contacts/data/users/models/user_response.dart'; import 'user_profile_cache_repository.dart'; class SettingsUserCache { diff --git a/apps/lib/features/settings/data/services/user_profile_cache_repository.dart b/apps/lib/features/settings/data/services/user_profile_cache_repository.dart index 578c9e4..4dfb873 100644 --- a/apps/lib/features/settings/data/services/user_profile_cache_repository.dart +++ b/apps/lib/features/settings/data/services/user_profile_cache_repository.dart @@ -3,7 +3,7 @@ import 'dart:async'; import '../../../../core/cache/cache_entry.dart'; import '../../../../core/cache/cache_policy.dart'; import '../../../../core/cache/hybrid_cache_store.dart'; -import '../../../users/data/models/user_response.dart'; +import '../../../contacts/data/users/models/user_response.dart'; class UserProfileCacheRepository { static const String cacheKey = 'settings:user_profile'; diff --git a/apps/lib/features/settings/data/settings_api.dart b/apps/lib/features/settings/data/settings_api.dart index 08cc8ae..376af4d 100644 --- a/apps/lib/features/settings/data/settings_api.dart +++ b/apps/lib/features/settings/data/settings_api.dart @@ -1,4 +1,4 @@ -import 'package:social_app/core/api/i_api_client.dart'; +import 'package:social_app/core/network/i_api_client.dart'; class AppVersionResponse { final bool hasUpdate; diff --git a/apps/lib/features/settings/ui/screens/edit_profile_screen.dart b/apps/lib/features/settings/presentation/screens/edit_profile_screen.dart similarity index 85% rename from apps/lib/features/settings/ui/screens/edit_profile_screen.dart rename to apps/lib/features/settings/presentation/screens/edit_profile_screen.dart index f60eaff..6e5e073 100644 --- a/apps/lib/features/settings/ui/screens/edit_profile_screen.dart +++ b/apps/lib/features/settings/presentation/screens/edit_profile_screen.dart @@ -2,15 +2,16 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:social_app/core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; -import '../../../../core/di/injection.dart'; +import '../../../../app/di/injection.dart'; import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/app_loading_indicator.dart'; import '../../../../shared/widgets/toast/toast.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; import '../../data/services/settings_user_cache.dart'; -import '../../../users/data/models/user_response.dart'; -import '../../../users/data/users_api.dart'; +import '../../../contacts/data/users/models/user_response.dart'; +import '../../../contacts/data/users/users_api.dart'; import '../widgets/account_section_card.dart'; import '../widgets/settings_page_scaffold.dart'; @@ -69,7 +70,11 @@ class _EditProfileScreenState extends State { setState(() { _isLoading = false; }); - Toast.show(context, '加载用户信息失败', type: ToastType.error); + Toast.show( + context, + context.l10n.settingsEditProfileLoadFailed, + type: ToastType.error, + ); } } } @@ -112,13 +117,21 @@ class _EditProfileScreenState extends State { try { await _usersApi.uploadAvatar(_selectedAvatar!); if (mounted) { - Toast.show(context, '头像上传成功', type: ToastType.success); + Toast.show( + context, + context.l10n.settingsEditProfileAvatarUploadSuccess, + type: ToastType.success, + ); _selectedAvatar = null; await _loadUser(); } } catch (e) { if (mounted) { - Toast.show(context, '头像上传失败,请重试', type: ToastType.error); + Toast.show( + context, + context.l10n.settingsEditProfileAvatarUploadFailed, + type: ToastType.error, + ); } } finally { if (mounted) { @@ -139,11 +152,19 @@ class _EditProfileScreenState extends State { if (usernameChanged) { if (newUsername.isEmpty) { - Toast.show(context, '用户名不能为空', type: ToastType.warning); + Toast.show( + context, + context.l10n.settingsEditProfileUsernameRequired, + type: ToastType.warning, + ); return; } if (newUsername.length < 3 || newUsername.length > 30) { - Toast.show(context, '用户名需要3-30个字符', type: ToastType.warning); + Toast.show( + context, + context.l10n.settingsEditProfileUsernameLengthInvalid, + type: ToastType.warning, + ); return; } } @@ -167,12 +188,20 @@ class _EditProfileScreenState extends State { } if (mounted) { - Toast.show(context, '保存成功', type: ToastType.success); + Toast.show( + context, + context.l10n.settingsEditProfileSaveSuccess, + type: ToastType.success, + ); context.pop(true); } } catch (e) { if (mounted) { - Toast.show(context, '保存失败,请重试', type: ToastType.error); + Toast.show( + context, + context.l10n.settingsEditProfileSaveFailed, + type: ToastType.error, + ); } } finally { if (mounted) { @@ -192,8 +221,10 @@ class _EditProfileScreenState extends State { @override Widget build(BuildContext context) { + final l10n = context.l10n; + return SettingsPageScaffold( - title: '编辑资料', + title: l10n.settingsEditProfileTitle, onBack: () => context.pop(), resizeOnKeyboard: false, maintainBottomViewPadding: true, @@ -213,7 +244,7 @@ class _EditProfileScreenState extends State { width: double.infinity, height: 52, child: AppButton( - text: '保存修改', + text: l10n.settingsEditProfileSaveChanges, onPressed: _hasChanges && !_isSaving ? _saveProfile : null, isLoading: _isSaving, ), @@ -222,8 +253,10 @@ class _EditProfileScreenState extends State { } Widget _buildBasicInfoSection() { + final l10n = context.l10n; + return AccountSectionCard( - title: '基础信息', + title: l10n.settingsEditProfileBasicInfo, backgroundColor: AppColors.white, borderColor: AppColors.borderSecondary, child: Column( @@ -231,8 +264,8 @@ class _EditProfileScreenState extends State { children: [ _buildAvatarSection(), const SizedBox(height: AppSpacing.lg), - const Text( - '用户名', + Text( + l10n.settingsEditProfileUsername, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, @@ -244,7 +277,9 @@ class _EditProfileScreenState extends State { controller: _usernameController, onChanged: (_) => _onFieldChanged(), style: const TextStyle(fontSize: 15, color: AppColors.slate900), - decoration: _buildInputDecoration('请输入用户名'), + decoration: _buildInputDecoration( + l10n.settingsEditProfileUsernameHint, + ), ), ], ), @@ -333,15 +368,17 @@ class _EditProfileScreenState extends State { } Widget _buildBioSection() { + final l10n = context.l10n; + return AccountSectionCard( - title: '个人简介', + title: l10n.settingsEditProfileBio, backgroundColor: AppColors.white, borderColor: AppColors.borderSecondary, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - '简介内容', + Text( + l10n.settingsEditProfileBioContent, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, @@ -356,7 +393,7 @@ class _EditProfileScreenState extends State { maxLength: 200, style: const TextStyle(fontSize: 15, color: AppColors.slate900), decoration: _buildInputDecoration( - '介绍一下自己吧', + l10n.settingsEditProfileBioHint, ).copyWith(contentPadding: const EdgeInsets.all(AppSpacing.lg)), ), ], diff --git a/apps/lib/features/settings/ui/screens/features_screen.dart b/apps/lib/features/settings/presentation/screens/features_screen.dart similarity index 86% rename from apps/lib/features/settings/ui/screens/features_screen.dart rename to apps/lib/features/settings/presentation/screens/features_screen.dart index fe6dea2..7e39097 100644 --- a/apps/lib/features/settings/ui/screens/features_screen.dart +++ b/apps/lib/features/settings/presentation/screens/features_screen.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import '../../../../core/di/injection.dart'; -import '../../../../core/router/app_routes.dart'; +import '../../../../app/di/injection.dart'; +import '../../../../app/router/app_routes.dart'; +import '../../../../core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/app_loading_indicator.dart'; @@ -43,7 +44,7 @@ class _FeaturesScreenState extends State { return BlocProvider.value( value: _cubit, child: SettingsPageScaffold( - title: '周期计划', + title: context.l10n.settingsFeaturesTitle, onBack: () => context.pop(), body: BlocBuilder( builder: (context, state) { @@ -67,17 +68,17 @@ class _FeaturesScreenState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildSectionTitle('每日'), + _buildSectionTitle(context.l10n.settingsSectionDaily), const SizedBox(height: AppSpacing.sm), if (dailyJobs.isEmpty) - _buildEmptyHint('暂无每日计划') + _buildEmptyHint(context.l10n.settingsNoDailyPlans) else ...dailyJobs.map(_buildJobCard), const SizedBox(height: AppSpacing.lg), - _buildSectionTitle('每周'), + _buildSectionTitle(context.l10n.settingsSectionWeekly), const SizedBox(height: AppSpacing.sm), if (weeklyJobs.isEmpty) - _buildEmptyHint('暂无每周计划') + _buildEmptyHint(context.l10n.settingsNoWeeklyPlans) else ...weeklyJobs.map(_buildJobCard), if (state.canCreateMore) ...[ @@ -181,7 +182,11 @@ class _FeaturesScreenState extends State { value: job.isActive, onChanged: (next) { if (job.isSystem) { - Toast.show(context, '系统预置任务状态不可修改', type: ToastType.info); + Toast.show( + context, + context.l10n.settingsSystemJobReadonly, + type: ToastType.info, + ); return; } _cubit.updateJobStatus(id: job.id, enabled: next); @@ -194,14 +199,18 @@ class _FeaturesScreenState extends State { } String _buildSubtitle(AutomationJobModel job) { - final statusText = job.isActive ? '已启用' : '未启用'; - final sourceText = job.isSystem ? '系统预置' : '自定义'; + final statusText = job.isActive + ? context.l10n.settingsJobStatusEnabled + : context.l10n.settingsJobStatusDisabled; + final sourceText = job.isSystem + ? context.l10n.settingsJobSourceSystem + : context.l10n.settingsJobSourceCustom; return '$sourceText • $statusText'; } Widget _buildCreateButton() { return AppButton( - text: '创建任务', + text: context.l10n.settingsCreateJob, onPressed: () async { await context.push(AppRoutes.settingsJobNew); if (!mounted) { diff --git a/apps/lib/features/settings/ui/screens/job_detail_screen.dart b/apps/lib/features/settings/presentation/screens/job_detail_screen.dart similarity index 81% rename from apps/lib/features/settings/ui/screens/job_detail_screen.dart rename to apps/lib/features/settings/presentation/screens/job_detail_screen.dart index 66e5729..76802eb 100644 --- a/apps/lib/features/settings/ui/screens/job_detail_screen.dart +++ b/apps/lib/features/settings/presentation/screens/job_detail_screen.dart @@ -3,8 +3,9 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; +import 'package:social_app/core/l10n/l10n.dart'; -import '../../../../core/di/injection.dart'; +import '../../../../app/di/injection.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/app_input.dart'; @@ -15,7 +16,7 @@ import '../../../../shared/widgets/detail_header_action_menu.dart'; import '../../../../shared/widgets/destructive_action_sheet.dart'; import '../../../../shared/widgets/toast/toast.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; -import '../../../../shared/utils/tool_name_localizer.dart'; +import '../../../../core/utils/tool_name_localizer.dart'; import '../../data/models/automation_job_model.dart'; import '../../data/services/automation_jobs_api.dart'; import '../../presentation/cubits/job_detail_cubit.dart'; @@ -74,9 +75,10 @@ class _JobDetailScreenState extends State { } }, builder: (context, state) { + final l10n = context.l10n; if (state.isLoading) { return SettingsPageScaffold( - title: '加载中', + title: l10n.commonLoading, body: const Center(child: AppLoadingIndicator()), ); } @@ -85,14 +87,14 @@ class _JobDetailScreenState extends State { final isEditMode = widget.jobId != null; if (isEditMode && job == null && state.error != null) { return SettingsPageScaffold( - title: '任务详情', + title: l10n.settingsJobDetailTitle, onBack: () => context.pop(), body: _buildLoadFailedView(state.error!), ); } return SettingsPageScaffold( - title: job?.title ?? '新建周期计划', + title: job?.title ?? l10n.settingsJobCreatePageTitle, onBack: () => context.pop(), trailing: job != null && !job.isSystem ? _buildHeaderActions(job.id, state) @@ -107,10 +109,11 @@ class _JobDetailScreenState extends State { } Widget _buildLoadFailedView(String error) { + final l10n = context.l10n; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildSectionTitle('加载失败'), + _buildSectionTitle(l10n.settingsJobLoadFailed), const SizedBox(height: AppSpacing.sm), Text( error, @@ -121,43 +124,64 @@ class _JobDetailScreenState extends State { ), ), const SizedBox(height: AppSpacing.md), - AppButton(text: '重试', onPressed: () => _cubit.loadJob(widget.jobId!)), + AppButton( + text: l10n.settingsJobRetry, + onPressed: () => _cubit.loadJob(widget.jobId!), + ), ], ); } Widget _buildDetailPage(AutomationJobModel job, JobDetailState state) { + final l10n = context.l10n; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildOverviewCard(job), const SizedBox(height: AppSpacing.lg), - _buildSectionTitle('计划配置'), + _buildSectionTitle(l10n.settingsJobPlanConfig), const SizedBox(height: AppSpacing.sm), _buildInfoCard([ - _buildInfoRow('周期', _scheduleLabel(job.config.schedule.type)), - _buildInfoRow('执行时间', _displayRunAt(job.config.schedule)), - _buildInfoRow('时区', job.timezone), - _buildInfoRow('状态', job.isActive ? '已启用' : '未启用'), + _buildInfoRow( + l10n.settingsJobCycle, + _scheduleLabel(job.config.schedule.type), + ), + _buildInfoRow( + l10n.settingsJobRunAt, + _displayRunAt(job.config.schedule), + ), + _buildInfoRow(l10n.settingsJobTimezone, job.timezone), + _buildInfoRow( + l10n.settingsJobStatusLabel, + job.isActive + ? l10n.settingsJobStatusEnabled + : l10n.settingsJobStatusDisabled, + ), ]), const SizedBox(height: AppSpacing.lg), - _buildSectionTitle('输入模板'), + _buildSectionTitle(l10n.settingsJobInputTemplate), const SizedBox(height: AppSpacing.sm), _buildTextBlock(job.config.inputTemplate), const SizedBox(height: AppSpacing.lg), - _buildSectionTitle('启用工具'), + _buildSectionTitle(l10n.settingsJobEnabledTools), const SizedBox(height: AppSpacing.sm), _buildToolWrap(job.config.enabledTools), const SizedBox(height: AppSpacing.lg), - _buildSectionTitle('上下文消息模式'), + _buildSectionTitle(l10n.settingsJobContextMode), const SizedBox(height: AppSpacing.sm), _buildInfoCard([ - _buildInfoRow('来源', _contextSourceLabel(job.config.context.source)), _buildInfoRow( - '窗口模式', + l10n.settingsJobContextSource, + _contextSourceLabel(job.config.context.source), + ), + _buildInfoRow( + l10n.settingsJobWindowMode, _windowModeLabel(job.config.context.windowMode), ), - _buildInfoRow('窗口数量', '${job.config.context.windowCount}'), + _buildInfoRow( + l10n.settingsJobWindowCount, + l10n.settingsJobWindowCountValue(job.config.context.windowCount), + ), ]), if (!job.isSystem && state.isSaving) const Padding( @@ -169,11 +193,12 @@ class _JobDetailScreenState extends State { } Widget _buildHeaderActions(String jobId, JobDetailState state) { + final l10n = context.l10n; return DetailHeaderActionMenu<_JobDetailHeaderAction>( - items: const [ + items: [ DetailHeaderActionItem<_JobDetailHeaderAction>( value: _JobDetailHeaderAction.delete, - label: '删除', + label: l10n.commonDelete, icon: Icons.delete_outline, isDestructive: true, ), @@ -190,11 +215,12 @@ class _JobDetailScreenState extends State { } Future _confirmAndDelete(String jobId) async { + final l10n = context.l10n; final confirmed = await showDestructiveActionSheet( context, - title: '删除周期计划', - message: '删除后将无法恢复,是否继续?', - confirmText: '确认删除', + title: l10n.settingsJobDeleteTitle, + message: l10n.settingsJobDeleteMessage, + confirmText: l10n.settingsJobDeleteConfirm, ); if (!confirmed) { return; @@ -204,12 +230,17 @@ class _JobDetailScreenState extends State { return; } if (success) { - Toast.show(context, '删除成功', type: ToastType.success); + Toast.show( + context, + l10n.settingsJobDeleteSuccess, + type: ToastType.success, + ); context.pop(); } } Widget _buildOverviewCard(AutomationJobModel job) { + final l10n = context.l10n; return Container( width: double.infinity, padding: const EdgeInsets.all(AppSpacing.lg), @@ -238,8 +269,16 @@ class _JobDetailScreenState extends State { spacing: AppSpacing.sm, runSpacing: AppSpacing.sm, children: [ - _buildBadge(job.isSystem ? '系统预置' : '自定义'), - _buildBadge(job.isActive ? '已启用' : '未启用'), + _buildBadge( + job.isSystem + ? l10n.settingsJobSourceSystem + : l10n.settingsJobSourceCustom, + ), + _buildBadge( + job.isActive + ? l10n.settingsJobStatusEnabled + : l10n.settingsJobStatusDisabled, + ), _buildBadge(_scheduleLabel(job.config.schedule.type)), ], ), @@ -271,6 +310,7 @@ class _JobDetailScreenState extends State { } Widget _buildCreateForm(JobDetailState state) { + final l10n = context.l10n; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -283,7 +323,7 @@ class _JobDetailScreenState extends State { _buildCreateContextSection(), const SizedBox(height: AppSpacing.xl), AppButton( - text: '创建任务', + text: l10n.settingsCreateJob, isLoading: state.isSaving, onPressed: state.isSaving ? null : _submitCreate, ), @@ -292,16 +332,21 @@ class _JobDetailScreenState extends State { } Widget _buildCreateBasicSection() { + final l10n = context.l10n; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildSectionTitle('基本信息'), + _buildSectionTitle(l10n.settingsJobBasicInfo), const SizedBox(height: AppSpacing.sm), - AppInput(label: '任务名称', hint: '请输入任务名称', controller: _titleController), + AppInput( + label: l10n.settingsJobName, + hint: l10n.settingsJobNameHint, + controller: _titleController, + ), const SizedBox(height: AppSpacing.md), AppInput( - label: '输入模板', - hint: '例如:请总结今天的记忆内容', + label: l10n.settingsJobInputTemplate, + hint: l10n.settingsJobTemplateHint, controller: _templateController, maxLines: 4, ), @@ -310,13 +355,14 @@ class _JobDetailScreenState extends State { } Widget _buildCreateRuleSection() { + final l10n = context.l10n; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildSectionTitle('执行规则'), + _buildSectionTitle(l10n.settingsJobExecutionRules), const SizedBox(height: AppSpacing.sm), _buildPickerTile( - label: '周期', + label: l10n.settingsJobCycle, value: _scheduleLabel(_scheduleType), onTap: _pickScheduleType, ), @@ -326,21 +372,26 @@ class _JobDetailScreenState extends State { ], const SizedBox(height: AppSpacing.sm), _buildPickerTile( - label: '执行时间', + label: l10n.settingsJobRunAt, value: _formatTime(_runAt), onTap: _pickRunAt, ), const SizedBox(height: AppSpacing.sm), - _buildPickerTile(label: '时区', value: _timezone, onTap: _pickTimezone), + _buildPickerTile( + label: l10n.settingsJobTimezone, + value: _timezone, + onTap: _pickTimezone, + ), ], ); } Widget _buildCreateToolSection() { + final l10n = context.l10n; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildSectionTitle('工具选择'), + _buildSectionTitle(l10n.settingsJobToolSelection), const SizedBox(height: AppSpacing.sm), _buildToolSelector(), ], @@ -348,25 +399,26 @@ class _JobDetailScreenState extends State { } Widget _buildCreateContextSection() { + final l10n = context.l10n; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildSectionTitle('上下文消息模式'), + _buildSectionTitle(l10n.settingsJobContextMode), const SizedBox(height: AppSpacing.sm), _buildPickerTile( - label: '来源', + label: l10n.settingsJobContextSource, value: _contextSourceLabel(_contextSource), onTap: _pickContextSource, ), const SizedBox(height: AppSpacing.sm), _buildPickerTile( - label: '窗口模式', + label: l10n.settingsJobWindowMode, value: _windowModeLabel(_contextWindowMode), onTap: _pickWindowMode, ), const SizedBox(height: AppSpacing.sm), _buildCounterTile( - label: '窗口数量', + label: l10n.settingsJobWindowCount, value: _contextWindowCount, onMinus: _contextWindowCount > 1 ? () { @@ -458,7 +510,7 @@ class _JobDetailScreenState extends State { children: [ Expanded( child: Text( - '$label:$value', + context.l10n.settingsJobCounterValue(label, value), style: const TextStyle( color: AppColors.slate800, fontSize: 14, @@ -542,14 +594,15 @@ class _JobDetailScreenState extends State { } Widget _buildWeekdaySelector() { - const weekdayLabels = { - 1: '周一', - 2: '周二', - 3: '周三', - 4: '周四', - 5: '周五', - 6: '周六', - 7: '周日', + final l10n = context.l10n; + final weekdayLabels = { + 1: l10n.settingsJobWeekdayMon, + 2: l10n.settingsJobWeekdayTue, + 3: l10n.settingsJobWeekdayWed, + 4: l10n.settingsJobWeekdayThu, + 5: l10n.settingsJobWeekdayFri, + 6: l10n.settingsJobWeekdaySat, + 7: l10n.settingsJobWeekdaySun, }; return Container( @@ -562,8 +615,8 @@ class _JobDetailScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - '执行日', + Text( + l10n.settingsJobRunDays, style: TextStyle( color: AppColors.slate500, fontSize: 12, @@ -622,7 +675,7 @@ class _JobDetailScreenState extends State { Widget _buildToolWrap(List tools) { if (tools.isEmpty) { - return _buildTextBlock('未启用工具'); + return _buildTextBlock(context.l10n.settingsJobNoToolsEnabled); } return Wrap( spacing: AppSpacing.sm, @@ -713,13 +766,17 @@ class _JobDetailScreenState extends State { } Future _pickScheduleType() async { + final l10n = context.l10n; final picked = await showAppSelectionSheet( context, - title: '选择周期', + title: l10n.settingsJobPickCycle, selectedValue: _scheduleType, - items: const [ - AppSelectionItem(value: 'daily', label: '每日'), - AppSelectionItem(value: 'weekly', label: '每周'), + items: [ + AppSelectionItem(value: 'daily', label: l10n.settingsJobScheduleDaily), + AppSelectionItem( + value: 'weekly', + label: l10n.settingsJobScheduleWeekly, + ), ], ); if (picked != null) { @@ -735,7 +792,7 @@ class _JobDetailScreenState extends State { Future _pickTimezone() async { final picked = await showAppSelectionSheet( context, - title: '选择时区', + title: context.l10n.settingsJobPickTimezone, selectedValue: _timezone, items: const [ AppSelectionItem(value: 'Asia/Shanghai', label: 'Asia/Shanghai'), @@ -750,11 +807,17 @@ class _JobDetailScreenState extends State { } Future _pickContextSource() async { + final l10n = context.l10n; final picked = await showAppSelectionSheet( context, - title: '选择上下文来源', + title: l10n.settingsJobPickContextSource, selectedValue: _contextSource, - items: const [AppSelectionItem(value: 'latest_chat', label: '最近聊天')], + items: [ + AppSelectionItem( + value: 'latest_chat', + label: l10n.settingsJobContextSourceLatestChat, + ), + ], ); if (picked != null) { setState(() { @@ -764,13 +827,17 @@ class _JobDetailScreenState extends State { } Future _pickWindowMode() async { + final l10n = context.l10n; final picked = await showAppSelectionSheet( context, - title: '选择窗口模式', + title: l10n.settingsJobPickWindowMode, selectedValue: _contextWindowMode, - items: const [ - AppSelectionItem(value: 'day', label: '按天数'), - AppSelectionItem(value: 'number', label: '按消息数'), + items: [ + AppSelectionItem(value: 'day', label: l10n.settingsJobWindowModeByDay), + AppSelectionItem( + value: 'number', + label: l10n.settingsJobWindowModeByNumber, + ), ], ); if (picked != null) { @@ -802,29 +869,30 @@ class _JobDetailScreenState extends State { } String _scheduleLabel(String scheduleType) { + final l10n = context.l10n; final normalized = scheduleType.toLowerCase(); if (normalized == 'daily') { - return '每日'; + return l10n.settingsJobScheduleDaily; } if (normalized == 'weekly') { - return '每周'; + return l10n.settingsJobScheduleWeekly; } return scheduleType; } String _contextSourceLabel(String source) { if (source == 'latest_chat') { - return '最近聊天'; + return context.l10n.settingsJobContextSourceLatestChat; } return source; } String _windowModeLabel(String mode) { if (mode == 'day') { - return '按天数'; + return context.l10n.settingsJobWindowModeByDay; } if (mode == 'number') { - return '按消息数'; + return context.l10n.settingsJobWindowModeByNumber; } return mode; } @@ -833,7 +901,11 @@ class _JobDetailScreenState extends State { final title = _titleController.text.trim(); final template = _templateController.text.trim(); if (title.isEmpty || template.isEmpty) { - Toast.show(context, '请填写完整信息', type: ToastType.error); + Toast.show( + context, + context.l10n.settingsJobFillRequired, + type: ToastType.error, + ); return; } @@ -863,7 +935,11 @@ class _JobDetailScreenState extends State { return; } if (success) { - Toast.show(context, '创建成功', type: ToastType.success); + Toast.show( + context, + context.l10n.settingsJobCreateSuccess, + type: ToastType.success, + ); context.pop(true); } } diff --git a/apps/lib/features/settings/ui/screens/memory_screen.dart b/apps/lib/features/settings/presentation/screens/memory_screen.dart similarity index 90% rename from apps/lib/features/settings/ui/screens/memory_screen.dart rename to apps/lib/features/settings/presentation/screens/memory_screen.dart index 8bae90a..d0fc4f1 100644 --- a/apps/lib/features/settings/ui/screens/memory_screen.dart +++ b/apps/lib/features/settings/presentation/screens/memory_screen.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:social_app/core/di/injection.dart'; +import 'package:social_app/app/di/injection.dart'; import 'package:social_app/core/theme/design_tokens.dart'; -import 'package:social_app/core/router/app_routes.dart'; +import 'package:social_app/core/l10n/l10n.dart'; +import 'package:social_app/app/router/app_routes.dart'; import 'package:social_app/shared/widgets/app_loading_indicator.dart'; import 'package:social_app/shared/widgets/app_pressable.dart'; import '../widgets/settings_page_scaffold.dart'; @@ -45,7 +46,7 @@ class _MemoryScreenState extends State { } catch (e) { if (!mounted) return; setState(() { - _error = '加载失败,请重试'; + _error = L10n.current.memoryLoadFailedRetry; _isLoading = false; }); } @@ -54,7 +55,7 @@ class _MemoryScreenState extends State { @override Widget build(BuildContext context) { return SettingsPageScaffold( - title: '我的记忆', + title: context.l10n.memoryTitle, onBack: () => context.pop(), body: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -127,8 +128,8 @@ class _MemoryScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - '智能记忆', + Text( + context.l10n.memorySmartTitle, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, @@ -137,7 +138,7 @@ class _MemoryScreenState extends State { ), const SizedBox(height: 2), Text( - '持续学习你的偏好和习惯', + context.l10n.memorySmartDesc, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, @@ -169,7 +170,7 @@ class _MemoryScreenState extends State { Icon(Icons.error_outline, size: 48, color: AppColors.slate300), const SizedBox(height: AppSpacing.md), Text( - _error ?? '加载失败', + _error ?? context.l10n.memoryLoadFailedRetry, style: TextStyle(fontSize: 14, color: AppColors.slate500), ), const SizedBox(height: AppSpacing.lg), @@ -186,9 +187,9 @@ class _MemoryScreenState extends State { borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all(color: AppColors.blue100), ), - child: const Text( - '重新加载', - style: TextStyle( + child: Text( + context.l10n.memoryReload, + style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.blue600, @@ -205,11 +206,11 @@ class _MemoryScreenState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _buildSectionLabel('用户记忆'), + _buildSectionLabel(context.l10n.memorySectionUser), const SizedBox(height: AppSpacing.sm), _buildUserMemoryCard(), const SizedBox(height: AppSpacing.md), - _buildSectionLabel('工作记忆'), + _buildSectionLabel(context.l10n.memorySectionWork), const SizedBox(height: AppSpacing.sm), _buildWorkMemoryCard(), ], @@ -285,8 +286,8 @@ class _MemoryScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - '个人偏好', + Text( + context.l10n.memoryUserProfile, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, @@ -295,7 +296,9 @@ class _MemoryScreenState extends State { ), const SizedBox(height: 2), Text( - hasData ? userMemory.summary : '暂无信息', + hasData + ? userMemory.summary + : context.l10n.memoryNoInfo, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, @@ -325,7 +328,12 @@ class _MemoryScreenState extends State { '${userMemory.interests.length}', '${userMemory.recurringRoutines.length}', ], - labels: ['联系人', '地点', '兴趣', '日程'], + labels: [ + context.l10n.memoryStatContacts, + context.l10n.memoryStatPlaces, + context.l10n.memoryStatInterests, + context.l10n.memoryStatSchedule, + ], ), ], ], @@ -389,8 +397,8 @@ class _MemoryScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - '工作画像', + Text( + context.l10n.memoryWorkProfile, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, @@ -399,7 +407,9 @@ class _MemoryScreenState extends State { ), const SizedBox(height: 2), Text( - hasData ? workMemory.summary : '暂无信息', + hasData + ? workMemory.summary + : context.l10n.memoryNoInfo, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, @@ -429,7 +439,12 @@ class _MemoryScreenState extends State { '${workMemory.currentProjects.length}', '${workMemory.teamMembers.length}', ], - labels: ['专长', '工具', '项目', '团队'], + labels: [ + context.l10n.memoryStatExpertise, + context.l10n.memoryStatTools, + context.l10n.memoryStatProjects, + context.l10n.memoryStatTeam, + ], ), ], ], diff --git a/apps/lib/features/settings/ui/screens/settings_screen.dart b/apps/lib/features/settings/presentation/screens/settings_screen.dart similarity index 89% rename from apps/lib/features/settings/ui/screens/settings_screen.dart rename to apps/lib/features/settings/presentation/screens/settings_screen.dart index e7263e6..0d59cd2 100644 --- a/apps/lib/features/settings/ui/screens/settings_screen.dart +++ b/apps/lib/features/settings/presentation/screens/settings_screen.dart @@ -2,8 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:social_app/core/constants/app_constants.dart'; -import 'package:social_app/core/di/injection.dart'; -import 'package:social_app/core/router/app_routes.dart'; +import 'package:social_app/app/di/injection.dart'; +import 'package:social_app/app/router/app_routes.dart'; +import 'package:social_app/core/l10n/l10n.dart'; import 'package:social_app/core/theme/design_tokens.dart'; import 'package:social_app/shared/widgets/app_button.dart'; import 'package:social_app/shared/widgets/app_loading_indicator.dart'; @@ -11,16 +12,16 @@ import 'package:social_app/shared/widgets/app_pressable.dart'; import 'package:social_app/shared/widgets/destructive_action_sheet.dart'; import 'package:social_app/shared/widgets/toast/toast.dart'; import 'package:social_app/shared/widgets/toast/toast_type.dart'; -import 'package:social_app/shared/utils/phone_display_formatter.dart'; +import 'package:social_app/core/utils/phone_display_formatter.dart'; import 'package:social_app/features/auth/presentation/bloc/auth_bloc.dart'; import 'package:social_app/features/auth/presentation/bloc/auth_event.dart'; import 'package:social_app/features/auth/presentation/bloc/auth_state.dart'; -import 'package:social_app/features/friends/data/friends_api.dart'; +import 'package:social_app/features/contacts/data/friends_api.dart'; import 'package:social_app/features/settings/data/settings_api.dart'; import 'package:social_app/features/settings/data/services/automation_jobs_api.dart'; import 'package:social_app/features/settings/data/services/settings_user_cache.dart'; -import 'package:social_app/features/users/data/models/user_response.dart'; -import 'package:social_app/features/home/ui/navigation/home_return_policy.dart'; +import 'package:social_app/features/contacts/data/users/models/user_response.dart'; +import 'package:social_app/features/home/presentation/navigation/home_return_policy.dart'; import '../widgets/settings_page_scaffold.dart'; const settingsProfileEditButtonKey = ValueKey('settings_profile_edit_button'); @@ -107,7 +108,7 @@ class _SettingsScreenState extends State { @override Widget build(BuildContext context) { return SettingsPageScaffold( - title: '设置', + title: context.l10n.settingsTitle, onBack: () => returnToHomePreserveState(context, forceGoHome: true), body: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -126,20 +127,6 @@ class _SettingsScreenState extends State { ); } - Widget _buildSectionLabel(String label) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: AppSpacing.xs), - child: Text( - label, - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: AppColors.slate500, - ), - ), - ); - } - Widget _buildProfileHero() { if (_isLoading) { return Container( @@ -154,9 +141,10 @@ class _SettingsScreenState extends State { ); } - final username = _user?.username ?? '未设置'; + final l10n = context.l10n; + final username = _user?.username ?? l10n.settingsUnset; final phone = _user?.phone == null - ? '未设置' + ? l10n.settingsUnset : formatPhoneForDisplay(_user?.phone); return Container( @@ -288,8 +276,8 @@ class _SettingsScreenState extends State { borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.borderQuaternary), ), - child: const Text( - 'Free', + child: Text( + context.l10n.settingsFreeBadge, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, @@ -355,23 +343,29 @@ class _SettingsScreenState extends State { } String _buildFriendsSubtitle() { + final l10n = context.l10n; if (_friendsCount == 0) { - return '暂无联系人'; + return l10n.settingsNoContacts; } if (_friendsCount == 1) { - return '已添加 1 位:$_firstFriendName'; + return l10n.settingsContactsAddedOne( + _firstFriendName ?? l10n.commonUnknown, + ); } - return '已添加 $_friendsCount 位联系人'; + return l10n.settingsContactsAddedMany(_friendsCount); } String _buildAutomationSubtitle() { + final l10n = context.l10n; if (_enabledJobsCount == 0) { - return '暂无启用计划'; + return l10n.settingsNoEnabledPlans; } if (_enabledJobsCount == 1) { - return '已启用:${_firstEnabledJobTitle ?? '周期计划'}'; + return l10n.settingsEnabledPlanOne( + _firstEnabledJobTitle ?? l10n.settingsFeaturesTitle, + ); } - return '已启用 $_enabledJobsCount 个计划'; + return l10n.settingsEnabledPlanMany(_enabledJobsCount); } Widget _buildQuickActions(BuildContext context) { @@ -381,7 +375,7 @@ class _SettingsScreenState extends State { child: _buildActionCard( icon: Icons.people, iconColor: AppColors.blue500, - title: '联系人', + title: context.l10n.contactsTitle, subtitle: _buildFriendsSubtitle(), onTap: () => context.push(AppRoutes.contactsList), ), @@ -391,7 +385,7 @@ class _SettingsScreenState extends State { child: _buildActionCard( icon: Icons.auto_awesome, iconColor: AppColors.blue500, - title: '周期计划', + title: context.l10n.settingsFeaturesTitle, subtitle: _buildAutomationSubtitle(), onTap: () => context.push(AppRoutes.settingsFeatures), ), @@ -519,8 +513,8 @@ class _SettingsScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - '升级到 Pro', + Text( + context.l10n.settingsUpgradeProTitle, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, @@ -529,7 +523,7 @@ class _SettingsScreenState extends State { ), const SizedBox(height: 2), Text( - '解锁更多高级功能', + context.l10n.settingsUpgradeProDesc, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, @@ -554,8 +548,8 @@ class _SettingsScreenState extends State { ), ], ), - child: const Text( - '升级', + child: Text( + context.l10n.settingsUpgradeButton, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, @@ -579,19 +573,19 @@ class _SettingsScreenState extends State { children: [ _buildMenuItem( icon: Icons.notifications, - title: '提醒设置', + title: context.l10n.settingsMenuNotifications, onTap: () {}, ), _buildDivider(), _buildMenuItem( icon: Icons.bookmark, - title: '我的记忆', + title: context.l10n.memoryTitle, onTap: () => context.push(AppRoutes.settingsMemory), ), _buildDivider(), _buildMenuItem( icon: Icons.system_update, - title: '检查更新', + title: context.l10n.settingsMenuCheckUpdates, trailing: 'v${AppConstants.version}', onTap: _checkForUpdates, ), @@ -677,9 +671,9 @@ class _SettingsScreenState extends State { Future _onTapLogout() async { final confirmed = await showDestructiveActionSheet( context, - title: '退出登录', - message: '确定退出当前账户吗?', - confirmText: '确认退出', + title: context.l10n.settingsLogoutTitle, + message: context.l10n.settingsLogoutConfirmMessage, + confirmText: context.l10n.settingsLogoutConfirm, ); if (!confirmed || !mounted) { return; @@ -694,7 +688,11 @@ class _SettingsScreenState extends State { .timeout(const Duration(seconds: 5)); } catch (_) { if (!mounted) return; - Toast.show(context, '退出失败,请稍后重试', type: ToastType.error); + Toast.show( + context, + context.l10n.settingsLogoutFailed, + type: ToastType.error, + ); return; } if (!mounted) return; @@ -713,28 +711,32 @@ class _SettingsScreenState extends State { if (!mounted) return; if (!result.hasUpdate) { - Toast.show(context, '当前已是最新版本', type: ToastType.success); + Toast.show( + context, + context.l10n.settingsLatestVersion, + type: ToastType.success, + ); return; } final message = result.updateType == 'required' - ? '有新版本可用 (${result.latestVersionName}),请立即更新' - : '发现新版本 (${result.latestVersionName}),是否更新?'; + ? context.l10n.settingsUpdateRequired(result.latestVersionName) + : context.l10n.settingsUpdateOptional(result.latestVersionName); final shouldUpdate = await showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('检查更新'), + title: Text(context.l10n.settingsUpdateDialogTitle), content: Text(message), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), - child: const Text('取消'), + child: Text(context.l10n.commonCancel), ), if (result.downloadUrl != null) TextButton( onPressed: () => Navigator.pop(context, true), - child: const Text('更新'), + child: Text(context.l10n.settingsUpdateAction), ), ], ), @@ -743,13 +745,17 @@ class _SettingsScreenState extends State { if (shouldUpdate == true && result.downloadUrl != null && mounted) { Toast.show( context, - '下载链接: ${result.downloadUrl}', + context.l10n.settingsDownloadLink(result.downloadUrl!), type: ToastType.info, ); } } catch (e) { if (!mounted) return; - Toast.show(context, '检查更新失败', type: ToastType.error); + Toast.show( + context, + context.l10n.settingsUpdateCheckFailed, + type: ToastType.error, + ); } } @@ -759,7 +765,7 @@ class _SettingsScreenState extends State { height: 52, child: AppButton( key: settingsLogoutButtonKey, - text: '退出登录', + text: context.l10n.settingsLogoutTitle, isOutlined: true, onPressed: () => _onTapLogout(), ), diff --git a/apps/lib/features/settings/ui/screens/user_memory_detail_screen.dart b/apps/lib/features/settings/presentation/screens/user_memory_detail_screen.dart similarity index 88% rename from apps/lib/features/settings/ui/screens/user_memory_detail_screen.dart rename to apps/lib/features/settings/presentation/screens/user_memory_detail_screen.dart index 9a7b12b..cf5de0a 100644 --- a/apps/lib/features/settings/ui/screens/user_memory_detail_screen.dart +++ b/apps/lib/features/settings/presentation/screens/user_memory_detail_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:social_app/core/di/injection.dart'; +import 'package:social_app/app/di/injection.dart'; +import 'package:social_app/core/l10n/l10n.dart'; import 'package:social_app/core/theme/design_tokens.dart'; import 'package:social_app/shared/widgets/app_loading_indicator.dart'; import 'package:social_app/shared/widgets/app_pressable.dart'; @@ -48,7 +49,7 @@ class _UserMemoryDetailScreenState extends State { } catch (e) { if (!mounted) return; setState(() { - _error = '加载失败'; + _error = L10n.current.memoryLoadFailedRetry; _isLoading = false; }); } @@ -69,13 +70,21 @@ class _UserMemoryDetailScreenState extends State { _isSaving = false; _hasChanges = false; }); - Toast.show(context, '保存成功', type: ToastType.success); + Toast.show( + context, + context.l10n.settingsMemorySaveSuccess, + type: ToastType.success, + ); } catch (e) { if (!mounted) return; setState(() { _isSaving = false; }); - Toast.show(context, '保存失败', type: ToastType.error); + Toast.show( + context, + context.l10n.settingsMemorySaveFailed, + type: ToastType.error, + ); } } @@ -89,7 +98,7 @@ class _UserMemoryDetailScreenState extends State { @override Widget build(BuildContext context) { return SettingsPageScaffold( - title: '编辑个人偏好', + title: context.l10n.settingsUserMemoryEditTitle, onBack: () => context.pop(), footer: _hasChanges ? _buildSaveButton() : null, body: Column( @@ -123,7 +132,10 @@ class _UserMemoryDetailScreenState extends State { children: [ Icon(Icons.error_outline, size: 48, color: AppColors.slate300), const SizedBox(height: AppSpacing.md), - Text(_error ?? '加载失败', style: TextStyle(color: AppColors.slate500)), + Text( + _error ?? context.l10n.memoryLoadFailedRetry, + style: TextStyle(color: AppColors.slate500), + ), const SizedBox(height: AppSpacing.lg), AppPressable( onTap: _loadMemory, @@ -138,8 +150,8 @@ class _UserMemoryDetailScreenState extends State { borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all(color: AppColors.blue100), ), - child: const Text( - '重新加载', + child: Text( + context.l10n.memoryReload, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, @@ -160,7 +172,10 @@ class _UserMemoryDetailScreenState extends State { children: [ Icon(Icons.person_off_outlined, size: 48, color: AppColors.slate300), const SizedBox(height: AppSpacing.md), - Text('暂无个人偏好信息', style: TextStyle(color: AppColors.slate500)), + Text( + context.l10n.settingsUserMemoryEmptyProfile, + style: TextStyle(color: AppColors.slate500), + ), ], ), ); @@ -191,8 +206,8 @@ class _UserMemoryDetailScreenState extends State { child: Center( child: _isSaving ? const AppLoadingIndicator(variant: AppLoadingVariant.button) - : const Text( - '保存更改', + : Text( + context.l10n.todoSaveChanges, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, @@ -229,23 +244,23 @@ class _UserMemoryDetailScreenState extends State { Widget _buildBasicInfoSection() { return _buildSection( - title: '基本信息', + title: context.l10n.settingsUserMemorySectionBasic, icon: Icons.person_outline, children: [ _buildEditField( - label: '职业', + label: context.l10n.settingsUserMemoryFieldOccupation, value: _memory?.occupation, onChanged: (value) => _updateMemory(_memory!.copyWith(occupation: value)), ), _buildEditField( - label: '时区', + label: context.l10n.settingsUserMemoryFieldTimezone, value: _memory?.timezone, onChanged: (value) => _updateMemory(_memory!.copyWith(timezone: value)), ), _buildEditField( - label: '主要语言', + label: context.l10n.settingsUserMemoryFieldPrimaryLanguage, value: _memory?.primaryLanguage, onChanged: (value) => _updateMemory(_memory!.copyWith(primaryLanguage: value)), @@ -256,12 +271,12 @@ class _UserMemoryDetailScreenState extends State { Widget _buildPeopleSection() { return _buildSection( - title: '联系人', + title: context.l10n.settingsUserMemorySectionContacts, icon: Icons.people_outline, count: _memory?.people.length ?? 0, children: [ if (_memory?.people.isEmpty ?? true) - _buildEmptySection('暂无联系人') + _buildEmptySection(context.l10n.settingsUserMemoryEmptyContacts) else ..._memory!.people.asMap().entries.map((entry) { final index = entry.key; @@ -269,9 +284,9 @@ class _UserMemoryDetailScreenState extends State { return _buildPersonItem(person, index); }), const SizedBox(height: AppSpacing.sm), - _buildAddButton('添加联系人', () { + _buildAddButton(context.l10n.settingsUserMemoryAddContact, () { final newPeople = List.from(_memory!.people) - ..add(Person(name: '新联系人')); + ..add(Person(name: context.l10n.settingsUserMemoryNewContact)); _updateMemory(_memory!.copyWith(people: newPeople)); }), ], @@ -294,7 +309,7 @@ class _UserMemoryDetailScreenState extends State { children: [ Expanded( child: _buildEditField( - label: '姓名', + label: context.l10n.settingsUserMemoryFieldName, value: person.name, onChanged: (value) { final newPeople = List.from(_memory!.people); @@ -322,7 +337,7 @@ class _UserMemoryDetailScreenState extends State { children: [ Expanded( child: _buildEditField( - label: '关系', + label: context.l10n.settingsUserMemoryFieldRelationship, value: person.relationship, onChanged: (value) { final newPeople = List.from(_memory!.people); @@ -334,7 +349,7 @@ class _UserMemoryDetailScreenState extends State { const SizedBox(width: AppSpacing.sm), Expanded( child: _buildEditField( - label: '角色', + label: context.l10n.settingsUserMemoryFieldRole, value: person.role, onChanged: (value) { final newPeople = List.from(_memory!.people); @@ -347,7 +362,7 @@ class _UserMemoryDetailScreenState extends State { ), const SizedBox(height: AppSpacing.sm), _buildEditField( - label: '联系方式', + label: context.l10n.settingsUserMemoryFieldContact, value: person.preferredContactChannel, onChanged: (value) { final newPeople = List.from(_memory!.people); @@ -359,7 +374,7 @@ class _UserMemoryDetailScreenState extends State { ), const SizedBox(height: AppSpacing.sm), _buildEditField( - label: '备注', + label: context.l10n.settingsUserMemoryFieldNotes, value: person.notes, onChanged: (value) { final newPeople = List.from(_memory!.people); @@ -374,12 +389,12 @@ class _UserMemoryDetailScreenState extends State { Widget _buildPlacesSection() { return _buildSection( - title: '地点', + title: context.l10n.settingsUserMemorySectionPlaces, icon: Icons.place_outlined, count: _memory?.places.length ?? 0, children: [ if (_memory?.places.isEmpty ?? true) - _buildEmptySection('暂无地点') + _buildEmptySection(context.l10n.settingsUserMemoryEmptyPlaces) else ..._memory!.places.asMap().entries.map((entry) { final index = entry.key; @@ -387,9 +402,9 @@ class _UserMemoryDetailScreenState extends State { return _buildPlaceItem(place, index); }), const SizedBox(height: AppSpacing.sm), - _buildAddButton('添加地点', () { + _buildAddButton(context.l10n.settingsUserMemoryAddPlace, () { final newPlaces = List.from(_memory!.places) - ..add(Place(name: '新地点')); + ..add(Place(name: context.l10n.settingsUserMemoryNewPlace)); _updateMemory(_memory!.copyWith(places: newPlaces)); }), ], @@ -412,7 +427,7 @@ class _UserMemoryDetailScreenState extends State { children: [ Expanded( child: _buildEditField( - label: '名称', + label: context.l10n.settingsUserMemoryFieldName, value: place.name, onChanged: (value) { final newPlaces = List.from(_memory!.places); @@ -440,7 +455,7 @@ class _UserMemoryDetailScreenState extends State { children: [ Expanded( child: _buildEditField( - label: '类别', + label: context.l10n.settingsUserMemoryFieldCategory, value: place.category, onChanged: (value) { final newPlaces = List.from(_memory!.places); @@ -452,7 +467,7 @@ class _UserMemoryDetailScreenState extends State { const SizedBox(width: AppSpacing.sm), Expanded( child: _buildEditField( - label: '偏好', + label: context.l10n.settingsUserMemoryFieldPreference, value: place.preference, onChanged: (value) { final newPlaces = List.from(_memory!.places); @@ -465,7 +480,7 @@ class _UserMemoryDetailScreenState extends State { ), const SizedBox(height: AppSpacing.sm), _buildEditField( - label: '地址', + label: context.l10n.settingsUserMemoryFieldAddress, value: place.address, onChanged: (value) { final newPlaces = List.from(_memory!.places); @@ -481,11 +496,11 @@ class _UserMemoryDetailScreenState extends State { Widget _buildPreferencesSection() { final prefs = _memory!.preferences; return _buildSection( - title: '偏好设置', + title: context.l10n.settingsUserMemorySectionPreferences, icon: Icons.settings_outlined, children: [ _buildEditField( - label: '沟通风格', + label: context.l10n.settingsUserMemoryFieldCommunicationStyle, value: prefs.communicationStyle, onChanged: (value) { _updateMemory( @@ -496,7 +511,7 @@ class _UserMemoryDetailScreenState extends State { }, ), _buildEditField( - label: '位置偏好', + label: context.l10n.settingsUserMemoryFieldLocationPreference, value: prefs.locationPreference, onChanged: (value) { _updateMemory( @@ -507,7 +522,7 @@ class _UserMemoryDetailScreenState extends State { }, ), _buildEditField( - label: '工作生活方式', + label: context.l10n.settingsUserMemoryFieldWorkLifestyle, value: prefs.workLifestyle, onChanged: (value) { _updateMemory( @@ -523,7 +538,7 @@ class _UserMemoryDetailScreenState extends State { Widget _buildInterestsSection() { return _buildSection( - title: '兴趣', + title: context.l10n.settingsUserMemorySectionInterests, icon: Icons.interests_outlined, children: [ _buildTagsSection( @@ -545,7 +560,7 @@ class _UserMemoryDetailScreenState extends State { Widget _buildAvoidTopicsSection() { return _buildSection( - title: '回避话题', + title: context.l10n.settingsUserMemorySectionAvoidTopics, icon: Icons.not_interested_outlined, children: [ _buildTagsSection( @@ -567,12 +582,12 @@ class _UserMemoryDetailScreenState extends State { Widget _buildRecurringRoutinesSection() { return _buildSection( - title: '周期习惯', + title: context.l10n.settingsUserMemorySectionRoutines, icon: Icons.schedule_outlined, count: _memory?.recurringRoutines.length ?? 0, children: [ if (_memory?.recurringRoutines.isEmpty ?? true) - _buildEmptySection('暂无周期习惯') + _buildEmptySection(context.l10n.settingsUserMemoryEmptyRoutines) else ..._memory!.recurringRoutines.asMap().entries.map((entry) { final index = entry.key; @@ -580,10 +595,13 @@ class _UserMemoryDetailScreenState extends State { return _buildRoutineItem(routine, index); }), const SizedBox(height: AppSpacing.sm), - _buildAddButton('添加习惯', () { - final newRoutines = List.from( - _memory!.recurringRoutines, - )..add(RecurringRoutine(name: '新习惯')); + _buildAddButton(context.l10n.settingsUserMemoryAddRoutine, () { + final newRoutines = + List.from(_memory!.recurringRoutines)..add( + RecurringRoutine( + name: context.l10n.settingsUserMemoryNewRoutine, + ), + ); _updateMemory(_memory!.copyWith(recurringRoutines: newRoutines)); }), ], @@ -606,7 +624,7 @@ class _UserMemoryDetailScreenState extends State { children: [ Expanded( child: _buildEditField( - label: '名称', + label: context.l10n.settingsUserMemoryFieldName, value: routine.name, onChanged: (value) { final newRoutines = List.from( @@ -641,7 +659,7 @@ class _UserMemoryDetailScreenState extends State { children: [ Expanded( child: _buildEditField( - label: '描述', + label: context.l10n.settingsUserMemoryFieldDescription, value: routine.description, onChanged: (value) { final newRoutines = List.from( @@ -657,7 +675,7 @@ class _UserMemoryDetailScreenState extends State { const SizedBox(width: AppSpacing.sm), Expanded( child: _buildEditField( - label: '周期', + label: context.l10n.settingsUserMemoryFieldCadence, value: routine.cadence, onChanged: (value) { final newRoutines = List.from( @@ -760,7 +778,7 @@ class _UserMemoryDetailScreenState extends State { vertical: AppSpacing.sm, ), border: InputBorder.none, - hintText: '输入$label', + hintText: context.l10n.settingsMemoryInputHint(label), hintStyle: TextStyle(color: AppColors.slate400, fontSize: 14), ), ), @@ -855,7 +873,7 @@ class _UserMemoryDetailScreenState extends State { Icon(Icons.add, size: 14, color: AppColors.slate500), const SizedBox(width: AppSpacing.xs), Text( - '添加', + context.l10n.contactsAdd, style: TextStyle(fontSize: 13, color: AppColors.slate500), ), ], @@ -873,16 +891,18 @@ class _UserMemoryDetailScreenState extends State { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('添加'), + title: Text(context.l10n.contactsAdd), content: TextField( controller: controller, autofocus: true, - decoration: const InputDecoration(hintText: '输入内容'), + decoration: InputDecoration( + hintText: context.l10n.settingsMemoryInputContent, + ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('取消'), + child: Text(context.l10n.commonCancel), ), TextButton( onPressed: () { @@ -891,7 +911,7 @@ class _UserMemoryDetailScreenState extends State { } Navigator.pop(context); }, - child: const Text('添加'), + child: Text(context.l10n.contactsAdd), ), ], ), diff --git a/apps/lib/features/settings/ui/screens/user_memory_view_screen.dart b/apps/lib/features/settings/presentation/screens/user_memory_view_screen.dart similarity index 78% rename from apps/lib/features/settings/ui/screens/user_memory_view_screen.dart rename to apps/lib/features/settings/presentation/screens/user_memory_view_screen.dart index 5221e72..40af1ef 100644 --- a/apps/lib/features/settings/ui/screens/user_memory_view_screen.dart +++ b/apps/lib/features/settings/presentation/screens/user_memory_view_screen.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:social_app/core/di/injection.dart'; -import 'package:social_app/core/router/app_routes.dart'; +import 'package:social_app/app/di/injection.dart'; +import 'package:social_app/app/router/app_routes.dart'; +import 'package:social_app/core/l10n/l10n.dart'; import 'package:social_app/core/theme/design_tokens.dart'; import 'package:social_app/shared/widgets/app_loading_indicator.dart'; import 'package:social_app/shared/widgets/app_pressable.dart'; @@ -48,7 +49,7 @@ class _UserMemoryViewScreenState extends State { } catch (_) { if (!mounted) return; setState(() { - _error = '加载失败'; + _error = L10n.current.memoryLoadFailedRetry; _isLoading = false; }); } @@ -64,13 +65,13 @@ class _UserMemoryViewScreenState extends State { @override Widget build(BuildContext context) { return SettingsPageScaffold( - title: '个人偏好', + title: context.l10n.memoryUserProfile, onBack: () => context.pop(), trailing: DetailHeaderActionMenu<_UserMemoryHeaderAction>( - items: const [ + items: [ DetailHeaderActionItem<_UserMemoryHeaderAction>( value: _UserMemoryHeaderAction.edit, - label: '编辑', + label: context.l10n.commonEdit, icon: Icons.edit_outlined, ), ], @@ -100,7 +101,10 @@ class _UserMemoryViewScreenState extends State { children: [ Icon(Icons.error_outline, size: 48, color: AppColors.slate300), const SizedBox(height: AppSpacing.md), - Text(_error ?? '加载失败', style: TextStyle(color: AppColors.slate500)), + Text( + _error ?? context.l10n.memoryLoadFailedRetry, + style: TextStyle(color: AppColors.slate500), + ), const SizedBox(height: AppSpacing.lg), AppPressable( onTap: _loadMemory, @@ -115,8 +119,8 @@ class _UserMemoryViewScreenState extends State { borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all(color: AppColors.blue100), ), - child: const Text( - '重新加载', + child: Text( + context.l10n.memoryReload, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, @@ -131,80 +135,89 @@ class _UserMemoryViewScreenState extends State { } Widget _buildContent(UserMemoryContent memory) { + final l10n = context.l10n; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildSectionCard( - title: '基本信息', + title: l10n.settingsUserMemorySectionBasic, icon: Icons.person_outline, children: [ - _buildInfoRow(Icons.work_outline, '职业', _text(memory.occupation)), - _buildInfoRow(Icons.public_outlined, '时区', _text(memory.timezone)), + _buildInfoRow( + Icons.work_outline, + l10n.settingsUserMemoryFieldOccupation, + _text(memory.occupation), + ), + _buildInfoRow( + Icons.public_outlined, + l10n.settingsUserMemoryFieldTimezone, + _text(memory.timezone), + ), _buildInfoRow( Icons.translate_outlined, - '主要语言', + l10n.settingsUserMemoryFieldPrimaryLanguage, _text(memory.primaryLanguage), ), ], ), const SizedBox(height: AppSpacing.md), _buildSectionCard( - title: '偏好设置', + title: l10n.settingsUserMemorySectionPreferences, icon: Icons.tune, children: [ _buildInfoRow( Icons.chat_bubble_outline, - '沟通风格', + l10n.settingsUserMemoryFieldCommunicationStyle, _text(memory.preferences.communicationStyle), ), _buildInfoRow( Icons.place_outlined, - '位置偏好', + l10n.settingsUserMemoryFieldLocationPreference, _text(memory.preferences.locationPreference), ), _buildInfoRow( Icons.balcony_outlined, - '工作生活方式', + l10n.settingsUserMemoryFieldWorkLifestyle, _text(memory.preferences.workLifestyle), ), _buildInfoRow( Icons.language_outlined, - '语言偏好', + l10n.settingsUserMemoryFieldLanguagePreference, _listText(memory.preferences.languagePreference), ), _buildInfoRow( Icons.notifications_outlined, - '通知偏好', + l10n.settingsUserMemoryFieldNotificationPreference, _listText(memory.preferences.notificationPreference), ), ], ), const SizedBox(height: AppSpacing.md), _buildSectionCard( - title: '日程偏好', + title: l10n.settingsUserMemorySectionSchedule, icon: Icons.schedule_outlined, children: [ _buildInfoRow( Icons.timer_outlined, - '会议缓冲时间', + l10n.settingsUserMemoryFieldMeetingBuffer, _minutesText(memory.schedulingPreferences.meetingBufferMinutes), ), _buildInfoRow( Icons.format_list_numbered, - '每日最多会议', + l10n.settingsUserMemoryFieldMaxMeetingsPerDay, _numberText(memory.schedulingPreferences.maxMeetingsPerDay), ), _buildInfoRow( Icons.timelapse_outlined, - '偏好会议时长', + l10n.settingsUserMemoryFieldPreferredMeetingDuration, _intListText( memory.schedulingPreferences.preferredMeetingDurationMinutes, - suffix: '分钟', + suffix: l10n.settingsUserMemoryMinute, ), ), _buildInfoRow( Icons.note_outlined, - '备注', + l10n.settingsUserMemoryFieldNotes, _text(memory.schedulingPreferences.notes), multiline: true, ), @@ -212,37 +225,37 @@ class _UserMemoryViewScreenState extends State { ), const SizedBox(height: AppSpacing.md), _buildSectionCard( - title: '联系人', + title: l10n.settingsUserMemorySectionContacts, icon: Icons.people_outline, children: [_buildPeople(memory.people)], ), const SizedBox(height: AppSpacing.md), _buildSectionCard( - title: '地点', + title: l10n.settingsUserMemorySectionPlaces, icon: Icons.place_outlined, children: [_buildPlaces(memory.places)], ), const SizedBox(height: AppSpacing.md), _buildSectionCard( - title: '兴趣', + title: l10n.settingsUserMemorySectionInterests, icon: Icons.interests_outlined, children: [_buildTags(memory.interests)], ), const SizedBox(height: AppSpacing.md), _buildSectionCard( - title: '回避话题', + title: l10n.settingsUserMemorySectionAvoidTopics, icon: Icons.not_interested_outlined, children: [_buildTags(memory.avoidTopics)], ), const SizedBox(height: AppSpacing.md), _buildSectionCard( - title: '自定义规则', + title: l10n.settingsUserMemorySectionCustomRules, icon: Icons.rule_folder_outlined, children: [_buildTags(memory.customRules)], ), const SizedBox(height: AppSpacing.md), _buildSectionCard( - title: '周期习惯', + title: l10n.settingsUserMemorySectionRoutines, icon: Icons.repeat, children: [_buildRoutines(memory.recurringRoutines)], ), @@ -347,7 +360,7 @@ class _UserMemoryViewScreenState extends State { Widget _buildPeople(List people) { if (people.isEmpty) { - return _buildEmptyTip('暂无联系人'); + return _buildEmptyTip(context.l10n.settingsUserMemoryEmptyContacts); } return Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -363,25 +376,29 @@ class _UserMemoryViewScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildInfoRow(Icons.badge_outlined, '姓名', _text(person.name)), + _buildInfoRow( + Icons.badge_outlined, + context.l10n.settingsUserMemoryFieldName, + _text(person.name), + ), _buildInfoRow( Icons.account_tree_outlined, - '关系', + context.l10n.settingsUserMemoryFieldRelationship, _text(person.relationship), ), _buildInfoRow( Icons.person_pin_outlined, - '角色', + context.l10n.settingsUserMemoryFieldRole, _text(person.role), ), _buildInfoRow( Icons.phone_outlined, - '联系方式', + context.l10n.settingsUserMemoryFieldContact, _text(person.preferredContactChannel), ), _buildInfoRow( Icons.note_outlined, - '备注', + context.l10n.settingsUserMemoryFieldNotes, _text(person.notes), multiline: true, ), @@ -394,7 +411,7 @@ class _UserMemoryViewScreenState extends State { Widget _buildPlaces(List places) { if (places.isEmpty) { - return _buildEmptyTip('暂无地点'); + return _buildEmptyTip(context.l10n.settingsUserMemoryEmptyPlaces); } return Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -410,18 +427,26 @@ class _UserMemoryViewScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildInfoRow(Icons.place_outlined, '名称', _text(place.name)), + _buildInfoRow( + Icons.place_outlined, + context.l10n.settingsUserMemoryFieldName, + _text(place.name), + ), _buildInfoRow( Icons.category_outlined, - '类别', + context.l10n.settingsUserMemoryFieldCategory, _text(place.category), ), _buildInfoRow( Icons.favorite_border, - '偏好', + context.l10n.settingsUserMemoryFieldPreference, _text(place.preference), ), - _buildInfoRow(Icons.map_outlined, '地址', _text(place.address)), + _buildInfoRow( + Icons.map_outlined, + context.l10n.settingsUserMemoryFieldAddress, + _text(place.address), + ), ], ), ); @@ -431,7 +456,7 @@ class _UserMemoryViewScreenState extends State { Widget _buildRoutines(List routines) { if (routines.isEmpty) { - return _buildEmptyTip('暂无周期习惯'); + return _buildEmptyTip(context.l10n.settingsUserMemoryEmptyRoutines); } return Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -447,14 +472,22 @@ class _UserMemoryViewScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildInfoRow(Icons.title_outlined, '名称', _text(routine.name)), + _buildInfoRow( + Icons.title_outlined, + context.l10n.settingsUserMemoryFieldName, + _text(routine.name), + ), _buildInfoRow( Icons.subject_outlined, - '描述', + context.l10n.settingsUserMemoryFieldDescription, _text(routine.description), multiline: true, ), - _buildInfoRow(Icons.repeat_one, '周期', _text(routine.cadence)), + _buildInfoRow( + Icons.repeat_one, + context.l10n.settingsUserMemoryFieldCadence, + _text(routine.cadence), + ), ], ), ); @@ -464,7 +497,7 @@ class _UserMemoryViewScreenState extends State { Widget _buildTags(List tags) { if (tags.isEmpty) { - return _buildEmptyTip('暂无数据'); + return _buildEmptyTip(context.l10n.memoryNoInfo); } return Wrap( spacing: AppSpacing.sm, @@ -529,26 +562,26 @@ class _UserMemoryViewScreenState extends State { String _text(String? value) { final raw = value?.trim() ?? ''; - return raw.isEmpty ? '未设置' : raw; + return raw.isEmpty ? context.l10n.settingsUnset : raw; } String _listText(List values) { - if (values.isEmpty) return '未设置'; + if (values.isEmpty) return context.l10n.settingsUnset; return values.join('、'); } String _intListText(List values, {required String suffix}) { - if (values.isEmpty) return '未设置'; + if (values.isEmpty) return context.l10n.settingsUnset; return values.map((value) => '$value$suffix').join('、'); } String _minutesText(int? value) { - if (value == null) return '未设置'; - return '$value 分钟'; + if (value == null) return context.l10n.settingsUnset; + return context.l10n.settingsUserMemoryMinutesValue(value); } String _numberText(int? value) { - if (value == null) return '未设置'; + if (value == null) return context.l10n.settingsUnset; return '$value'; } } diff --git a/apps/lib/features/settings/ui/screens/work_memory_detail_screen.dart b/apps/lib/features/settings/presentation/screens/work_memory_detail_screen.dart similarity index 88% rename from apps/lib/features/settings/ui/screens/work_memory_detail_screen.dart rename to apps/lib/features/settings/presentation/screens/work_memory_detail_screen.dart index da520be..0b010fe 100644 --- a/apps/lib/features/settings/ui/screens/work_memory_detail_screen.dart +++ b/apps/lib/features/settings/presentation/screens/work_memory_detail_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:social_app/core/di/injection.dart'; +import 'package:social_app/app/di/injection.dart'; +import 'package:social_app/core/l10n/l10n.dart'; import 'package:social_app/core/theme/design_tokens.dart'; import 'package:social_app/shared/widgets/app_loading_indicator.dart'; import 'package:social_app/shared/widgets/app_pressable.dart'; @@ -48,7 +49,7 @@ class _WorkMemoryDetailScreenState extends State { } catch (e) { if (!mounted) return; setState(() { - _error = '加载失败'; + _error = L10n.current.memoryLoadFailedRetry; _isLoading = false; }); } @@ -69,13 +70,21 @@ class _WorkMemoryDetailScreenState extends State { _isSaving = false; _hasChanges = false; }); - Toast.show(context, '保存成功', type: ToastType.success); + Toast.show( + context, + context.l10n.settingsMemorySaveSuccess, + type: ToastType.success, + ); } catch (e) { if (!mounted) return; setState(() { _isSaving = false; }); - Toast.show(context, '保存失败', type: ToastType.error); + Toast.show( + context, + context.l10n.settingsMemorySaveFailed, + type: ToastType.error, + ); } } @@ -89,7 +98,7 @@ class _WorkMemoryDetailScreenState extends State { @override Widget build(BuildContext context) { return SettingsPageScaffold( - title: '编辑工作画像', + title: context.l10n.settingsWorkMemoryEditTitle, onBack: () => context.pop(), footer: _hasChanges ? _buildSaveButton() : null, body: Column( @@ -123,7 +132,10 @@ class _WorkMemoryDetailScreenState extends State { children: [ Icon(Icons.error_outline, size: 48, color: AppColors.slate300), const SizedBox(height: AppSpacing.md), - Text(_error ?? '加载失败', style: TextStyle(color: AppColors.slate500)), + Text( + _error ?? context.l10n.memoryLoadFailedRetry, + style: TextStyle(color: AppColors.slate500), + ), const SizedBox(height: AppSpacing.lg), AppPressable( onTap: _loadMemory, @@ -138,8 +150,8 @@ class _WorkMemoryDetailScreenState extends State { borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all(color: AppColors.blue100), ), - child: const Text( - '重新加载', + child: Text( + context.l10n.memoryReload, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, @@ -160,7 +172,10 @@ class _WorkMemoryDetailScreenState extends State { children: [ Icon(Icons.work_off_outlined, size: 48, color: AppColors.slate300), const SizedBox(height: AppSpacing.md), - Text('暂无工作信息', style: TextStyle(color: AppColors.slate500)), + Text( + context.l10n.settingsWorkMemoryEmptyProfile, + style: TextStyle(color: AppColors.slate500), + ), ], ), ); @@ -191,8 +206,8 @@ class _WorkMemoryDetailScreenState extends State { child: Center( child: _isSaving ? const AppLoadingIndicator(variant: AppLoadingVariant.button) - : const Text( - '保存更改', + : Text( + context.l10n.todoSaveChanges, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, @@ -231,11 +246,11 @@ class _WorkMemoryDetailScreenState extends State { Widget _buildBasicInfoSection() { return _buildSection( - title: '基本信息', + title: context.l10n.settingsWorkMemorySectionBasic, icon: Icons.work_outline, children: [ _buildEditField( - label: '职业', + label: context.l10n.settingsWorkMemoryFieldOccupation, value: _memory?.occupation, onChanged: (value) => _updateMemory(_memory!.copyWith(occupation: value)), @@ -246,7 +261,7 @@ class _WorkMemoryDetailScreenState extends State { Widget _buildExpertiseSection() { return _buildSection( - title: '专长', + title: context.l10n.settingsWorkMemorySectionExpertise, icon: Icons.psychology_outlined, count: _memory?.expertise.length ?? 0, children: [ @@ -269,7 +284,7 @@ class _WorkMemoryDetailScreenState extends State { Widget _buildPreferredToolsSection() { return _buildSection( - title: '偏好工具', + title: context.l10n.settingsWorkMemorySectionPreferredTools, icon: Icons.build_outlined, count: _memory?.preferredTools.length ?? 0, children: [ @@ -294,12 +309,12 @@ class _WorkMemoryDetailScreenState extends State { Widget _buildProjectsSection() { return _buildSection( - title: '当前项目', + title: context.l10n.settingsWorkMemorySectionCurrentProjects, icon: Icons.folder_outlined, count: _memory?.currentProjects.length ?? 0, children: [ if (_memory?.currentProjects.isEmpty ?? true) - _buildEmptySection('暂无项目') + _buildEmptySection(context.l10n.settingsWorkMemoryEmptyProjects) else ..._memory!.currentProjects.asMap().entries.map((entry) { final index = entry.key; @@ -307,10 +322,11 @@ class _WorkMemoryDetailScreenState extends State { return _buildProjectItem(project, index); }), const SizedBox(height: AppSpacing.sm), - _buildAddButton('添加项目', () { - final newProjects = List.from( - _memory!.currentProjects, - )..add(CurrentProject(name: '新项目')); + _buildAddButton(context.l10n.settingsWorkMemoryAddProject, () { + final newProjects = + List.from(_memory!.currentProjects)..add( + CurrentProject(name: context.l10n.settingsWorkMemoryNewProject), + ); _updateMemory(_memory!.copyWith(currentProjects: newProjects)); }), ], @@ -333,7 +349,7 @@ class _WorkMemoryDetailScreenState extends State { children: [ Expanded( child: _buildEditField( - label: '项目名称', + label: context.l10n.settingsWorkMemoryFieldProjectName, value: project.name, onChanged: (value) { final newProjects = List.from( @@ -368,7 +384,7 @@ class _WorkMemoryDetailScreenState extends State { children: [ Expanded( child: _buildEditField( - label: '状态', + label: context.l10n.settingsWorkMemoryFieldStatus, value: project.status, onChanged: (value) { final newProjects = List.from( @@ -384,7 +400,7 @@ class _WorkMemoryDetailScreenState extends State { const SizedBox(width: AppSpacing.sm), Expanded( child: _buildEditField( - label: '优先级', + label: context.l10n.settingsWorkMemoryFieldPriority, value: project.priority, onChanged: (value) { final newProjects = List.from( @@ -401,7 +417,7 @@ class _WorkMemoryDetailScreenState extends State { ), const SizedBox(height: AppSpacing.sm), _buildEditField( - label: '描述', + label: context.l10n.settingsUserMemoryFieldDescription, value: project.description, onChanged: (value) { final newProjects = List.from( @@ -413,7 +429,7 @@ class _WorkMemoryDetailScreenState extends State { ), const SizedBox(height: AppSpacing.sm), _buildEditField( - label: '截止日期', + label: context.l10n.settingsWorkMemoryFieldDeadline, value: project.deadline?.toIso8601String().split('T').first, onChanged: (value) { final newProjects = List.from( @@ -432,12 +448,12 @@ class _WorkMemoryDetailScreenState extends State { Widget _buildTeamMembersSection() { return _buildSection( - title: '团队成员', + title: context.l10n.settingsWorkMemorySectionTeamMembers, icon: Icons.groups_outlined, count: _memory?.teamMembers.length ?? 0, children: [ if (_memory?.teamMembers.isEmpty ?? true) - _buildEmptySection('暂无团队成员') + _buildEmptySection(context.l10n.settingsWorkMemoryEmptyTeamMembers) else ..._memory!.teamMembers.asMap().entries.map((entry) { final index = entry.key; @@ -445,9 +461,9 @@ class _WorkMemoryDetailScreenState extends State { return _buildTeamMemberItem(member, index); }), const SizedBox(height: AppSpacing.sm), - _buildAddButton('添加成员', () { + _buildAddButton(context.l10n.settingsWorkMemoryAddMember, () { final newMembers = List.from(_memory!.teamMembers) - ..add(TeamMember(name: '新成员')); + ..add(TeamMember(name: context.l10n.settingsWorkMemoryNewMember)); _updateMemory(_memory!.copyWith(teamMembers: newMembers)); }), ], @@ -470,7 +486,7 @@ class _WorkMemoryDetailScreenState extends State { children: [ Expanded( child: _buildEditField( - label: '姓名', + label: context.l10n.settingsUserMemoryFieldName, value: member.name, onChanged: (value) { final newMembers = List.from( @@ -500,7 +516,7 @@ class _WorkMemoryDetailScreenState extends State { children: [ Expanded( child: _buildEditField( - label: '角色', + label: context.l10n.settingsUserMemoryFieldRole, value: member.role, onChanged: (value) { final newMembers = List.from( @@ -514,7 +530,7 @@ class _WorkMemoryDetailScreenState extends State { const SizedBox(width: AppSpacing.sm), Expanded( child: _buildEditField( - label: '关系', + label: context.l10n.settingsUserMemoryFieldRelationship, value: member.relationship, onChanged: (value) { final newMembers = List.from( @@ -529,7 +545,7 @@ class _WorkMemoryDetailScreenState extends State { ), const SizedBox(height: AppSpacing.sm), _buildEditField( - label: '联系方式', + label: context.l10n.settingsUserMemoryFieldContact, value: member.preferredContactChannel, onChanged: (value) { final newMembers = List.from(_memory!.teamMembers); @@ -541,7 +557,7 @@ class _WorkMemoryDetailScreenState extends State { ), const SizedBox(height: AppSpacing.sm), _buildEditField( - label: '备注', + label: context.l10n.settingsUserMemoryFieldNotes, value: member.notes, onChanged: (value) { final newMembers = List.from(_memory!.teamMembers); @@ -557,11 +573,11 @@ class _WorkMemoryDetailScreenState extends State { Widget _buildWorkHabitsSection() { final habits = _memory!.workHabits; return _buildSection( - title: '工作习惯', + title: context.l10n.settingsWorkMemorySectionWorkHabits, icon: Icons.schedule_outlined, children: [ _buildEditField( - label: '通知渠道', + label: context.l10n.settingsWorkMemoryFieldNotificationChannel, value: habits.notificationChannel, onChanged: (value) { _updateMemory( @@ -573,7 +589,7 @@ class _WorkMemoryDetailScreenState extends State { ), const SizedBox(height: AppSpacing.sm), _buildEditField( - label: '备注', + label: context.l10n.settingsWorkMemoryFieldNotes, value: habits.notes, onChanged: (value) { _updateMemory( @@ -587,11 +603,11 @@ class _WorkMemoryDetailScreenState extends State { Widget _buildTeamContextSection() { return _buildSection( - title: '团队背景', + title: context.l10n.settingsWorkMemorySectionTeamContext, icon: Icons.business_outlined, children: [ _buildEditField( - label: '团队背景描述', + label: context.l10n.settingsWorkMemoryFieldTeamContext, value: _memory?.teamContext, onChanged: (value) => _updateMemory(_memory!.copyWith(teamContext: value)), @@ -602,7 +618,7 @@ class _WorkMemoryDetailScreenState extends State { Widget _buildWorkRulesSection() { return _buildSection( - title: '工作规则', + title: context.l10n.settingsWorkMemorySectionWorkRules, icon: Icons.rule_outlined, children: [ _buildTagsSection( @@ -705,7 +721,7 @@ class _WorkMemoryDetailScreenState extends State { vertical: AppSpacing.sm, ), border: InputBorder.none, - hintText: '输入$label', + hintText: context.l10n.settingsMemoryInputHint(label), hintStyle: TextStyle(color: AppColors.slate400, fontSize: 14), ), ), @@ -802,7 +818,7 @@ class _WorkMemoryDetailScreenState extends State { Icon(Icons.add, size: 14, color: AppColors.slate500), const SizedBox(width: AppSpacing.xs), Text( - '添加', + context.l10n.contactsAdd, style: TextStyle(fontSize: 13, color: AppColors.slate500), ), ], @@ -820,16 +836,18 @@ class _WorkMemoryDetailScreenState extends State { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('添加'), + title: Text(context.l10n.contactsAdd), content: TextField( controller: controller, autofocus: true, - decoration: const InputDecoration(hintText: '输入内容'), + decoration: InputDecoration( + hintText: context.l10n.settingsMemoryInputContent, + ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('取消'), + child: Text(context.l10n.commonCancel), ), TextButton( onPressed: () { @@ -838,7 +856,7 @@ class _WorkMemoryDetailScreenState extends State { } Navigator.pop(context); }, - child: const Text('添加'), + child: Text(context.l10n.contactsAdd), ), ], ), diff --git a/apps/lib/features/settings/ui/screens/work_memory_view_screen.dart b/apps/lib/features/settings/presentation/screens/work_memory_view_screen.dart similarity index 79% rename from apps/lib/features/settings/ui/screens/work_memory_view_screen.dart rename to apps/lib/features/settings/presentation/screens/work_memory_view_screen.dart index 84bf490..ce45b6a 100644 --- a/apps/lib/features/settings/ui/screens/work_memory_view_screen.dart +++ b/apps/lib/features/settings/presentation/screens/work_memory_view_screen.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:social_app/core/di/injection.dart'; -import 'package:social_app/core/router/app_routes.dart'; +import 'package:social_app/app/di/injection.dart'; +import 'package:social_app/app/router/app_routes.dart'; +import 'package:social_app/core/l10n/l10n.dart'; import 'package:social_app/core/theme/design_tokens.dart'; import 'package:social_app/shared/widgets/app_loading_indicator.dart'; import 'package:social_app/shared/widgets/app_pressable.dart'; @@ -48,7 +49,7 @@ class _WorkMemoryViewScreenState extends State { } catch (_) { if (!mounted) return; setState(() { - _error = '加载失败'; + _error = L10n.current.memoryLoadFailedRetry; _isLoading = false; }); } @@ -64,13 +65,13 @@ class _WorkMemoryViewScreenState extends State { @override Widget build(BuildContext context) { return SettingsPageScaffold( - title: '工作画像', + title: context.l10n.memoryWorkProfile, onBack: () => context.pop(), trailing: DetailHeaderActionMenu<_WorkMemoryHeaderAction>( - items: const [ + items: [ DetailHeaderActionItem<_WorkMemoryHeaderAction>( value: _WorkMemoryHeaderAction.edit, - label: '编辑', + label: context.l10n.commonEdit, icon: Icons.edit_outlined, ), ], @@ -100,7 +101,10 @@ class _WorkMemoryViewScreenState extends State { children: [ Icon(Icons.error_outline, size: 48, color: AppColors.slate300), const SizedBox(height: AppSpacing.md), - Text(_error ?? '加载失败', style: TextStyle(color: AppColors.slate500)), + Text( + _error ?? context.l10n.memoryLoadFailedRetry, + style: TextStyle(color: AppColors.slate500), + ), const SizedBox(height: AppSpacing.lg), AppPressable( onTap: _loadMemory, @@ -115,8 +119,8 @@ class _WorkMemoryViewScreenState extends State { borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all(color: AppColors.blue100), ), - child: const Text( - '重新加载', + child: Text( + context.l10n.memoryReload, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, @@ -131,81 +135,86 @@ class _WorkMemoryViewScreenState extends State { } Widget _buildContent(WorkProfileContent memory) { + final l10n = context.l10n; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildSectionCard( - title: '基本信息', + title: l10n.settingsWorkMemorySectionBasic, icon: Icons.work_outline, children: [ - _buildInfoRow(Icons.badge_outlined, '职业', _text(memory.occupation)), + _buildInfoRow( + Icons.badge_outlined, + l10n.settingsWorkMemoryFieldOccupation, + _text(memory.occupation), + ), ], ), const SizedBox(height: AppSpacing.md), _buildSectionCard( - title: '专长', + title: l10n.settingsWorkMemorySectionExpertise, icon: Icons.psychology_outlined, children: [_buildTags(memory.expertise)], ), const SizedBox(height: AppSpacing.md), _buildSectionCard( - title: '偏好工具', + title: l10n.settingsWorkMemorySectionPreferredTools, icon: Icons.build_outlined, children: [_buildTags(memory.preferredTools)], ), const SizedBox(height: AppSpacing.md), _buildSectionCard( - title: '当前项目', + title: l10n.settingsWorkMemorySectionCurrentProjects, icon: Icons.folder_outlined, children: [_buildProjects(memory.currentProjects)], ), const SizedBox(height: AppSpacing.md), _buildSectionCard( - title: '团队成员', + title: l10n.settingsWorkMemorySectionTeamMembers, icon: Icons.groups_outlined, children: [_buildTeamMembers(memory.teamMembers)], ), const SizedBox(height: AppSpacing.md), _buildSectionCard( - title: '工作习惯', + title: l10n.settingsWorkMemorySectionWorkHabits, icon: Icons.schedule_outlined, children: [ _buildInfoRow( Icons.timelapse_outlined, - '可用时段', + l10n.settingsWorkMemoryFieldAvailableHours, _timeWindowSummary(memory.workHabits.availableHours), ), _buildInfoRow( Icons.flash_on_outlined, - '深度工作时段', + l10n.settingsWorkMemoryFieldDeepWorkBlocks, _timeWindowSummary(memory.workHabits.deepWorkBlocks), ), _buildInfoRow( Icons.meeting_room_outlined, - '偏好会议时段', + l10n.settingsWorkMemoryFieldPreferredMeetingWindows, _timeWindowSummary(memory.workHabits.preferredMeetingWindows), ), _buildInfoRow( Icons.do_not_disturb_alt_outlined, - '免打扰时段', + l10n.settingsWorkMemoryFieldNoMeetingWindows, _timeWindowSummary(memory.workHabits.noMeetingWindows), ), _buildInfoRow( Icons.timer_outlined, - '偏好会议时长', + l10n.settingsWorkMemoryFieldPreferredMeetingDuration, _intListText( memory.workHabits.preferredMeetingDurationMinutes, - suffix: '分钟', + suffix: l10n.settingsWorkMemoryMinute, ), ), _buildInfoRow( Icons.notifications_outlined, - '通知渠道', + l10n.settingsWorkMemoryFieldNotificationChannel, _text(memory.workHabits.notificationChannel), ), _buildInfoRow( Icons.note_outlined, - '备注', + l10n.settingsWorkMemoryFieldNotes, _text(memory.workHabits.notes), multiline: true, ), @@ -213,12 +222,12 @@ class _WorkMemoryViewScreenState extends State { ), const SizedBox(height: AppSpacing.md), _buildSectionCard( - title: '团队背景', + title: l10n.settingsWorkMemorySectionTeamContext, icon: Icons.business_outlined, children: [ _buildInfoRow( Icons.apartment_outlined, - '团队背景描述', + l10n.settingsWorkMemoryFieldTeamContext, _text(memory.teamContext), multiline: true, ), @@ -226,7 +235,7 @@ class _WorkMemoryViewScreenState extends State { ), const SizedBox(height: AppSpacing.md), _buildSectionCard( - title: '工作规则', + title: l10n.settingsWorkMemorySectionWorkRules, icon: Icons.rule_outlined, children: [_buildTags(memory.workRules)], ), @@ -331,7 +340,7 @@ class _WorkMemoryViewScreenState extends State { Widget _buildProjects(List projects) { if (projects.isEmpty) { - return _buildEmptyTip('暂无项目'); + return _buildEmptyTip(context.l10n.settingsWorkMemoryEmptyProjects); } return Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -347,41 +356,51 @@ class _WorkMemoryViewScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildInfoRow(Icons.title_outlined, '项目名称', _text(project.name)), + _buildInfoRow( + Icons.title_outlined, + context.l10n.settingsWorkMemoryFieldProjectName, + _text(project.name), + ), _buildInfoRow( Icons.subject_outlined, - '描述', + context.l10n.settingsUserMemoryFieldDescription, _text(project.description), multiline: true, ), - _buildInfoRow(Icons.flag_outlined, '状态', _text(project.status)), + _buildInfoRow( + Icons.flag_outlined, + context.l10n.settingsWorkMemoryFieldStatus, + _text(project.status), + ), _buildInfoRow( Icons.priority_high_outlined, - '优先级', + context.l10n.settingsWorkMemoryFieldPriority, _text(project.priority), ), _buildInfoRow( Icons.event_outlined, - '截止日期', + context.l10n.settingsWorkMemoryFieldDeadline, project.deadline == null - ? '未设置' + ? context.l10n.settingsUnset : project.deadline!.toIso8601String().split('T').first, ), _buildInfoRow( Icons.group_add_outlined, - '协作人', + context.l10n.settingsWorkMemoryFieldCollaborators, _listText(project.collaborators), ), _buildInfoRow( Icons.emoji_events_outlined, - '关键里程碑', + context.l10n.settingsWorkMemoryFieldMilestones, project.keyMilestones.isEmpty - ? '未设置' - : '${project.keyMilestones.length} 项', + ? context.l10n.settingsUnset + : context.l10n.settingsWorkMemoryMilestoneCount( + project.keyMilestones.length, + ), ), _buildInfoRow( Icons.note_alt_outlined, - '备注', + context.l10n.settingsWorkMemoryFieldNotes, _text(project.notes), ), ], @@ -393,7 +412,7 @@ class _WorkMemoryViewScreenState extends State { Widget _buildTeamMembers(List members) { if (members.isEmpty) { - return _buildEmptyTip('暂无团队成员'); + return _buildEmptyTip(context.l10n.settingsWorkMemoryEmptyTeamMembers); } return Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -409,23 +428,31 @@ class _WorkMemoryViewScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildInfoRow(Icons.badge_outlined, '姓名', _text(member.name)), + _buildInfoRow( + Icons.badge_outlined, + context.l10n.settingsUserMemoryFieldName, + _text(member.name), + ), _buildInfoRow( Icons.person_pin_outlined, - '角色', + context.l10n.settingsUserMemoryFieldRole, _text(member.role), ), _buildInfoRow( Icons.account_tree_outlined, - '关系', + context.l10n.settingsUserMemoryFieldRelationship, _text(member.relationship), ), _buildInfoRow( Icons.phone_outlined, - '联系方式', + context.l10n.settingsUserMemoryFieldContact, _text(member.preferredContactChannel), ), - _buildInfoRow(Icons.note_outlined, '备注', _text(member.notes)), + _buildInfoRow( + Icons.note_outlined, + context.l10n.settingsUserMemoryFieldNotes, + _text(member.notes), + ), ], ), ); @@ -435,7 +462,7 @@ class _WorkMemoryViewScreenState extends State { Widget _buildTags(List tags) { if (tags.isEmpty) { - return _buildEmptyTip('暂无数据'); + return _buildEmptyTip(context.l10n.memoryNoInfo); } return Wrap( spacing: AppSpacing.sm, @@ -502,21 +529,21 @@ class _WorkMemoryViewScreenState extends State { String _text(String? value) { final raw = value?.trim() ?? ''; - return raw.isEmpty ? '未设置' : raw; + return raw.isEmpty ? context.l10n.settingsUnset : raw; } String _listText(List values) { - if (values.isEmpty) return '未设置'; + if (values.isEmpty) return context.l10n.settingsUnset; return values.join('、'); } String _intListText(List values, {required String suffix}) { - if (values.isEmpty) return '未设置'; + if (values.isEmpty) return context.l10n.settingsUnset; return values.map((value) => '$value$suffix').join('、'); } String _timeWindowSummary(List windows) { - if (windows.isEmpty) return '未设置'; - return '${windows.length} 个时段'; + if (windows.isEmpty) return context.l10n.settingsUnset; + return context.l10n.settingsWorkMemoryTimeWindowCount(windows.length); } } diff --git a/apps/lib/features/settings/ui/widgets/account_section_card.dart b/apps/lib/features/settings/presentation/widgets/account_section_card.dart similarity index 100% rename from apps/lib/features/settings/ui/widgets/account_section_card.dart rename to apps/lib/features/settings/presentation/widgets/account_section_card.dart diff --git a/apps/lib/features/settings/ui/widgets/settings_page_scaffold.dart b/apps/lib/features/settings/presentation/widgets/settings_page_scaffold.dart similarity index 100% rename from apps/lib/features/settings/ui/widgets/settings_page_scaffold.dart rename to apps/lib/features/settings/presentation/widgets/settings_page_scaffold.dart diff --git a/apps/lib/features/todo/data/todo_api.dart b/apps/lib/features/todo/data/todo_api.dart index 70cc419..e422a08 100644 --- a/apps/lib/features/todo/data/todo_api.dart +++ b/apps/lib/features/todo/data/todo_api.dart @@ -1,4 +1,4 @@ -import 'package:social_app/core/api/i_api_client.dart'; +import 'package:social_app/core/network/i_api_client.dart'; class TodoApi { final IApiClient _client; diff --git a/apps/lib/features/todo/ui/screens/todo_detail_screen.dart b/apps/lib/features/todo/presentation/screens/todo_detail_screen.dart similarity index 84% rename from apps/lib/features/todo/ui/screens/todo_detail_screen.dart rename to apps/lib/features/todo/presentation/screens/todo_detail_screen.dart index b3dc750..23b337b 100644 --- a/apps/lib/features/todo/ui/screens/todo_detail_screen.dart +++ b/apps/lib/features/todo/presentation/screens/todo_detail_screen.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; import 'package:lucide_icons/lucide_icons.dart'; -import '../../../../core/di/injection.dart'; -import '../../../../core/router/app_routes.dart'; +import '../../../../app/di/injection.dart'; +import '../../../../app/router/app_routes.dart'; +import '../../../../core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/back_title_page_header.dart'; import '../../../../shared/widgets/detail_header_action_menu.dart'; @@ -70,15 +72,15 @@ class _TodoDetailScreenState extends State { String _getPriorityLabel(int priority) { switch (priority) { case 1: - return '重要紧急'; + return context.l10n.todoQuadrantImportantUrgent; case 2: - return '重要不紧急'; + return context.l10n.todoQuadrantImportantNotUrgent; case 3: - return '紧急不重要'; + return context.l10n.todoQuadrantUrgentNotImportant; case 4: - return '不紧急不重要'; + return context.l10n.todoQuadrantNotUrgentNotImportant; default: - return '未知'; + return context.l10n.commonUnknown; } } @@ -122,7 +124,7 @@ class _TodoDetailScreenState extends State { Widget _buildHeader() { return BackTitlePageHeader( - title: '待办详情', + title: context.l10n.todoDetailTitle, onBack: () => context.pop(_didMutate), trailing: _buildHeaderMenu(), ); @@ -133,15 +135,15 @@ class _TodoDetailScreenState extends State { return null; } return DetailHeaderActionMenu<_TodoHeaderAction>( - items: const [ + items: [ DetailHeaderActionItem<_TodoHeaderAction>( value: _TodoHeaderAction.edit, - label: '编辑', + label: context.l10n.commonEdit, icon: LucideIcons.pencil, ), DetailHeaderActionItem<_TodoHeaderAction>( value: _TodoHeaderAction.delete, - label: '删除', + label: context.l10n.commonDelete, icon: LucideIcons.trash2, isDestructive: true, ), @@ -167,11 +169,14 @@ class _TodoDetailScreenState extends State { } if (_error != null) { - return ErrorRetrySurface(message: '加载失败: $_error', onRetry: _loadTodo); + return ErrorRetrySurface( + message: context.l10n.commonLoadFailed(_error!), + onRetry: _loadTodo, + ); } if (_todo == null) { - return const Center(child: Text('待办不存在')); + return Center(child: Text(context.l10n.todoNotFound)); } return Padding( @@ -182,7 +187,7 @@ class _TodoDetailScreenState extends State { const SizedBox(height: 12), if (_todo!.scheduleItems.isNotEmpty) ...[ Text( - '日历事件卡片', + context.l10n.todoCalendarEventCards, style: TextStyle( fontFamily: 'Inter', fontSize: 12, @@ -208,8 +213,9 @@ class _TodoDetailScreenState extends State { } String _formatEventTime(DateTime start, DateTime? end) { - final startStr = - '${start.year}年${start.month}月${start.day}日 ${start.hour.toString().padLeft(2, '0')}:${start.minute.toString().padLeft(2, '0')}'; + final startStr = DateFormat.yMd( + context.l10n.localeName, + ).add_Hm().format(start); if (end != null) { final endStr = '${end.hour.toString().padLeft(2, '0')}:${end.minute.toString().padLeft(2, '0')}'; @@ -264,20 +270,22 @@ class _TodoDetailScreenState extends State { Container(height: 1, color: AppColors.border), const SizedBox(height: 8), _buildInfoRow( - label: '所属象限', + label: context.l10n.todoPriorityQuadrant, value: _getPriorityLabel(_todo!.priority), valueColor: _getPriorityColor(_todo!.priority), ), const SizedBox(height: 8), _buildInfoRow( - label: '关联日历事件', - value: '${_todo!.scheduleItems.length}个', + label: context.l10n.todoLinkedCalendarEvents, + value: context.l10n.todoItemCount(_todo!.scheduleItems.length), valueColor: AppColors.g3Text, ), const SizedBox(height: 8), _buildInfoRow( - label: '状态', - value: _todo!.status == 'done' ? '已完成' : '进行中', + label: context.l10n.todoStatus, + value: _todo!.status == 'done' + ? context.l10n.todoStatusDone + : context.l10n.todoStatusInProgress, valueColor: _todo!.status == 'done' ? AppColors.success : AppColors.blue600, @@ -289,11 +297,11 @@ class _TodoDetailScreenState extends State { String _buildSubtitle() { final parts = []; - parts.add('象限内顺序 #${_todo!.order + 1}'); + parts.add(context.l10n.todoQuadrantOrder(_todo!.order + 1)); if (_todo!.scheduleItems.isNotEmpty) { - parts.add('已拆分为${_todo!.scheduleItems.length}个日历事件'); + parts.add(context.l10n.todoSplitToEvents(_todo!.scheduleItems.length)); } else { - parts.add('未关联日历事件'); + parts.add(context.l10n.todoNoLinkedEvents); } return parts.join(' · '); } @@ -391,9 +399,9 @@ class _TodoDetailScreenState extends State { void _deleteTodo() async { final confirm = await showDestructiveActionSheet( context, - title: '删除待办', - message: '确定要删除这个待办吗?', - confirmText: '确认删除', + title: context.l10n.todoDeleteTitle, + message: context.l10n.todoDeleteMessage, + confirmText: context.l10n.todoDeleteConfirm, ); if (confirm == true) { @@ -404,7 +412,11 @@ class _TodoDetailScreenState extends State { } } catch (e) { if (mounted) { - Toast.show(context, '删除失败: $e', type: ToastType.error); + Toast.show( + context, + context.l10n.todoDeleteFailed(e.toString()), + type: ToastType.error, + ); } } } diff --git a/apps/lib/features/todo/ui/screens/todo_edit_screen.dart b/apps/lib/features/todo/presentation/screens/todo_edit_screen.dart similarity index 89% rename from apps/lib/features/todo/ui/screens/todo_edit_screen.dart rename to apps/lib/features/todo/presentation/screens/todo_edit_screen.dart index 5da20c3..af888d5 100644 --- a/apps/lib/features/todo/ui/screens/todo_edit_screen.dart +++ b/apps/lib/features/todo/presentation/screens/todo_edit_screen.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; -import '../../../../core/di/injection.dart'; +import '../../../../app/di/injection.dart'; +import '../../../../core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/app_pressable.dart'; @@ -134,7 +136,9 @@ class _TodoEditScreenState extends State { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ BackTitlePageHeader( - title: widget.isCreateMode ? '新建待办' : '编辑待办', + title: widget.isCreateMode + ? context.l10n.todoCreateTitle + : context.l10n.todoEditTitle, ), Expanded(child: _buildBody()), _buildBottomAction(), @@ -152,11 +156,14 @@ class _TodoEditScreenState extends State { } if (_error != null) { - return ErrorRetrySurface(message: '加载失败: $_error', onRetry: _loadPage); + return ErrorRetrySurface( + message: context.l10n.commonLoadFailed(_error!), + onRetry: _loadPage, + ); } if (!widget.isCreateMode && _todo == null) { - return const Center(child: Text('待办不存在')); + return Center(child: Text(context.l10n.todoNotFound)); } return ListView( @@ -194,8 +201,8 @@ class _TodoEditScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - '待办信息', + Text( + context.l10n.todoInfoTitle, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, @@ -205,10 +212,10 @@ class _TodoEditScreenState extends State { const SizedBox(height: AppSpacing.xs), Text( widget.isCreateMode - ? '创建后可在四象限中查看并继续调整优先级与关联事件。' + ? context.l10n.todoInfoDescCreate : _todo?.status == 'done' - ? '该待办已完成,你仍可调整内容并重新组织关联事件。' - : '调整标题、优先级和关联事件,保持任务结构清晰。', + ? context.l10n.todoInfoDescDone + : context.l10n.todoInfoDescDefault, style: const TextStyle(fontSize: 13, color: AppColors.slate500), ), ], @@ -229,19 +236,19 @@ class _TodoEditScreenState extends State { children: [ AppSheetInputField( controller: _titleController, - label: '标题', - hint: '输入待办标题', + label: context.l10n.todoFieldTitle, + hint: context.l10n.todoFieldTitleHint, ), const SizedBox(height: AppSpacing.lg), AppSheetInputField( controller: _descriptionController, - label: '描述(可选)', - hint: '补充细节或备注', + label: context.l10n.todoFieldDescriptionOptional, + hint: context.l10n.todoFieldDescriptionHint, maxLines: 2, ), const SizedBox(height: AppSpacing.lg), - const Text( - '优先级', + Text( + context.l10n.todoPriority, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, @@ -254,21 +261,21 @@ class _TodoEditScreenState extends State { runSpacing: AppSpacing.sm, children: [ _PriorityPill( - label: '重要紧急', + label: context.l10n.todoQuadrantImportantUrgent, selected: _priority == 1, borderColor: AppColors.g1Border, activeColor: AppColors.g1Text, onTap: () => setState(() => _priority = 1), ), _PriorityPill( - label: '紧急不重要', + label: context.l10n.todoQuadrantUrgentNotImportant, selected: _priority == 3, borderColor: AppColors.g2Border, activeColor: AppColors.g2Text, onTap: () => setState(() => _priority = 3), ), _PriorityPill( - label: '重要不紧急', + label: context.l10n.todoQuadrantImportantNotUrgent, selected: _priority == 2, borderColor: AppColors.g3Border, activeColor: AppColors.g3Text, @@ -295,8 +302,8 @@ class _TodoEditScreenState extends State { Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - const Text( - '关联日历事件', + Text( + context.l10n.todoLinkedCalendarEvents, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, @@ -305,7 +312,7 @@ class _TodoEditScreenState extends State { ), const Spacer(), Text( - '${_selectedScheduleItemIds.length}项', + context.l10n.todoItemCount(_selectedScheduleItemIds.length), style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, @@ -316,12 +323,12 @@ class _TodoEditScreenState extends State { ), const SizedBox(height: AppSpacing.sm), if (_scheduleItems.isEmpty) - const Padding( + Padding( padding: EdgeInsets.symmetric(vertical: AppSpacing.xl), child: Center( child: Text( - '暂无可关联的日历事件', - style: TextStyle(color: AppColors.slate500), + context.l10n.todoNoSelectableCalendarEvents, + style: const TextStyle(color: AppColors.slate500), ), ), ) @@ -372,7 +379,11 @@ class _TodoEditScreenState extends State { border: const Border(top: BorderSide(color: AppColors.borderSecondary)), ), child: AppButton( - text: _saving ? '保存中...' : (widget.isCreateMode ? '创建待办' : '保存修改'), + text: _saving + ? context.l10n.todoSaveInProgress + : (widget.isCreateMode + ? context.l10n.todoCreateButton + : context.l10n.todoSaveChanges), onPressed: canSave ? _save : null, ), ); @@ -384,7 +395,7 @@ class _TodoEditScreenState extends State { } final title = _titleController.text.trim(); if (title.isEmpty) { - Toast.show(context, '请输入标题', type: ToastType.warning); + Toast.show(context, context.l10n.todoEnterTitle, type: ToastType.warning); return; } @@ -420,7 +431,11 @@ class _TodoEditScreenState extends State { if (!mounted) { return; } - Toast.show(context, '保存失败: $error', type: ToastType.error); + Toast.show( + context, + context.l10n.todoSaveFailed(error.toString()), + type: ToastType.error, + ); } finally { if (mounted) { setState(() { @@ -431,7 +446,7 @@ class _TodoEditScreenState extends State { } String _formatDate(DateTime dt) { - return '${dt.year}年${dt.month}月${dt.day}日 ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; + return DateFormat.yMd(context.l10n.localeName).add_Hm().format(dt); } } diff --git a/apps/lib/features/todo/ui/screens/todo_quadrants_screen.dart b/apps/lib/features/todo/presentation/screens/todo_quadrants_screen.dart similarity index 94% rename from apps/lib/features/todo/ui/screens/todo_quadrants_screen.dart rename to apps/lib/features/todo/presentation/screens/todo_quadrants_screen.dart index fa17f69..921a0e7 100644 --- a/apps/lib/features/todo/ui/screens/todo_quadrants_screen.dart +++ b/apps/lib/features/todo/presentation/screens/todo_quadrants_screen.dart @@ -2,10 +2,11 @@ import 'package:flutter/material.dart'; import 'package:drag_and_drop_lists/drag_and_drop_lists.dart'; import 'package:go_router/go_router.dart'; import 'package:lucide_icons/lucide_icons.dart'; -import '../../../../core/di/injection.dart'; -import '../../../../core/router/app_routes.dart'; +import '../../../../app/di/injection.dart'; +import '../../../../app/router/app_routes.dart'; +import '../../../../core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; -import '../../../home/ui/navigation/home_return_policy.dart'; +import '../../../home/presentation/navigation/home_return_policy.dart'; import '../../../../shared/widgets/app_pull_refresh_feedback.dart'; import '../../../../shared/widgets/app_pressable.dart'; import '../../../../shared/widgets/back_title_page_header.dart'; @@ -13,8 +14,8 @@ import '../../../../shared/widgets/error_retry_surface.dart'; import '../../../../shared/widgets/full_screen_loading.dart'; import '../../../../shared/widgets/toast/toast.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; -import '../../../calendar/ui/calendar_state_manager.dart'; -import '../../../calendar/ui/widgets/bottom_dock.dart'; +import '../../../calendar/presentation/calendar_state_manager.dart'; +import '../../../calendar/presentation/widgets/bottom_dock.dart'; import '../../data/todo_api.dart'; import '../../data/todo_repository.dart'; @@ -88,7 +89,7 @@ class _TodoQuadrantsScreenState extends State { setState(() { _todos = previousTodos; }); - Toast.show(context, '移动失败', type: ToastType.error); + Toast.show(context, context.l10n.todoMoveFailed, type: ToastType.error); } finally { if (mounted) { setState(() { @@ -236,7 +237,11 @@ class _TodoQuadrantsScreenState extends State { }); } else { setState(() => _isPullRefreshing = false); - Toast.show(context, '刷新失败,请稍后重试', type: ToastType.error); + Toast.show( + context, + context.l10n.todoRefreshFailed, + type: ToastType.error, + ); } } finally { _loadingTodosRequest = false; @@ -275,7 +280,11 @@ class _TodoQuadrantsScreenState extends State { } } catch (e) { if (mounted) { - Toast.show(context, '完成失败: $e', type: ToastType.error); + Toast.show( + context, + context.l10n.todoCompleteFailed(e.toString()), + type: ToastType.error, + ); } } } @@ -320,7 +329,7 @@ class _TodoQuadrantsScreenState extends State { Widget _buildHeader() { return BackTitlePageHeader( - title: '待办事项', + title: context.l10n.todoScreenTitle, showBackButton: false, trailing: Row( mainAxisSize: MainAxisSize.min, @@ -361,7 +370,10 @@ class _TodoQuadrantsScreenState extends State { } if (_error != null) { - return ErrorRetrySurface(message: '加载失败: $_error', onRetry: _loadTodos); + return ErrorRetrySurface( + message: context.l10n.commonLoadFailed(_error!), + onRetry: _loadTodos, + ); } final content = _buildDragBoard(); @@ -385,7 +397,7 @@ class _TodoQuadrantsScreenState extends State { final quadrants = [ _QuadrantMeta( value: 1, - title: '重要紧急', + title: context.l10n.todoQuadrantImportantUrgent, textColor: AppColors.g1Text, dividerColor: AppColors.g1Divider, borderColor: AppColors.g1Border, @@ -393,7 +405,7 @@ class _TodoQuadrantsScreenState extends State { ), _QuadrantMeta( value: 3, - title: '紧急不重要', + title: context.l10n.todoQuadrantUrgentNotImportant, textColor: AppColors.g2Text, dividerColor: AppColors.g2Divider, borderColor: AppColors.g2Border, @@ -401,7 +413,7 @@ class _TodoQuadrantsScreenState extends State { ), _QuadrantMeta( value: 2, - title: '重要不紧急', + title: context.l10n.todoQuadrantImportantNotUrgent, textColor: AppColors.g3Text, dividerColor: AppColors.g3Divider, borderColor: AppColors.g3Border, @@ -500,7 +512,7 @@ class _TodoQuadrantsScreenState extends State { ), ), Text( - '${meta.items.length}项', + context.l10n.todoItemCount(meta.items.length), style: TextStyle( fontFamily: 'Inter', fontSize: 12, @@ -522,7 +534,7 @@ class _TodoQuadrantsScreenState extends State { height: 60, child: Center( child: Text( - '暂无待办', + context.l10n.todoNoItems, style: TextStyle( fontFamily: 'Inter', fontSize: 13, diff --git a/apps/lib/features/todo/ui/widgets/todo_drag_item.dart b/apps/lib/features/todo/presentation/widgets/todo_drag_item.dart similarity index 100% rename from apps/lib/features/todo/ui/widgets/todo_drag_item.dart rename to apps/lib/features/todo/presentation/widgets/todo_drag_item.dart diff --git a/apps/lib/core/schemas/ui_schema.dart b/apps/lib/features/ui_schema/domain/models/ui_schema.dart similarity index 100% rename from apps/lib/core/schemas/ui_schema.dart rename to apps/lib/features/ui_schema/domain/models/ui_schema.dart diff --git a/apps/lib/core/schemas/ui_schema/actions.dart b/apps/lib/features/ui_schema/domain/models/ui_schema/actions.dart similarity index 100% rename from apps/lib/core/schemas/ui_schema/actions.dart rename to apps/lib/features/ui_schema/domain/models/ui_schema/actions.dart diff --git a/apps/lib/core/schemas/ui_schema/builders.dart b/apps/lib/features/ui_schema/domain/models/ui_schema/builders.dart similarity index 100% rename from apps/lib/core/schemas/ui_schema/builders.dart rename to apps/lib/features/ui_schema/domain/models/ui_schema/builders.dart diff --git a/apps/lib/core/schemas/ui_schema/common_types.dart b/apps/lib/features/ui_schema/domain/models/ui_schema/common_types.dart similarity index 100% rename from apps/lib/core/schemas/ui_schema/common_types.dart rename to apps/lib/features/ui_schema/domain/models/ui_schema/common_types.dart diff --git a/apps/lib/core/schemas/ui_schema/document.dart b/apps/lib/features/ui_schema/domain/models/ui_schema/document.dart similarity index 100% rename from apps/lib/core/schemas/ui_schema/document.dart rename to apps/lib/features/ui_schema/domain/models/ui_schema/document.dart diff --git a/apps/lib/core/schemas/ui_schema/enums.dart b/apps/lib/features/ui_schema/domain/models/ui_schema/enums.dart similarity index 100% rename from apps/lib/core/schemas/ui_schema/enums.dart rename to apps/lib/features/ui_schema/domain/models/ui_schema/enums.dart diff --git a/apps/lib/core/schemas/ui_schema/nodes.dart b/apps/lib/features/ui_schema/domain/models/ui_schema/nodes.dart similarity index 100% rename from apps/lib/core/schemas/ui_schema/nodes.dart rename to apps/lib/features/ui_schema/domain/models/ui_schema/nodes.dart diff --git a/apps/lib/features/chat/ui/navigation/ui_schema_navigation.dart b/apps/lib/features/ui_schema/presentation/navigation/ui_schema_navigation.dart similarity index 100% rename from apps/lib/features/chat/ui/navigation/ui_schema_navigation.dart rename to apps/lib/features/ui_schema/presentation/navigation/ui_schema_navigation.dart diff --git a/apps/lib/features/chat/ui/widgets/ui_schema_renderer.dart b/apps/lib/features/ui_schema/presentation/widgets/ui_schema_renderer.dart similarity index 92% rename from apps/lib/features/chat/ui/widgets/ui_schema_renderer.dart rename to apps/lib/features/ui_schema/presentation/widgets/ui_schema_renderer.dart index d639f65..ac5ee86 100644 --- a/apps/lib/features/chat/ui/widgets/ui_schema_renderer.dart +++ b/apps/lib/features/ui_schema/presentation/widgets/ui_schema_renderer.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:social_app/core/l10n/l10n.dart'; import 'package:social_app/core/theme/design_tokens.dart'; import 'package:social_app/shared/widgets/toast/toast.dart'; import 'package:social_app/shared/widgets/toast/toast_type.dart'; @@ -13,7 +14,7 @@ class UiSchemaRenderer { } final root = _asMap(schema['root']); if (root == null) { - return _fallback('无效 UI Schema'); + return _fallback(L10n.current.uiSchemaInvalid); } return _renderLayoutNode(root); } @@ -23,7 +24,7 @@ class UiSchemaRenderer { return switch (type) { 'stack' => _renderStack(node), 'grid' => _renderGrid(node), - _ => _fallback('不支持的布局节点: $type'), + _ => _fallback(L10n.current.uiSchemaUnsupportedLayout(type)), }; } @@ -41,7 +42,7 @@ class UiSchemaRenderer { 'divider' => _renderDivider(node), 'stack' => _renderStack(node), 'grid' => _renderGrid(node), - _ => _fallback('未知节点: $type'), + _ => _fallback(L10n.current.uiSchemaUnknownNode(type)), }; } @@ -191,7 +192,10 @@ class UiSchemaRenderer { ), ), child: Text( - _asString(node['label'], fallback: '操作'), + _asString( + node['label'], + fallback: L10n.current.uiSchemaActionFallback, + ), style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600), ), ); @@ -206,13 +210,21 @@ class UiSchemaRenderer { final actionType = _asString(action?['type']); switch (actionType) { case 'copy': - Toast.show(context, '已复制', type: ToastType.success); + Toast.show( + context, + L10n.current.commonCopySuccess, + type: ToastType.success, + ); return; case 'navigation': _handleNavigationAction(context, action); return; default: - Toast.show(context, '该操作暂未接入', type: ToastType.info); + Toast.show( + context, + L10n.current.uiSchemaActionNotImplemented, + type: ToastType.info, + ); return; } } @@ -222,13 +234,21 @@ class UiSchemaRenderer { Map? action, ) { if (action == null) { - Toast.show(context, '导航参数无效', type: ToastType.warning); + Toast.show( + context, + L10n.current.uiSchemaNavigationInvalidParams, + type: ToastType.warning, + ); return; } final path = _asString(action['path']).trim(); if (!isValidInternalNavigationPath(path)) { - Toast.show(context, '导航路径无效', type: ToastType.warning); + Toast.show( + context, + L10n.current.uiSchemaNavigationInvalidPath, + type: ToastType.warning, + ); return; } @@ -242,7 +262,11 @@ class UiSchemaRenderer { } context.push(target); } on FormatException { - Toast.show(context, '导航路径无效', type: ToastType.warning); + Toast.show( + context, + L10n.current.uiSchemaNavigationInvalidPath, + type: ToastType.warning, + ); } } diff --git a/apps/lib/l10n/app_en.arb b/apps/lib/l10n/app_en.arb new file mode 100644 index 0000000..b2b6998 --- /dev/null +++ b/apps/lib/l10n/app_en.arb @@ -0,0 +1,775 @@ +{ + "@@locale": "en", + "appTitle": "Linksy", + "commonConfirm": "Confirm", + "commonCancel": "Cancel", + "commonSave": "Save", + "commonDone": "Done", + "commonRetry": "Retry", + "commonRefreshing": "Refreshing", + "commonLoading": "Loading...", + "commonEdit": "Edit", + "commonDelete": "Delete", + "commonShare": "Share", + "commonArchive": "Archive", + "commonCopySuccess": "Copied", + "commonLoadFailed": "Load failed: {error}", + "@commonLoadFailed": { + "placeholders": { + "error": {} + } + }, + "commonUnknown": "Unknown", + "toastLabelSuccess": "Success", + "toastLabelWarning": "Warning", + "toastLabelError": "Error", + "toastLabelInfo": "Info", + "errorGenericSafe": "Request failed, please try again later", + "errorForbidden": "You do not have permission to perform this action", + "errorNotFound": "Requested resource was not found", + "errorTooManyRequests": "Too many requests, please try again later", + "errorServer": "Server error, please try again later", + "errorAgentSseConnectionLimit": "Too many connections, please try again later", + "errorAgentAttachmentEmpty": "Attachment is empty", + "errorAgentAttachmentTooLarge": "Attachment is too large", + "errorAgentAudioEmpty": "Audio content is empty", + "errorAgentAudioTooLarge": "Audio file is too large", + "errorAgentAudioUnsupportedFormat": "Unsupported audio format", + "errorAgentAsrUnavailable": "Speech service is temporarily unavailable", + "errorAgentInvalidLastEventId": "Invalid event cursor, please refresh and retry", + "errorAgentInvalidBinaryUrl": "Invalid image link, please upload again", + "errorRequestFailed": "Request failed", + "errorNetwork": "Network error", + "errorReLogin": "Please sign in again", + "errorNetworkTimeout": "Network timeout. Ensure your device and server are on the same network and retry.", + "errorNetworkUnavailable": "Cannot connect to server. Please enable network access for this app in iPhone settings.", + "homeViewHistory": "View History", + "homeNoEarlierHistory": "No earlier history", + "homeSheetTakePhoto": "Take Photo", + "homeSheetPhotoLibrary": "Photo Library", + "homeDateLabelWithYear": "{year}-{month}-{day} {weekday}", + "@homeDateLabelWithYear": {"placeholders": {"year": {"type": "int"}, "month": {"type": "int"}, "day": {"type": "int"}, "weekday": {}}}, + "homeDateLabelNoYear": "{month}-{day} {weekday}", + "@homeDateLabelNoYear": {"placeholders": {"month": {"type": "int"}, "day": {"type": "int"}, "weekday": {}}}, + "homeRecordingReleaseCancel": "Release to cancel", + "homeRecordingReleaseSend": "Release to send", + "homeRecordingHintReleaseCancel": "Release to cancel", + "homeRecordingHintReleaseSend": "Release to send, slide up to cancel", + "homeHoldToSpeakText": "Hold to speak", + "homeInputHint": "Type a message...", + "homeTranscribing": "Transcribing voice...", + "homeRecordingCanceled": "Canceled", + "homeToolPreparing": "Preparing tool", + "homeToolExecuting": "Running task", + "homeToolExecutionFailed": "Execution failed", + "homeToolCompleted": "Completed", + "homeRecorderPluginUnavailable": "Recorder plugin is unavailable. Fully restart the app and retry.", + "homeRecorderPermissionDenied": "Microphone permission is not granted", + "homeStopRequested": "Stop requested", + "homeNoValidSpeech": "No valid speech detected. Please move closer to the microphone and retry.", + "agentStageRouting": "Analyzing intent", + "agentStageExecution": "Executing task", + "agentStageMemory": "Loading memory", + "agentStageProcessing": "Processing task", + "agUiEventRunStarted": "Run started", + "agUiEventRunFinished": "Run finished", + "agUiEventRunError": "Run failed", + "agUiEventStepStarted": "Step started", + "agUiEventStepFinished": "Step finished", + "agUiEventTextMessageEnd": "Text output completed", + "agUiEventToolCallStart": "Tool call started", + "agUiEventToolCallArgs": "Tool arguments updated", + "agUiEventToolCallEnd": "Tool call ended", + "agUiEventToolCallResult": "Tool result received", + "agUiEventToolCallError": "Tool call failed", + "agUiEventUnknown": "Unknown event", + "chatRunCanceled": "This run was canceled", + "chatRunFailed": "This run failed", + "chatSseInterruptedRetry": "Connection interrupted, please try again", + "chatTimestampToday": "Today", + "chatTimestampYesterday": "Yesterday", + "chatTimestampMonthDay": "{month}/{day}", + "@chatTimestampMonthDay": { + "placeholders": { + "month": { + "type": "int" + }, + "day": { + "type": "int" + } + } + }, + "homeUnreadMessages": "{count} new messages", + "@homeUnreadMessages": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "calendarToday": "Today", + "calendarEventNoAccessOrMissing": "Event not found or no permission", + "calendarDayWeekMonthYearLabel": "{year}-{month}", + "@calendarDayWeekMonthYearLabel": {"placeholders": {"year": {"type": "int"}, "month": {"type": "int"}}}, + "validatorPhoneRequired": "Please enter phone number", + "validatorPhoneInvalid86": "Please enter a valid +86 phone number", + "validatorPasswordRequired": "Please enter password", + "validatorPasswordMin8": "Password must be at least 8 characters", + "validatorRequired": "Please enter {fieldName}", + "@validatorRequired": {"placeholders": {"fieldName": {}}}, + "validatorNicknameRequired": "Please enter nickname", + "validatorNicknameMin2": "Nickname must be at least 2 characters", + "authAgreementTitle": "Please agree to the policies", + "authAgreementMessage": "Before using our services, please read and agree to the User Agreement and Privacy Policy.\n\nWe can only provide services after your consent.", + "authAgreementSemantics": "Agree to User Agreement and Privacy Policy", + "authAgreementPrefix": "I have read and agree to", + "authAgreementTerms": "User Agreement", + "authAgreementAnd": "and", + "authAgreementPrivacy": "Privacy Policy", + "authPhoneHint": "Enter phone number", + "authCodeHint": "Enter verification code", + "authSendCode": "Send Code", + "authShowPassword": "Show password", + "authHidePassword": "Hide password", + "authLoginFailed": "Login failed", + "authCheckInput": "Please check your input", + "authLoginOrRegister": "Login / Register", + "authInvalidPhone": "Please enter a valid phone number", + "authSendCodeFailed": "Failed to send verification code", + "inputUsernameRequired": "Please enter username", + "inputUsernameMin": "Username must be at least 3 characters", + "inputUsernameMax": "Username must be at most 30 characters", + "inputPhoneRequired": "Please enter phone number", + "inputPhoneInvalid": "Invalid phone number format", + "inputPasswordRequired": "Please enter password", + "inputPasswordMin": "Password must be at least 6 characters", + "inputCodeRequired": "Please enter verification code", + "inputCodeInvalid": "Verification code must be 6 digits", + "uiSchemaInvalid": "Invalid UI schema", + "uiSchemaUnsupportedLayout": "Unsupported layout node: {type}", + "@uiSchemaUnsupportedLayout": { + "placeholders": { + "type": {} + } + }, + "uiSchemaUnknownNode": "Unknown node: {type}", + "@uiSchemaUnknownNode": { + "placeholders": { + "type": {} + } + }, + "uiSchemaActionFallback": "Action", + "uiSchemaActionNotImplemented": "This action is not available yet", + "uiSchemaNavigationInvalidParams": "Invalid navigation params", + "uiSchemaNavigationInvalidPath": "Invalid navigation path", + "notificationSnoozeMinutes": "{minutes} min", + "@notificationSnoozeMinutes": { + "placeholders": { + "minutes": { + "type": "int" + } + } + }, + "notificationSnoozeLater": "Remind later", + "notificationChannelName": "Schedule alarm", + "notificationChannelDescription": "Alarm-style notifications for scheduled events", + "notificationStartsNow": "Event starts now", + "notificationStartsInMinutes": "Event starts in {minutes} minutes", + "@notificationStartsInMinutes": { + "placeholders": { + "minutes": { + "type": "int" + } + } + }, + "notificationLocation": "Location: {location}", + "@notificationLocation": { + "placeholders": { + "location": {} + } + }, + "notificationNotes": "Notes: {notes}", + "@notificationNotes": { + "placeholders": { + "notes": {} + } + }, + "todoScreenTitle": "To-Do", + "todoDetailTitle": "To-Do Details", + "todoCreateTitle": "Create To-Do", + "todoEditTitle": "Edit To-Do", + "todoMoveFailed": "Move failed", + "todoRefreshFailed": "Refresh failed, please try again", + "todoCompleteFailed": "Failed to complete: {error}", + "@todoCompleteFailed": { + "placeholders": { + "error": {} + } + }, + "todoNotFound": "To-do not found", + "todoCalendarEventCards": "Calendar Event Cards", + "todoPriorityQuadrant": "Quadrant", + "todoLinkedCalendarEvents": "Linked Calendar Events", + "todoStatus": "Status", + "todoStatusDone": "Done", + "todoStatusInProgress": "In progress", + "todoQuadrantOrder": "Order in quadrant #{order}", + "@todoQuadrantOrder": { + "placeholders": { + "order": { + "type": "int" + } + } + }, + "todoSplitToEvents": "Split into {count} calendar events", + "@todoSplitToEvents": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "todoNoLinkedEvents": "No linked calendar events", + "todoDeleteTitle": "Delete To-Do", + "todoDeleteMessage": "Are you sure you want to delete this to-do?", + "todoDeleteConfirm": "Delete", + "todoDeleteFailed": "Delete failed: {error}", + "@todoDeleteFailed": { + "placeholders": { + "error": {} + } + }, + "todoQuadrantImportantUrgent": "Important & Urgent", + "todoQuadrantUrgentNotImportant": "Urgent, Not Important", + "todoQuadrantImportantNotUrgent": "Important, Not Urgent", + "todoQuadrantNotUrgentNotImportant": "Not Urgent, Not Important", + "todoNoItems": "No to-dos", + "todoItemCount": "{count} items", + "@todoItemCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "todoInfoTitle": "To-Do Info", + "todoInfoDescCreate": "After creation, you can view it in quadrants and continue adjusting priority and linked events.", + "todoInfoDescDone": "This to-do is completed. You can still adjust content and reorganize linked events.", + "todoInfoDescDefault": "Adjust title, priority, and linked events to keep tasks organized.", + "todoFieldTitle": "Title", + "todoFieldTitleHint": "Enter to-do title", + "todoFieldDescriptionOptional": "Description (optional)", + "todoFieldDescriptionHint": "Add details or notes", + "todoPriority": "Priority", + "todoNoSelectableCalendarEvents": "No calendar events available to link", + "todoSaveInProgress": "Saving...", + "todoCreateButton": "Create To-Do", + "todoSaveChanges": "Save Changes", + "todoEnterTitle": "Please enter a title", + "todoSaveFailed": "Save failed: {error}", + "@todoSaveFailed": { + "placeholders": { + "error": {} + } + }, + "contactsTitle": "Contacts", + "contactsSearchHint": "Enter username or phone number", + "contactsSearchEmptyQuery": "Please enter username or phone number", + "contactsSearchFailed": "Search failed, please try again", + "contactsSearchNoUser": "User not found", + "contactsFriendRequestSent": "Friend request sent", + "contactsSendFailed": "Send failed, please try again", + "contactsSectionNew": "New Contacts", + "contactsSectionAll": "All Contacts", + "contactsStatusAlreadyFriend": "Already friends", + "contactsStatusSent": "Sent", + "contactsAdd": "Add", + "contactsEmptyTitle": "No contacts", + "contactsEmptyDesc": "Search by phone to add friends and start chatting", + "contactsPendingConfirm": "Waiting for confirmation", + "contactsAddSheetTitle": "Add {username}", + "@contactsAddSheetTitle": {"placeholders": {"username": {}}}, + "contactsAddSheetDesc": "Send a verification message so the other person can confirm your identity", + "contactsAddSheetMessageHint": "Hi, I am...", + "contactsSend": "Send", + "contactEditTitle": "Edit Contact", + "contactAddTitle": "Add Contact", + "contactNickname": "Nickname", + "contactNicknameHint": "Enter nickname", + "contactPhone": "Phone", + "contactPhoneHint": "+86 Enter 11-digit phone number", + "contactRemark": "Remark", + "contactRemarkHint": "Enter remark", + "contactDelete": "Delete Contact", + "contactFillRequired": "Please fill nickname and phone", + "contactDeleteConfirmTitle": "Delete Contact", + "contactDeleteConfirmMessage": "Are you sure to delete this contact?", + "messagesLoadFailed": "Failed to load messages, please try again", + "messagesSenderLoadFailed": "Failed to load sender info, pull to retry", + "messagesFriendRequestMissing": "Missing friend request data", + "messagesAcceptedFriendRequest": "Friend request accepted", + "messagesRejectedFriendRequest": "Friend request rejected", + "messagesActionFailed": "Action failed, please try again", + "messagesTabUnread": "Unread", + "messagesTabRead": "Read", + "messagesEmptyUnreadTitle": "No unread messages", + "messagesEmptyReadTitle": "No read messages", + "messagesEmptyUnreadDesc": "New messages will appear here", + "messagesEmptyReadDesc": "Processed messages will appear here", + "messagesFriendRequestLoadFailed": "Failed to load friend request info", + "messagesFriendRequestTitle": "{username} wants to add you as a friend", + "@messagesFriendRequestTitle": {"placeholders": {"username": {}}}, + "messagesCalendarInvite": "Calendar invite", + "messagesSystemMessage": "System message", + "messagesTapToView": "Tap to view details", + "messagesInviteJoinCalendar": "Invites you to join calendar", + "messagesInviteAccepted": "Calendar invite accepted", + "messagesInviteRejected": "Calendar invite rejected", + "messagesCalendarUpdated": "Updated calendar event", + "messagesInviteStatusAccepted": "Accepted", + "messagesInviteStatusRejected": "Rejected", + "messagesInviteStatusHandled": "Handled", + "messagesInviteDetailNotFound": "Invite not found or expired", + "messagesInviteAcceptedToast": "Invite accepted", + "messagesInviteRejectedToast": "Invite rejected", + "messagesInviteOperationFailed": "Operation failed, please try again", + "messagesInviteDetailTitle": "Calendar Invite Details", + "messagesInviteEvent": "Event: {title}", + "@messagesInviteEvent": {"placeholders": {"title": {}}}, + "messagesInviteUnnamedEvent": "Unnamed schedule", + "messagesInviteSender": "Sender: {name}", + "@messagesInviteSender": {"placeholders": {"name": {}}}, + "messagesInviteUnknownUser": "Unknown user", + "messagesInviteTime": "Time: {time}", + "@messagesInviteTime": {"placeholders": {"time": {}}}, + "messagesInviteStatus": "Status: {status}", + "@messagesInviteStatus": {"placeholders": {"status": {}}}, + "messagesInviteId": "Invite ID: {id}", + "@messagesInviteId": {"placeholders": {"id": {}}}, + "messagesInviteTip": "Accept to join this calendar event. Reject to mark this invite as handled.", + "messagesInviteAlreadyHandled": "This invite has been handled", + "messagesReject": "Reject", + "messagesAccept": "Accept", + "messagesStatusPending": "Pending", + "settingsFeaturesTitle": "Recurring Plans", + "settingsSectionDaily": "Daily", + "settingsSectionWeekly": "Weekly", + "settingsNoDailyPlans": "No daily plans", + "settingsNoWeeklyPlans": "No weekly plans", + "settingsSystemJobReadonly": "System preset jobs cannot be changed", + "settingsJobStatusEnabled": "Enabled", + "settingsJobStatusDisabled": "Disabled", + "settingsJobSourceSystem": "System preset", + "settingsJobSourceCustom": "Custom", + "settingsCreateJob": "Create Job", + "memoryTitle": "My Memory", + "memoryLoadFailedRetry": "Load failed, please retry", + "memorySmartTitle": "Smart Memory", + "memorySmartDesc": "Continuously learns your preferences and habits", + "memoryReload": "Reload", + "memorySectionUser": "User Memory", + "memorySectionWork": "Work Memory", + "memoryUserProfile": "Personal Preferences", + "memoryWorkProfile": "Work Profile", + "memoryNoInfo": "No info", + "memoryStatContacts": "Contacts", + "memoryStatPlaces": "Places", + "memoryStatInterests": "Interests", + "memoryStatSchedule": "Schedule", + "memoryStatExpertise": "Expertise", + "memoryStatTools": "Tools", + "memoryStatProjects": "Projects", + "memoryStatTeam": "Team", + "memorySummaryContactsCount": "{count} contacts", + "@memorySummaryContactsCount": {"placeholders": {"count": {"type": "int"}}}, + "memorySummaryPlacesCount": "{count} places", + "@memorySummaryPlacesCount": {"placeholders": {"count": {"type": "int"}}}, + "memorySummaryInterestsCount": "{count} interests", + "@memorySummaryInterestsCount": {"placeholders": {"count": {"type": "int"}}}, + "memorySummaryExpertiseCount": "{count} expertise areas", + "@memorySummaryExpertiseCount": {"placeholders": {"count": {"type": "int"}}}, + "memorySummaryProjectsCount": "{count} projects", + "@memorySummaryProjectsCount": {"placeholders": {"count": {"type": "int"}}}, + "memorySummaryTeamMembersCount": "{count} team members", + "@memorySummaryTeamMembersCount": {"placeholders": {"count": {"type": "int"}}}, + "toolCalendarRead": "Read Calendar", + "toolCalendarWrite": "Write Calendar", + "toolCalendarShare": "Share Calendar", + "toolUserLookup": "Lookup Contact", + "toolMemoryWrite": "Write Memory", + "toolMemoryForget": "Forget Memory", + "settingsTitle": "Settings", + "settingsUnset": "Not set", + "settingsFreeBadge": "Free", + "settingsNoContacts": "No contacts", + "settingsContactsAddedOne": "Added 1 contact: {name}", + "@settingsContactsAddedOne": {"placeholders": {"name": {}}}, + "settingsContactsAddedMany": "Added {count} contacts", + "@settingsContactsAddedMany": {"placeholders": {"count": {"type": "int"}}}, + "settingsNoEnabledPlans": "No enabled plans", + "settingsEnabledPlanOne": "Enabled: {title}", + "@settingsEnabledPlanOne": {"placeholders": {"title": {}}}, + "settingsEnabledPlanMany": "Enabled {count} plans", + "@settingsEnabledPlanMany": {"placeholders": {"count": {"type": "int"}}}, + "settingsUpgradeProTitle": "Upgrade to Pro", + "settingsUpgradeProDesc": "Unlock more advanced features", + "settingsUpgradeButton": "Upgrade", + "settingsMenuNotifications": "Reminder Settings", + "settingsMenuCheckUpdates": "Check for Updates", + "settingsLogoutTitle": "Log Out", + "settingsLogoutConfirmMessage": "Are you sure you want to log out of this account?", + "settingsLogoutConfirm": "Confirm Logout", + "settingsLogoutFailed": "Logout failed, please try again later", + "settingsLatestVersion": "You already have the latest version", + "settingsUpdateRequired": "A new version is available ({version}), please update now", + "@settingsUpdateRequired": {"placeholders": {"version": {}}}, + "settingsUpdateOptional": "New version found ({version}), update now?", + "@settingsUpdateOptional": {"placeholders": {"version": {}}}, + "settingsUpdateDialogTitle": "Check for Updates", + "settingsUpdateAction": "Update", + "settingsDownloadLink": "Download link: {url}", + "@settingsDownloadLink": {"placeholders": {"url": {}}}, + "settingsUpdateCheckFailed": "Failed to check updates", + "settingsJobDetailTitle": "Job Detail", + "settingsJobCreatePageTitle": "Create Recurring Plan", + "settingsJobLoadFailed": "Load failed", + "settingsJobRetry": "Retry", + "settingsJobPlanConfig": "Plan Configuration", + "settingsJobCycle": "Cycle", + "settingsJobRunAt": "Run Time", + "settingsJobTimezone": "Timezone", + "settingsJobStatusLabel": "Status", + "settingsJobInputTemplate": "Input Template", + "settingsJobEnabledTools": "Enabled Tools", + "settingsJobContextMode": "Context Message Mode", + "settingsJobContextSource": "Source", + "settingsJobWindowMode": "Window Mode", + "settingsJobWindowCount": "Window Count", + "settingsJobWindowCountValue": "{count}", + "@settingsJobWindowCountValue": {"placeholders": {"count": {"type": "int"}}}, + "settingsJobDeleteTitle": "Delete Recurring Plan", + "settingsJobDeleteMessage": "This action cannot be undone. Continue?", + "settingsJobDeleteConfirm": "Confirm Delete", + "settingsJobDeleteSuccess": "Deleted successfully", + "settingsJobBasicInfo": "Basic Info", + "settingsJobName": "Job Name", + "settingsJobNameHint": "Enter job name", + "settingsJobTemplateHint": "Example: summarize today memory", + "settingsJobExecutionRules": "Execution Rules", + "settingsJobToolSelection": "Tool Selection", + "settingsJobCounterValue": "{label}: {value}", + "@settingsJobCounterValue": { + "placeholders": { + "label": {}, + "value": {"type": "int"} + } + }, + "settingsJobWeekdayMon": "Mon", + "settingsJobWeekdayTue": "Tue", + "settingsJobWeekdayWed": "Wed", + "settingsJobWeekdayThu": "Thu", + "settingsJobWeekdayFri": "Fri", + "settingsJobWeekdaySat": "Sat", + "settingsJobWeekdaySun": "Sun", + "settingsJobRunDays": "Run Days", + "settingsJobNoToolsEnabled": "No tools enabled", + "settingsJobPickCycle": "Choose Cycle", + "settingsJobScheduleDaily": "Daily", + "settingsJobScheduleWeekly": "Weekly", + "settingsJobPickTimezone": "Choose Timezone", + "settingsJobPickContextSource": "Choose Context Source", + "settingsJobContextSourceLatestChat": "Latest chat", + "settingsJobPickWindowMode": "Choose Window Mode", + "settingsJobWindowModeByDay": "By day", + "settingsJobWindowModeByNumber": "By message count", + "settingsJobFillRequired": "Please fill all required fields", + "settingsJobCreateSuccess": "Created successfully", + "settingsMemorySaveSuccess": "Saved successfully", + "settingsMemorySaveFailed": "Failed to save", + "settingsMemoryInputHint": "Enter {label}", + "@settingsMemoryInputHint": {"placeholders": {"label": {}}}, + "settingsMemoryInputContent": "Enter content", + "settingsUserMemoryEditTitle": "Edit Personal Preferences", + "settingsUserMemoryEmptyProfile": "No personal preferences", + "settingsUserMemorySectionBasic": "Basic Info", + "settingsUserMemorySectionPreferences": "Preferences", + "settingsUserMemorySectionSchedule": "Schedule Preferences", + "settingsUserMemorySectionContacts": "Contacts", + "settingsUserMemorySectionPlaces": "Places", + "settingsUserMemorySectionInterests": "Interests", + "settingsUserMemorySectionAvoidTopics": "Avoid Topics", + "settingsUserMemorySectionCustomRules": "Custom Rules", + "settingsUserMemorySectionRoutines": "Recurring Routines", + "settingsUserMemoryFieldOccupation": "Occupation", + "settingsUserMemoryFieldTimezone": "Timezone", + "settingsUserMemoryFieldPrimaryLanguage": "Primary Language", + "settingsUserMemoryFieldCommunicationStyle": "Communication Style", + "settingsUserMemoryFieldLocationPreference": "Location Preference", + "settingsUserMemoryFieldWorkLifestyle": "Work Lifestyle", + "settingsUserMemoryFieldLanguagePreference": "Language Preference", + "settingsUserMemoryFieldNotificationPreference": "Notification Preference", + "settingsUserMemoryFieldMeetingBuffer": "Meeting Buffer", + "settingsUserMemoryFieldMaxMeetingsPerDay": "Max Meetings per Day", + "settingsUserMemoryFieldPreferredMeetingDuration": "Preferred Meeting Duration", + "settingsUserMemoryFieldNotes": "Notes", + "settingsUserMemoryFieldName": "Name", + "settingsUserMemoryFieldRelationship": "Relationship", + "settingsUserMemoryFieldRole": "Role", + "settingsUserMemoryFieldContact": "Contact", + "settingsUserMemoryFieldCategory": "Category", + "settingsUserMemoryFieldPreference": "Preference", + "settingsUserMemoryFieldAddress": "Address", + "settingsUserMemoryFieldDescription": "Description", + "settingsUserMemoryFieldCadence": "Cadence", + "settingsUserMemoryMinute": "min", + "settingsUserMemoryMinutesValue": "{minutes} min", + "@settingsUserMemoryMinutesValue": { + "placeholders": { + "minutes": {"type": "int"} + } + }, + "settingsUserMemoryEmptyContacts": "No contacts", + "settingsUserMemoryEmptyPlaces": "No places", + "settingsUserMemoryEmptyRoutines": "No routines", + "settingsUserMemoryAddContact": "Add Contact", + "settingsUserMemoryNewContact": "New Contact", + "settingsUserMemoryAddPlace": "Add Place", + "settingsUserMemoryNewPlace": "New Place", + "settingsUserMemoryAddRoutine": "Add Routine", + "settingsUserMemoryNewRoutine": "New Routine", + "settingsWorkMemoryEditTitle": "Edit Work Profile", + "settingsWorkMemoryEmptyProfile": "No work info", + "settingsWorkMemorySectionBasic": "Basic Info", + "settingsWorkMemorySectionExpertise": "Expertise", + "settingsWorkMemorySectionPreferredTools": "Preferred Tools", + "settingsWorkMemorySectionCurrentProjects": "Current Projects", + "settingsWorkMemorySectionTeamMembers": "Team Members", + "settingsWorkMemorySectionWorkHabits": "Work Habits", + "settingsWorkMemorySectionTeamContext": "Team Context", + "settingsWorkMemorySectionWorkRules": "Work Rules", + "settingsWorkMemoryFieldOccupation": "Occupation", + "settingsWorkMemoryFieldAvailableHours": "Available Hours", + "settingsWorkMemoryFieldDeepWorkBlocks": "Deep Work Blocks", + "settingsWorkMemoryFieldPreferredMeetingWindows": "Preferred Meeting Windows", + "settingsWorkMemoryFieldNoMeetingWindows": "No-Meeting Windows", + "settingsWorkMemoryFieldPreferredMeetingDuration": "Preferred Meeting Duration", + "settingsWorkMemoryFieldNotificationChannel": "Notification Channel", + "settingsWorkMemoryFieldNotes": "Notes", + "settingsWorkMemoryFieldTeamContext": "Team Context", + "settingsWorkMemoryFieldProjectName": "Project Name", + "settingsWorkMemoryFieldStatus": "Status", + "settingsWorkMemoryFieldPriority": "Priority", + "settingsWorkMemoryFieldDeadline": "Deadline", + "settingsWorkMemoryFieldCollaborators": "Collaborators", + "settingsWorkMemoryFieldMilestones": "Milestones", + "settingsWorkMemoryMinute": "min", + "settingsWorkMemoryMilestoneCount": "{count} items", + "@settingsWorkMemoryMilestoneCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "settingsWorkMemoryEmptyProjects": "No projects", + "settingsWorkMemoryEmptyTeamMembers": "No team members", + "settingsWorkMemoryTimeWindowCount": "{count} windows", + "@settingsWorkMemoryTimeWindowCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "settingsWorkMemoryAddProject": "Add Project", + "settingsWorkMemoryNewProject": "New Project", + "settingsWorkMemoryAddMember": "Add Member", + "settingsWorkMemoryNewMember": "New Member", + "calendarDetailTitle": "Event Details", + "calendarDetailNotFoundTitle": "Event not found", + "calendarDetailNotFoundDesc": "It may have been deleted, or you do not have access.", + "calendarDetailTimeArrangement": "Time Arrangement", + "calendarDetailDateLabel": "{year}-{month}-{day} {weekday}", + "@calendarDetailDateLabel": { + "placeholders": { + "year": {"type": "int"}, + "month": {"type": "int"}, + "day": {"type": "int"}, + "weekday": {} + } + }, + "calendarDetailBasicInfo": "Basic Info", + "calendarDetailDate": "Date", + "calendarDetailReminder": "Reminder", + "calendarDetailColor": "Color", + "calendarDetailExtraInfo": "Extra Info", + "calendarDetailLocation": "Location", + "calendarDetailDescription": "Description", + "calendarDetailNotes": "Notes", + "calendarDetailReminderNone": "None", + "calendarDetailReminderOnTime": "On time", + "calendarDetailReminderBeforeMinutes": "{minutes} min before start", + "@calendarDetailReminderBeforeMinutes": { + "placeholders": { + "minutes": {"type": "int"} + } + }, + "calendarWeekdayMon": "Mon", + "calendarWeekdayTue": "Tue", + "calendarWeekdayWed": "Wed", + "calendarWeekdayThu": "Thu", + "calendarWeekdayFri": "Fri", + "calendarWeekdaySat": "Sat", + "calendarWeekdaySun": "Sun", + "calendarDetailDeleteTitle": "Delete Event", + "calendarDetailDeleteMessage": "Are you sure you want to delete this event?", + "calendarDetailDeleteConfirm": "Delete", + "calendarDetailArchiveTitle": "Archive Event", + "calendarDetailArchiveMessage": "This will mark the event as expired. Continue?", + "calendarDetailArchiveConfirm": "Archive", + "calendarDetailArchiveFailed": "Archive failed", + "calendarDetailDateTimeShort": "{month}/{day} {weekday} {time}", + "@calendarDetailDateTimeShort": { + "placeholders": { + "month": {"type": "int"}, + "day": {"type": "int"}, + "weekday": {}, + "time": {} + } + }, + "calendarDetailRangeWithStartEnd": "Start: {start}\nEnd: {end}", + "@calendarDetailRangeWithStartEnd": { + "placeholders": { + "start": {}, + "end": {} + } + }, + "calendarDetailStatusExpired": "Expired", + "calendarCreateEditTitle": "Edit Event", + "calendarCreateNewTitle": "New Event", + "calendarCreateTabBasic": "Basic", + "calendarCreateTabAdvanced": "Advanced", + "calendarCreateFieldTitle": "Title", + "calendarCreateFieldTitleHint": "Enter event title", + "calendarCreateFieldStart": "Start", + "calendarCreateFieldEnd": "End", + "calendarCreateFieldDescription": "Description", + "calendarCreateFieldDescriptionHint": "Enter description", + "calendarCreateFieldLocation": "Location", + "calendarCreateFieldLocationHint": "Enter location", + "calendarCreateFieldNotesHint": "Enter notes", + "calendarCreateOptionalField": "{label} (optional)", + "@calendarCreateOptionalField": {"placeholders": {"label": {}}}, + "calendarCreateDateTimeLabel": "{year}-{month}-{day} {hour}:{minute}", + "@calendarCreateDateTimeLabel": { + "placeholders": { + "year": {"type": "int"}, + "month": {"type": "int"}, + "day": {"type": "int"}, + "hour": {}, + "minute": {} + } + }, + "calendarCreateReminderNone": "No reminder", + "calendarCreateReminderTime": "Reminder Time", + "calendarCreatePickReminderTime": "Select Reminder Time", + "calendarCreateReminderPermissionFailed": "Failed to create reminder, check notification permission", + "settingsEditProfileLoadFailed": "Failed to load user profile", + "settingsEditProfileAvatarUploadSuccess": "Avatar uploaded successfully", + "settingsEditProfileAvatarUploadFailed": "Failed to upload avatar, please try again", + "settingsEditProfileUsernameRequired": "Username is required", + "settingsEditProfileUsernameLengthInvalid": "Username must be 3-30 characters", + "settingsEditProfileSaveSuccess": "Saved successfully", + "settingsEditProfileSaveFailed": "Save failed, please try again", + "settingsEditProfileTitle": "Edit Profile", + "settingsEditProfileSaveChanges": "Save Changes", + "settingsEditProfileBasicInfo": "Basic Info", + "settingsEditProfileUsername": "Username", + "settingsEditProfileUsernameHint": "Enter username", + "settingsEditProfileBio": "Bio", + "settingsEditProfileBioContent": "Bio Content", + "settingsEditProfileBioHint": "Tell us about yourself", + "calendarSharePhoneRequired": "Please enter a phone number", + "calendarShareInviteSent": "Invite sent", + "calendarShareInviteFailed": "Failed to send invite", + "calendarShareTitle": "Share Calendar", + "calendarSharePhoneLabel": "Phone", + "calendarSharePhoneHint": "Enter recipient's +86 phone number", + "calendarSharePermissionTitle": "Permissions", + "calendarSharePermissionView": "View", + "calendarSharePermissionViewDesc": "Can view this calendar event (required)", + "calendarSharePermissionEdit": "Edit", + "calendarSharePermissionEditDesc": "Can edit this calendar event", + "calendarSharePermissionInvite": "Invite", + "calendarSharePermissionInviteDesc": "Can invite others", + "calendarShareSendInvite": "Send Invite", + "calendarMonthHeader": "{month}", + "@calendarMonthHeader": { + "placeholders": { + "month": {"type": "int"} + } + }, + "calendarMonthToday": "Today", + "calendarMonthWeekdaySunShort": "S", + "calendarMonthWeekdayMonShort": "M", + "calendarMonthWeekdayTueShort": "T", + "calendarMonthWeekdayWedShort": "W", + "calendarMonthWeekdayThuShort": "T", + "calendarMonthWeekdayFriShort": "F", + "calendarMonthWeekdaySatShort": "S", + "calendarMonthYearLabel": "{year}", + "@calendarMonthYearLabel": { + "placeholders": { + "year": {"type": "int"} + } + }, + "calendarDateTimePickerDateLabel": "Date", + "calendarDateTimePickerYearUnit": "Y", + "calendarDateTimePickerMonthUnit": "M", + "calendarDateTimePickerDayUnit": "D", + "calendarDateTimePickerTimeLabel": "Time", + "calendarDateTimePickerTitle": "Select Time", + "messagesCalendarCardInviteTitle": "Calendar Invite", + "messagesCalendarCardInviteWithTitle": "Invites you to access \"{title}\"", + "@messagesCalendarCardInviteWithTitle": { + "placeholders": { + "title": {} + } + }, + "messagesCalendarCardInviteWithoutTitle": "Invites you to access a calendar", + "messagesCalendarCardUpdatedWithTitle": "{title} updated", + "@messagesCalendarCardUpdatedWithTitle": { + "placeholders": { + "title": {} + } + }, + "messagesCalendarCardUpdatedWithoutTitle": "Calendar event updated", + "messagesCalendarCardTimeMinutesAgo": "{minutes}m ago", + "@messagesCalendarCardTimeMinutesAgo": { + "placeholders": { + "minutes": {"type": "int"} + } + }, + "messagesCalendarCardTimeHoursAgo": "{hours}h ago", + "@messagesCalendarCardTimeHoursAgo": { + "placeholders": { + "hours": {"type": "int"} + } + }, + "messagesCalendarCardTimeDaysAgo": "{days}d ago", + "@messagesCalendarCardTimeDaysAgo": { + "placeholders": { + "days": {"type": "int"} + } + }, + "messagesCalendarCardTimeDate": "{month}/{day}", + "@messagesCalendarCardTimeDate": { + "placeholders": { + "month": {"type": "int"}, + "day": {"type": "int"} + } + }, + "messagesCalendarCardDeletedWithTitle": "{title} deleted", + "@messagesCalendarCardDeletedWithTitle": { + "placeholders": { + "title": {} + } + }, + "messagesCalendarCardDeletedWithoutTitle": "Calendar event deleted" +} diff --git a/apps/lib/l10n/app_localizations.dart b/apps/lib/l10n/app_localizations.dart new file mode 100644 index 0000000..c85cf34 --- /dev/null +++ b/apps/lib/l10n/app_localizations.dart @@ -0,0 +1,3445 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'app_localizations_en.dart'; +import 'app_localizations_zh.dart'; + +// ignore_for_file: type=lint + +/// Callers can lookup localized strings with an instance of AppLocalizations +/// returned by `AppLocalizations.of(context)`. +/// +/// Applications need to include `AppLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'l10n/app_localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: AppLocalizations.localizationsDelegates, +/// supportedLocales: AppLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the AppLocalizations.supportedLocales +/// property. +abstract class AppLocalizations { + AppLocalizations(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + + static AppLocalizations of(BuildContext context) { + return Localizations.of(context, AppLocalizations)!; + } + + static const LocalizationsDelegate delegate = + _AppLocalizationsDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = + >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [ + Locale('en'), + Locale('zh'), + ]; + + /// No description provided for @appTitle. + /// + /// In zh, this message translates to: + /// **'Linksy'** + String get appTitle; + + /// No description provided for @commonConfirm. + /// + /// In zh, this message translates to: + /// **'确认'** + String get commonConfirm; + + /// No description provided for @commonCancel. + /// + /// In zh, this message translates to: + /// **'取消'** + String get commonCancel; + + /// No description provided for @commonSave. + /// + /// In zh, this message translates to: + /// **'保存'** + String get commonSave; + + /// No description provided for @commonDone. + /// + /// In zh, this message translates to: + /// **'完成'** + String get commonDone; + + /// No description provided for @commonRetry. + /// + /// In zh, this message translates to: + /// **'重试'** + String get commonRetry; + + /// No description provided for @commonRefreshing. + /// + /// In zh, this message translates to: + /// **'正在刷新'** + String get commonRefreshing; + + /// No description provided for @commonLoading. + /// + /// In zh, this message translates to: + /// **'加载中...'** + String get commonLoading; + + /// No description provided for @commonEdit. + /// + /// In zh, this message translates to: + /// **'编辑'** + String get commonEdit; + + /// No description provided for @commonDelete. + /// + /// In zh, this message translates to: + /// **'删除'** + String get commonDelete; + + /// No description provided for @commonShare. + /// + /// In zh, this message translates to: + /// **'分享'** + String get commonShare; + + /// No description provided for @commonArchive. + /// + /// In zh, this message translates to: + /// **'归档'** + String get commonArchive; + + /// No description provided for @commonCopySuccess. + /// + /// In zh, this message translates to: + /// **'已复制'** + String get commonCopySuccess; + + /// No description provided for @commonLoadFailed. + /// + /// In zh, this message translates to: + /// **'加载失败: {error}'** + String commonLoadFailed(Object error); + + /// No description provided for @commonUnknown. + /// + /// In zh, this message translates to: + /// **'未知'** + String get commonUnknown; + + /// No description provided for @toastLabelSuccess. + /// + /// In zh, this message translates to: + /// **'成功'** + String get toastLabelSuccess; + + /// No description provided for @toastLabelWarning. + /// + /// In zh, this message translates to: + /// **'提醒'** + String get toastLabelWarning; + + /// No description provided for @toastLabelError. + /// + /// In zh, this message translates to: + /// **'错误'** + String get toastLabelError; + + /// No description provided for @toastLabelInfo. + /// + /// In zh, this message translates to: + /// **'提示'** + String get toastLabelInfo; + + /// No description provided for @errorGenericSafe. + /// + /// In zh, this message translates to: + /// **'请求失败,请稍后重试'** + String get errorGenericSafe; + + /// No description provided for @errorForbidden. + /// + /// In zh, this message translates to: + /// **'没有权限执行此操作'** + String get errorForbidden; + + /// No description provided for @errorNotFound. + /// + /// In zh, this message translates to: + /// **'请求的资源不存在'** + String get errorNotFound; + + /// No description provided for @errorTooManyRequests. + /// + /// In zh, this message translates to: + /// **'请求过于频繁,请稍后再试'** + String get errorTooManyRequests; + + /// No description provided for @errorServer. + /// + /// In zh, this message translates to: + /// **'服务器错误,请稍后再试'** + String get errorServer; + + /// No description provided for @errorAgentSseConnectionLimit. + /// + /// In zh, this message translates to: + /// **'连接过于频繁,请稍后重试'** + String get errorAgentSseConnectionLimit; + + /// No description provided for @errorAgentAttachmentEmpty. + /// + /// In zh, this message translates to: + /// **'附件内容为空'** + String get errorAgentAttachmentEmpty; + + /// No description provided for @errorAgentAttachmentTooLarge. + /// + /// In zh, this message translates to: + /// **'附件过大,请压缩后重试'** + String get errorAgentAttachmentTooLarge; + + /// No description provided for @errorAgentAudioEmpty. + /// + /// In zh, this message translates to: + /// **'音频内容为空'** + String get errorAgentAudioEmpty; + + /// No description provided for @errorAgentAudioTooLarge. + /// + /// In zh, this message translates to: + /// **'音频文件过大'** + String get errorAgentAudioTooLarge; + + /// No description provided for @errorAgentAudioUnsupportedFormat. + /// + /// In zh, this message translates to: + /// **'音频格式不支持'** + String get errorAgentAudioUnsupportedFormat; + + /// No description provided for @errorAgentAsrUnavailable. + /// + /// In zh, this message translates to: + /// **'语音服务暂不可用,请稍后重试'** + String get errorAgentAsrUnavailable; + + /// No description provided for @errorAgentInvalidLastEventId. + /// + /// In zh, this message translates to: + /// **'事件游标无效,请刷新后重试'** + String get errorAgentInvalidLastEventId; + + /// No description provided for @errorAgentInvalidBinaryUrl. + /// + /// In zh, this message translates to: + /// **'图片链接无效,请重新上传'** + String get errorAgentInvalidBinaryUrl; + + /// No description provided for @errorRequestFailed. + /// + /// In zh, this message translates to: + /// **'请求失败'** + String get errorRequestFailed; + + /// No description provided for @errorNetwork. + /// + /// In zh, this message translates to: + /// **'网络错误'** + String get errorNetwork; + + /// No description provided for @errorReLogin. + /// + /// In zh, this message translates to: + /// **'请重新登录'** + String get errorReLogin; + + /// No description provided for @errorNetworkTimeout. + /// + /// In zh, this message translates to: + /// **'网络超时,请确认手机与服务端在同一网络后重试'** + String get errorNetworkTimeout; + + /// No description provided for @errorNetworkUnavailable. + /// + /// In zh, this message translates to: + /// **'无法连接服务器。请在 iPhone 设置中为本应用开启无线数据,并确认本地网络权限已开启。'** + String get errorNetworkUnavailable; + + /// No description provided for @homeViewHistory. + /// + /// In zh, this message translates to: + /// **'查看历史'** + String get homeViewHistory; + + /// No description provided for @homeNoEarlierHistory. + /// + /// In zh, this message translates to: + /// **'没有更早的历史记录了'** + String get homeNoEarlierHistory; + + /// No description provided for @homeSheetTakePhoto. + /// + /// In zh, this message translates to: + /// **'拍照'** + String get homeSheetTakePhoto; + + /// No description provided for @homeSheetPhotoLibrary. + /// + /// In zh, this message translates to: + /// **'相册'** + String get homeSheetPhotoLibrary; + + /// No description provided for @homeDateLabelWithYear. + /// + /// In zh, this message translates to: + /// **'{year}年{month}月{day}日 {weekday}'** + String homeDateLabelWithYear(int year, int month, int day, Object weekday); + + /// No description provided for @homeDateLabelNoYear. + /// + /// In zh, this message translates to: + /// **'{month}月{day}日 {weekday}'** + String homeDateLabelNoYear(int month, int day, Object weekday); + + /// No description provided for @homeRecordingReleaseCancel. + /// + /// In zh, this message translates to: + /// **'松手取消'** + String get homeRecordingReleaseCancel; + + /// No description provided for @homeRecordingReleaseSend. + /// + /// In zh, this message translates to: + /// **'松手发送'** + String get homeRecordingReleaseSend; + + /// No description provided for @homeRecordingHintReleaseCancel. + /// + /// In zh, this message translates to: + /// **'松开取消'** + String get homeRecordingHintReleaseCancel; + + /// No description provided for @homeRecordingHintReleaseSend. + /// + /// In zh, this message translates to: + /// **'松开发送,上滑取消'** + String get homeRecordingHintReleaseSend; + + /// No description provided for @homeHoldToSpeakText. + /// + /// In zh, this message translates to: + /// **'按住说话'** + String get homeHoldToSpeakText; + + /// No description provided for @homeInputHint. + /// + /// In zh, this message translates to: + /// **'输入消息...'** + String get homeInputHint; + + /// No description provided for @homeTranscribing. + /// + /// In zh, this message translates to: + /// **'语音识别中...'** + String get homeTranscribing; + + /// No description provided for @homeRecordingCanceled. + /// + /// In zh, this message translates to: + /// **'已取消'** + String get homeRecordingCanceled; + + /// No description provided for @homeToolPreparing. + /// + /// In zh, this message translates to: + /// **'工具准备中'** + String get homeToolPreparing; + + /// No description provided for @homeToolExecuting. + /// + /// In zh, this message translates to: + /// **'任务执行中'** + String get homeToolExecuting; + + /// No description provided for @homeToolExecutionFailed. + /// + /// In zh, this message translates to: + /// **'执行失败'** + String get homeToolExecutionFailed; + + /// No description provided for @homeToolCompleted. + /// + /// In zh, this message translates to: + /// **'已完成'** + String get homeToolCompleted; + + /// No description provided for @homeRecorderPluginUnavailable. + /// + /// In zh, this message translates to: + /// **'录音组件未加载,请完全重启 App 后重试'** + String get homeRecorderPluginUnavailable; + + /// No description provided for @homeRecorderPermissionDenied. + /// + /// In zh, this message translates to: + /// **'录音权限未授权'** + String get homeRecorderPermissionDenied; + + /// No description provided for @homeStopRequested. + /// + /// In zh, this message translates to: + /// **'已请求停止'** + String get homeStopRequested; + + /// No description provided for @homeNoValidSpeech. + /// + /// In zh, this message translates to: + /// **'未识别到有效语音,请靠近麦克风并连续说话后重试'** + String get homeNoValidSpeech; + + /// No description provided for @agentStageRouting. + /// + /// In zh, this message translates to: + /// **'意图识别中'** + String get agentStageRouting; + + /// No description provided for @agentStageExecution. + /// + /// In zh, this message translates to: + /// **'任务执行中'** + String get agentStageExecution; + + /// No description provided for @agentStageMemory. + /// + /// In zh, this message translates to: + /// **'记忆提取中'** + String get agentStageMemory; + + /// No description provided for @agentStageProcessing. + /// + /// In zh, this message translates to: + /// **'任务处理中'** + String get agentStageProcessing; + + /// No description provided for @agUiEventRunStarted. + /// + /// In zh, this message translates to: + /// **'运行开始'** + String get agUiEventRunStarted; + + /// No description provided for @agUiEventRunFinished. + /// + /// In zh, this message translates to: + /// **'运行完成'** + String get agUiEventRunFinished; + + /// No description provided for @agUiEventRunError. + /// + /// In zh, this message translates to: + /// **'运行失败'** + String get agUiEventRunError; + + /// No description provided for @agUiEventStepStarted. + /// + /// In zh, this message translates to: + /// **'阶段开始'** + String get agUiEventStepStarted; + + /// No description provided for @agUiEventStepFinished. + /// + /// In zh, this message translates to: + /// **'阶段完成'** + String get agUiEventStepFinished; + + /// No description provided for @agUiEventTextMessageEnd. + /// + /// In zh, this message translates to: + /// **'文本输出完成'** + String get agUiEventTextMessageEnd; + + /// No description provided for @agUiEventToolCallStart. + /// + /// In zh, this message translates to: + /// **'工具调用开始'** + String get agUiEventToolCallStart; + + /// No description provided for @agUiEventToolCallArgs. + /// + /// In zh, this message translates to: + /// **'工具参数更新'** + String get agUiEventToolCallArgs; + + /// No description provided for @agUiEventToolCallEnd. + /// + /// In zh, this message translates to: + /// **'工具调用结束'** + String get agUiEventToolCallEnd; + + /// No description provided for @agUiEventToolCallResult. + /// + /// In zh, this message translates to: + /// **'工具结果返回'** + String get agUiEventToolCallResult; + + /// No description provided for @agUiEventToolCallError. + /// + /// In zh, this message translates to: + /// **'工具调用失败'** + String get agUiEventToolCallError; + + /// No description provided for @agUiEventUnknown. + /// + /// In zh, this message translates to: + /// **'未知事件'** + String get agUiEventUnknown; + + /// No description provided for @chatRunCanceled. + /// + /// In zh, this message translates to: + /// **'本次运行已取消'** + String get chatRunCanceled; + + /// No description provided for @chatRunFailed. + /// + /// In zh, this message translates to: + /// **'本次运行已失败'** + String get chatRunFailed; + + /// No description provided for @chatSseInterruptedRetry. + /// + /// In zh, this message translates to: + /// **'连接中断,请重试'** + String get chatSseInterruptedRetry; + + /// No description provided for @chatTimestampToday. + /// + /// In zh, this message translates to: + /// **'今天'** + String get chatTimestampToday; + + /// No description provided for @chatTimestampYesterday. + /// + /// In zh, this message translates to: + /// **'昨天'** + String get chatTimestampYesterday; + + /// No description provided for @chatTimestampMonthDay. + /// + /// In zh, this message translates to: + /// **'{month}月{day}日'** + String chatTimestampMonthDay(int month, int day); + + /// No description provided for @homeUnreadMessages. + /// + /// In zh, this message translates to: + /// **'有{count}条新消息'** + String homeUnreadMessages(int count); + + /// No description provided for @calendarToday. + /// + /// In zh, this message translates to: + /// **'今天'** + String get calendarToday; + + /// No description provided for @calendarEventNoAccessOrMissing. + /// + /// In zh, this message translates to: + /// **'日程不存在或无权限'** + String get calendarEventNoAccessOrMissing; + + /// No description provided for @calendarDayWeekMonthYearLabel. + /// + /// In zh, this message translates to: + /// **'{year}年{month}月'** + String calendarDayWeekMonthYearLabel(int year, int month); + + /// No description provided for @validatorPhoneRequired. + /// + /// In zh, this message translates to: + /// **'请输入手机号'** + String get validatorPhoneRequired; + + /// No description provided for @validatorPhoneInvalid86. + /// + /// In zh, this message translates to: + /// **'请输入有效的 +86 手机号'** + String get validatorPhoneInvalid86; + + /// No description provided for @validatorPasswordRequired. + /// + /// In zh, this message translates to: + /// **'请输入密码'** + String get validatorPasswordRequired; + + /// No description provided for @validatorPasswordMin8. + /// + /// In zh, this message translates to: + /// **'密码至少需要8位'** + String get validatorPasswordMin8; + + /// No description provided for @validatorRequired. + /// + /// In zh, this message translates to: + /// **'请输入{fieldName}'** + String validatorRequired(Object fieldName); + + /// No description provided for @validatorNicknameRequired. + /// + /// In zh, this message translates to: + /// **'请输入昵称'** + String get validatorNicknameRequired; + + /// No description provided for @validatorNicknameMin2. + /// + /// In zh, this message translates to: + /// **'昵称至少需要2个字符'** + String get validatorNicknameMin2; + + /// No description provided for @authAgreementTitle. + /// + /// In zh, this message translates to: + /// **'请先同意协议'** + String get authAgreementTitle; + + /// No description provided for @authAgreementMessage. + /// + /// In zh, this message translates to: + /// **'在使用我们的服务之前,请先阅读并同意《用户协议》和《隐私政策》。\n\n只有您同意上述协议,我们才能为您提供服务。'** + String get authAgreementMessage; + + /// No description provided for @authAgreementSemantics. + /// + /// In zh, this message translates to: + /// **'同意用户协议与隐私政策'** + String get authAgreementSemantics; + + /// No description provided for @authAgreementPrefix. + /// + /// In zh, this message translates to: + /// **'我已同意'** + String get authAgreementPrefix; + + /// No description provided for @authAgreementTerms. + /// + /// In zh, this message translates to: + /// **'《用户协议》'** + String get authAgreementTerms; + + /// No description provided for @authAgreementAnd. + /// + /// In zh, this message translates to: + /// **'与'** + String get authAgreementAnd; + + /// No description provided for @authAgreementPrivacy. + /// + /// In zh, this message translates to: + /// **'《隐私政策》'** + String get authAgreementPrivacy; + + /// No description provided for @authPhoneHint. + /// + /// In zh, this message translates to: + /// **'输入手机号'** + String get authPhoneHint; + + /// No description provided for @authCodeHint. + /// + /// In zh, this message translates to: + /// **'输入验证码'** + String get authCodeHint; + + /// No description provided for @authSendCode. + /// + /// In zh, this message translates to: + /// **'发送验证码'** + String get authSendCode; + + /// No description provided for @authShowPassword. + /// + /// In zh, this message translates to: + /// **'显示密码'** + String get authShowPassword; + + /// No description provided for @authHidePassword. + /// + /// In zh, this message translates to: + /// **'隐藏密码'** + String get authHidePassword; + + /// No description provided for @authLoginFailed. + /// + /// In zh, this message translates to: + /// **'登录失败'** + String get authLoginFailed; + + /// No description provided for @authCheckInput. + /// + /// In zh, this message translates to: + /// **'请检查输入'** + String get authCheckInput; + + /// No description provided for @authLoginOrRegister. + /// + /// In zh, this message translates to: + /// **'登录/注册'** + String get authLoginOrRegister; + + /// No description provided for @authInvalidPhone. + /// + /// In zh, this message translates to: + /// **'请输入有效手机号'** + String get authInvalidPhone; + + /// No description provided for @authSendCodeFailed. + /// + /// In zh, this message translates to: + /// **'验证码发送失败'** + String get authSendCodeFailed; + + /// No description provided for @inputUsernameRequired. + /// + /// In zh, this message translates to: + /// **'请输入用户名'** + String get inputUsernameRequired; + + /// No description provided for @inputUsernameMin. + /// + /// In zh, this message translates to: + /// **'用户名至少 3 个字符'** + String get inputUsernameMin; + + /// No description provided for @inputUsernameMax. + /// + /// In zh, this message translates to: + /// **'用户名最多 30 个字符'** + String get inputUsernameMax; + + /// No description provided for @inputPhoneRequired. + /// + /// In zh, this message translates to: + /// **'请输入手机号'** + String get inputPhoneRequired; + + /// No description provided for @inputPhoneInvalid. + /// + /// In zh, this message translates to: + /// **'手机号格式不正确'** + String get inputPhoneInvalid; + + /// No description provided for @inputPasswordRequired. + /// + /// In zh, this message translates to: + /// **'请输入密码'** + String get inputPasswordRequired; + + /// No description provided for @inputPasswordMin. + /// + /// In zh, this message translates to: + /// **'密码至少 6 个字符'** + String get inputPasswordMin; + + /// No description provided for @inputCodeRequired. + /// + /// In zh, this message translates to: + /// **'请输入验证码'** + String get inputCodeRequired; + + /// No description provided for @inputCodeInvalid. + /// + /// In zh, this message translates to: + /// **'验证码必须是 6 位数字'** + String get inputCodeInvalid; + + /// No description provided for @uiSchemaInvalid. + /// + /// In zh, this message translates to: + /// **'无效 UI Schema'** + String get uiSchemaInvalid; + + /// No description provided for @uiSchemaUnsupportedLayout. + /// + /// In zh, this message translates to: + /// **'不支持的布局节点: {type}'** + String uiSchemaUnsupportedLayout(Object type); + + /// No description provided for @uiSchemaUnknownNode. + /// + /// In zh, this message translates to: + /// **'未知节点: {type}'** + String uiSchemaUnknownNode(Object type); + + /// No description provided for @uiSchemaActionFallback. + /// + /// In zh, this message translates to: + /// **'操作'** + String get uiSchemaActionFallback; + + /// No description provided for @uiSchemaActionNotImplemented. + /// + /// In zh, this message translates to: + /// **'该操作暂未接入'** + String get uiSchemaActionNotImplemented; + + /// No description provided for @uiSchemaNavigationInvalidParams. + /// + /// In zh, this message translates to: + /// **'导航参数无效'** + String get uiSchemaNavigationInvalidParams; + + /// No description provided for @uiSchemaNavigationInvalidPath. + /// + /// In zh, this message translates to: + /// **'导航路径无效'** + String get uiSchemaNavigationInvalidPath; + + /// No description provided for @notificationSnoozeMinutes. + /// + /// In zh, this message translates to: + /// **'{minutes} 分钟'** + String notificationSnoozeMinutes(int minutes); + + /// No description provided for @notificationSnoozeLater. + /// + /// In zh, this message translates to: + /// **'稍后提醒'** + String get notificationSnoozeLater; + + /// No description provided for @notificationChannelName. + /// + /// In zh, this message translates to: + /// **'日程闹钟提醒'** + String get notificationChannelName; + + /// No description provided for @notificationChannelDescription. + /// + /// In zh, this message translates to: + /// **'日程到点闹钟式提醒通知'** + String get notificationChannelDescription; + + /// No description provided for @notificationStartsNow. + /// + /// In zh, this message translates to: + /// **'日程现在开始'** + String get notificationStartsNow; + + /// No description provided for @notificationStartsInMinutes. + /// + /// In zh, this message translates to: + /// **'日程即将开始(提前{minutes}分钟)'** + String notificationStartsInMinutes(int minutes); + + /// No description provided for @notificationLocation. + /// + /// In zh, this message translates to: + /// **'地点:{location}'** + String notificationLocation(Object location); + + /// No description provided for @notificationNotes. + /// + /// In zh, this message translates to: + /// **'备注:{notes}'** + String notificationNotes(Object notes); + + /// No description provided for @todoScreenTitle. + /// + /// In zh, this message translates to: + /// **'待办事项'** + String get todoScreenTitle; + + /// No description provided for @todoDetailTitle. + /// + /// In zh, this message translates to: + /// **'待办详情'** + String get todoDetailTitle; + + /// No description provided for @todoCreateTitle. + /// + /// In zh, this message translates to: + /// **'新建待办'** + String get todoCreateTitle; + + /// No description provided for @todoEditTitle. + /// + /// In zh, this message translates to: + /// **'编辑待办'** + String get todoEditTitle; + + /// No description provided for @todoMoveFailed. + /// + /// In zh, this message translates to: + /// **'移动失败'** + String get todoMoveFailed; + + /// No description provided for @todoRefreshFailed. + /// + /// In zh, this message translates to: + /// **'刷新失败,请稍后重试'** + String get todoRefreshFailed; + + /// No description provided for @todoCompleteFailed. + /// + /// In zh, this message translates to: + /// **'完成失败: {error}'** + String todoCompleteFailed(Object error); + + /// No description provided for @todoNotFound. + /// + /// In zh, this message translates to: + /// **'待办不存在'** + String get todoNotFound; + + /// No description provided for @todoCalendarEventCards. + /// + /// In zh, this message translates to: + /// **'日历事件卡片'** + String get todoCalendarEventCards; + + /// No description provided for @todoPriorityQuadrant. + /// + /// In zh, this message translates to: + /// **'所属象限'** + String get todoPriorityQuadrant; + + /// No description provided for @todoLinkedCalendarEvents. + /// + /// In zh, this message translates to: + /// **'关联日历事件'** + String get todoLinkedCalendarEvents; + + /// No description provided for @todoStatus. + /// + /// In zh, this message translates to: + /// **'状态'** + String get todoStatus; + + /// No description provided for @todoStatusDone. + /// + /// In zh, this message translates to: + /// **'已完成'** + String get todoStatusDone; + + /// No description provided for @todoStatusInProgress. + /// + /// In zh, this message translates to: + /// **'进行中'** + String get todoStatusInProgress; + + /// No description provided for @todoQuadrantOrder. + /// + /// In zh, this message translates to: + /// **'象限内顺序 #{order}'** + String todoQuadrantOrder(int order); + + /// No description provided for @todoSplitToEvents. + /// + /// In zh, this message translates to: + /// **'已拆分为{count}个日历事件'** + String todoSplitToEvents(int count); + + /// No description provided for @todoNoLinkedEvents. + /// + /// In zh, this message translates to: + /// **'未关联日历事件'** + String get todoNoLinkedEvents; + + /// No description provided for @todoDeleteTitle. + /// + /// In zh, this message translates to: + /// **'删除待办'** + String get todoDeleteTitle; + + /// No description provided for @todoDeleteMessage. + /// + /// In zh, this message translates to: + /// **'确定要删除这个待办吗?'** + String get todoDeleteMessage; + + /// No description provided for @todoDeleteConfirm. + /// + /// In zh, this message translates to: + /// **'确认删除'** + String get todoDeleteConfirm; + + /// No description provided for @todoDeleteFailed. + /// + /// In zh, this message translates to: + /// **'删除失败: {error}'** + String todoDeleteFailed(Object error); + + /// No description provided for @todoQuadrantImportantUrgent. + /// + /// In zh, this message translates to: + /// **'重要紧急'** + String get todoQuadrantImportantUrgent; + + /// No description provided for @todoQuadrantUrgentNotImportant. + /// + /// In zh, this message translates to: + /// **'紧急不重要'** + String get todoQuadrantUrgentNotImportant; + + /// No description provided for @todoQuadrantImportantNotUrgent. + /// + /// In zh, this message translates to: + /// **'重要不紧急'** + String get todoQuadrantImportantNotUrgent; + + /// No description provided for @todoQuadrantNotUrgentNotImportant. + /// + /// In zh, this message translates to: + /// **'不紧急不重要'** + String get todoQuadrantNotUrgentNotImportant; + + /// No description provided for @todoNoItems. + /// + /// In zh, this message translates to: + /// **'暂无待办'** + String get todoNoItems; + + /// No description provided for @todoItemCount. + /// + /// In zh, this message translates to: + /// **'{count}项'** + String todoItemCount(int count); + + /// No description provided for @todoInfoTitle. + /// + /// In zh, this message translates to: + /// **'待办信息'** + String get todoInfoTitle; + + /// No description provided for @todoInfoDescCreate. + /// + /// In zh, this message translates to: + /// **'创建后可在四象限中查看并继续调整优先级与关联事件。'** + String get todoInfoDescCreate; + + /// No description provided for @todoInfoDescDone. + /// + /// In zh, this message translates to: + /// **'该待办已完成,你仍可调整内容并重新组织关联事件。'** + String get todoInfoDescDone; + + /// No description provided for @todoInfoDescDefault. + /// + /// In zh, this message translates to: + /// **'调整标题、优先级和关联事件,保持任务结构清晰。'** + String get todoInfoDescDefault; + + /// No description provided for @todoFieldTitle. + /// + /// In zh, this message translates to: + /// **'标题'** + String get todoFieldTitle; + + /// No description provided for @todoFieldTitleHint. + /// + /// In zh, this message translates to: + /// **'输入待办标题'** + String get todoFieldTitleHint; + + /// No description provided for @todoFieldDescriptionOptional. + /// + /// In zh, this message translates to: + /// **'描述(可选)'** + String get todoFieldDescriptionOptional; + + /// No description provided for @todoFieldDescriptionHint. + /// + /// In zh, this message translates to: + /// **'补充细节或备注'** + String get todoFieldDescriptionHint; + + /// No description provided for @todoPriority. + /// + /// In zh, this message translates to: + /// **'优先级'** + String get todoPriority; + + /// No description provided for @todoNoSelectableCalendarEvents. + /// + /// In zh, this message translates to: + /// **'暂无可关联的日历事件'** + String get todoNoSelectableCalendarEvents; + + /// No description provided for @todoSaveInProgress. + /// + /// In zh, this message translates to: + /// **'保存中...'** + String get todoSaveInProgress; + + /// No description provided for @todoCreateButton. + /// + /// In zh, this message translates to: + /// **'创建待办'** + String get todoCreateButton; + + /// No description provided for @todoSaveChanges. + /// + /// In zh, this message translates to: + /// **'保存修改'** + String get todoSaveChanges; + + /// No description provided for @todoEnterTitle. + /// + /// In zh, this message translates to: + /// **'请输入标题'** + String get todoEnterTitle; + + /// No description provided for @todoSaveFailed. + /// + /// In zh, this message translates to: + /// **'保存失败: {error}'** + String todoSaveFailed(Object error); + + /// No description provided for @contactsTitle. + /// + /// In zh, this message translates to: + /// **'联系人'** + String get contactsTitle; + + /// No description provided for @contactsSearchHint. + /// + /// In zh, this message translates to: + /// **'输入用户名或手机号'** + String get contactsSearchHint; + + /// No description provided for @contactsSearchEmptyQuery. + /// + /// In zh, this message translates to: + /// **'请输入用户名或手机号'** + String get contactsSearchEmptyQuery; + + /// No description provided for @contactsSearchFailed. + /// + /// In zh, this message translates to: + /// **'搜索失败,请稍后重试'** + String get contactsSearchFailed; + + /// No description provided for @contactsSearchNoUser. + /// + /// In zh, this message translates to: + /// **'未找到该用户'** + String get contactsSearchNoUser; + + /// No description provided for @contactsFriendRequestSent. + /// + /// In zh, this message translates to: + /// **'好友请求已发送'** + String get contactsFriendRequestSent; + + /// No description provided for @contactsSendFailed. + /// + /// In zh, this message translates to: + /// **'发送失败,请稍后重试'** + String get contactsSendFailed; + + /// No description provided for @contactsSectionNew. + /// + /// In zh, this message translates to: + /// **'新的联系人'** + String get contactsSectionNew; + + /// No description provided for @contactsSectionAll. + /// + /// In zh, this message translates to: + /// **'全部联系人'** + String get contactsSectionAll; + + /// No description provided for @contactsStatusAlreadyFriend. + /// + /// In zh, this message translates to: + /// **'已是好友'** + String get contactsStatusAlreadyFriend; + + /// No description provided for @contactsStatusSent. + /// + /// In zh, this message translates to: + /// **'已发送'** + String get contactsStatusSent; + + /// No description provided for @contactsAdd. + /// + /// In zh, this message translates to: + /// **'添加'** + String get contactsAdd; + + /// No description provided for @contactsEmptyTitle. + /// + /// In zh, this message translates to: + /// **'暂无联系人'** + String get contactsEmptyTitle; + + /// No description provided for @contactsEmptyDesc. + /// + /// In zh, this message translates to: + /// **'搜索手机号添加好友开始聊天吧'** + String get contactsEmptyDesc; + + /// No description provided for @contactsPendingConfirm. + /// + /// In zh, this message translates to: + /// **'等待对方确认'** + String get contactsPendingConfirm; + + /// No description provided for @contactsAddSheetTitle. + /// + /// In zh, this message translates to: + /// **'添加 {username}'** + String contactsAddSheetTitle(Object username); + + /// No description provided for @contactsAddSheetDesc. + /// + /// In zh, this message translates to: + /// **'发送一条验证信息,方便对方确认你的身份'** + String get contactsAddSheetDesc; + + /// No description provided for @contactsAddSheetMessageHint. + /// + /// In zh, this message translates to: + /// **'你好,我是...'** + String get contactsAddSheetMessageHint; + + /// No description provided for @contactsSend. + /// + /// In zh, this message translates to: + /// **'发送'** + String get contactsSend; + + /// No description provided for @contactEditTitle. + /// + /// In zh, this message translates to: + /// **'编辑联系人'** + String get contactEditTitle; + + /// No description provided for @contactAddTitle. + /// + /// In zh, this message translates to: + /// **'添加联系人'** + String get contactAddTitle; + + /// No description provided for @contactNickname. + /// + /// In zh, this message translates to: + /// **'昵称'** + String get contactNickname; + + /// No description provided for @contactNicknameHint. + /// + /// In zh, this message translates to: + /// **'请输入昵称'** + String get contactNicknameHint; + + /// No description provided for @contactPhone. + /// + /// In zh, this message translates to: + /// **'手机号'** + String get contactPhone; + + /// No description provided for @contactPhoneHint. + /// + /// In zh, this message translates to: + /// **'+86 请输入 11 位手机号'** + String get contactPhoneHint; + + /// No description provided for @contactRemark. + /// + /// In zh, this message translates to: + /// **'备注'** + String get contactRemark; + + /// No description provided for @contactRemarkHint. + /// + /// In zh, this message translates to: + /// **'请输入备注'** + String get contactRemarkHint; + + /// No description provided for @contactDelete. + /// + /// In zh, this message translates to: + /// **'删除联系人'** + String get contactDelete; + + /// No description provided for @contactFillRequired. + /// + /// In zh, this message translates to: + /// **'请填写昵称和手机号'** + String get contactFillRequired; + + /// No description provided for @contactDeleteConfirmTitle. + /// + /// In zh, this message translates to: + /// **'删除联系人'** + String get contactDeleteConfirmTitle; + + /// No description provided for @contactDeleteConfirmMessage. + /// + /// In zh, this message translates to: + /// **'确定要删除此联系人吗?'** + String get contactDeleteConfirmMessage; + + /// No description provided for @messagesLoadFailed. + /// + /// In zh, this message translates to: + /// **'消息加载失败,请稍后重试'** + String get messagesLoadFailed; + + /// No description provided for @messagesSenderLoadFailed. + /// + /// In zh, this message translates to: + /// **'发送者信息加载失败,请下拉重试'** + String get messagesSenderLoadFailed; + + /// No description provided for @messagesFriendRequestMissing. + /// + /// In zh, this message translates to: + /// **'好友请求数据缺失'** + String get messagesFriendRequestMissing; + + /// No description provided for @messagesAcceptedFriendRequest. + /// + /// In zh, this message translates to: + /// **'已接受好友请求'** + String get messagesAcceptedFriendRequest; + + /// No description provided for @messagesRejectedFriendRequest. + /// + /// In zh, this message translates to: + /// **'已拒绝好友请求'** + String get messagesRejectedFriendRequest; + + /// No description provided for @messagesActionFailed. + /// + /// In zh, this message translates to: + /// **'处理失败,请稍后重试'** + String get messagesActionFailed; + + /// No description provided for @messagesTabUnread. + /// + /// In zh, this message translates to: + /// **'未读'** + String get messagesTabUnread; + + /// No description provided for @messagesTabRead. + /// + /// In zh, this message translates to: + /// **'已读'** + String get messagesTabRead; + + /// No description provided for @messagesEmptyUnreadTitle. + /// + /// In zh, this message translates to: + /// **'暂无未读消息'** + String get messagesEmptyUnreadTitle; + + /// No description provided for @messagesEmptyReadTitle. + /// + /// In zh, this message translates to: + /// **'暂无已读消息'** + String get messagesEmptyReadTitle; + + /// No description provided for @messagesEmptyUnreadDesc. + /// + /// In zh, this message translates to: + /// **'有新消息时会在这里显示'** + String get messagesEmptyUnreadDesc; + + /// No description provided for @messagesEmptyReadDesc. + /// + /// In zh, this message translates to: + /// **'处理过的消息会显示在这里'** + String get messagesEmptyReadDesc; + + /// No description provided for @messagesFriendRequestLoadFailed. + /// + /// In zh, this message translates to: + /// **'好友请求信息加载失败'** + String get messagesFriendRequestLoadFailed; + + /// No description provided for @messagesFriendRequestTitle. + /// + /// In zh, this message translates to: + /// **'{username} 请求添加您为好友'** + String messagesFriendRequestTitle(Object username); + + /// No description provided for @messagesCalendarInvite. + /// + /// In zh, this message translates to: + /// **'日历邀请'** + String get messagesCalendarInvite; + + /// No description provided for @messagesSystemMessage. + /// + /// In zh, this message translates to: + /// **'系统消息'** + String get messagesSystemMessage; + + /// No description provided for @messagesTapToView. + /// + /// In zh, this message translates to: + /// **'点击查看详情'** + String get messagesTapToView; + + /// No description provided for @messagesInviteJoinCalendar. + /// + /// In zh, this message translates to: + /// **'邀请您加入日历'** + String get messagesInviteJoinCalendar; + + /// No description provided for @messagesInviteAccepted. + /// + /// In zh, this message translates to: + /// **'已接受日历邀请'** + String get messagesInviteAccepted; + + /// No description provided for @messagesInviteRejected. + /// + /// In zh, this message translates to: + /// **'已拒绝日历邀请'** + String get messagesInviteRejected; + + /// No description provided for @messagesCalendarUpdated. + /// + /// In zh, this message translates to: + /// **'更新了日历事件'** + String get messagesCalendarUpdated; + + /// No description provided for @messagesInviteStatusAccepted. + /// + /// In zh, this message translates to: + /// **'已接受'** + String get messagesInviteStatusAccepted; + + /// No description provided for @messagesInviteStatusRejected. + /// + /// In zh, this message translates to: + /// **'已拒绝'** + String get messagesInviteStatusRejected; + + /// No description provided for @messagesInviteStatusHandled. + /// + /// In zh, this message translates to: + /// **'已处理'** + String get messagesInviteStatusHandled; + + /// No description provided for @messagesInviteDetailNotFound. + /// + /// In zh, this message translates to: + /// **'邀请不存在或已失效'** + String get messagesInviteDetailNotFound; + + /// No description provided for @messagesInviteAcceptedToast. + /// + /// In zh, this message translates to: + /// **'已接受邀请'** + String get messagesInviteAcceptedToast; + + /// No description provided for @messagesInviteRejectedToast. + /// + /// In zh, this message translates to: + /// **'已拒绝邀请'** + String get messagesInviteRejectedToast; + + /// No description provided for @messagesInviteOperationFailed. + /// + /// In zh, this message translates to: + /// **'操作失败,请稍后重试'** + String get messagesInviteOperationFailed; + + /// No description provided for @messagesInviteDetailTitle. + /// + /// In zh, this message translates to: + /// **'日历邀请详情'** + String get messagesInviteDetailTitle; + + /// No description provided for @messagesInviteEvent. + /// + /// In zh, this message translates to: + /// **'事件:{title}'** + String messagesInviteEvent(Object title); + + /// No description provided for @messagesInviteUnnamedEvent. + /// + /// In zh, this message translates to: + /// **'未命名日程'** + String get messagesInviteUnnamedEvent; + + /// No description provided for @messagesInviteSender. + /// + /// In zh, this message translates to: + /// **'邀请人:{name}'** + String messagesInviteSender(Object name); + + /// No description provided for @messagesInviteUnknownUser. + /// + /// In zh, this message translates to: + /// **'未知用户'** + String get messagesInviteUnknownUser; + + /// No description provided for @messagesInviteTime. + /// + /// In zh, this message translates to: + /// **'消息时间:{time}'** + String messagesInviteTime(Object time); + + /// No description provided for @messagesInviteStatus. + /// + /// In zh, this message translates to: + /// **'状态:{status}'** + String messagesInviteStatus(Object status); + + /// No description provided for @messagesInviteId. + /// + /// In zh, this message translates to: + /// **'邀请ID:{id}'** + String messagesInviteId(Object id); + + /// No description provided for @messagesInviteTip. + /// + /// In zh, this message translates to: + /// **'同意后将加入该日历事件,拒绝后该邀请会被标记为已处理'** + String get messagesInviteTip; + + /// No description provided for @messagesInviteAlreadyHandled. + /// + /// In zh, this message translates to: + /// **'该邀请已处理,无需重复操作'** + String get messagesInviteAlreadyHandled; + + /// No description provided for @messagesReject. + /// + /// In zh, this message translates to: + /// **'拒绝'** + String get messagesReject; + + /// No description provided for @messagesAccept. + /// + /// In zh, this message translates to: + /// **'同意'** + String get messagesAccept; + + /// No description provided for @messagesStatusPending. + /// + /// In zh, this message translates to: + /// **'待处理'** + String get messagesStatusPending; + + /// No description provided for @settingsFeaturesTitle. + /// + /// In zh, this message translates to: + /// **'周期计划'** + String get settingsFeaturesTitle; + + /// No description provided for @settingsSectionDaily. + /// + /// In zh, this message translates to: + /// **'每日'** + String get settingsSectionDaily; + + /// No description provided for @settingsSectionWeekly. + /// + /// In zh, this message translates to: + /// **'每周'** + String get settingsSectionWeekly; + + /// No description provided for @settingsNoDailyPlans. + /// + /// In zh, this message translates to: + /// **'暂无每日计划'** + String get settingsNoDailyPlans; + + /// No description provided for @settingsNoWeeklyPlans. + /// + /// In zh, this message translates to: + /// **'暂无每周计划'** + String get settingsNoWeeklyPlans; + + /// No description provided for @settingsSystemJobReadonly. + /// + /// In zh, this message translates to: + /// **'系统预置任务状态不可修改'** + String get settingsSystemJobReadonly; + + /// No description provided for @settingsJobStatusEnabled. + /// + /// In zh, this message translates to: + /// **'已启用'** + String get settingsJobStatusEnabled; + + /// No description provided for @settingsJobStatusDisabled. + /// + /// In zh, this message translates to: + /// **'未启用'** + String get settingsJobStatusDisabled; + + /// No description provided for @settingsJobSourceSystem. + /// + /// In zh, this message translates to: + /// **'系统预置'** + String get settingsJobSourceSystem; + + /// No description provided for @settingsJobSourceCustom. + /// + /// In zh, this message translates to: + /// **'自定义'** + String get settingsJobSourceCustom; + + /// No description provided for @settingsCreateJob. + /// + /// In zh, this message translates to: + /// **'创建任务'** + String get settingsCreateJob; + + /// No description provided for @memoryTitle. + /// + /// In zh, this message translates to: + /// **'我的记忆'** + String get memoryTitle; + + /// No description provided for @memoryLoadFailedRetry. + /// + /// In zh, this message translates to: + /// **'加载失败,请重试'** + String get memoryLoadFailedRetry; + + /// No description provided for @memorySmartTitle. + /// + /// In zh, this message translates to: + /// **'智能记忆'** + String get memorySmartTitle; + + /// No description provided for @memorySmartDesc. + /// + /// In zh, this message translates to: + /// **'持续学习你的偏好和习惯'** + String get memorySmartDesc; + + /// No description provided for @memoryReload. + /// + /// In zh, this message translates to: + /// **'重新加载'** + String get memoryReload; + + /// No description provided for @memorySectionUser. + /// + /// In zh, this message translates to: + /// **'用户记忆'** + String get memorySectionUser; + + /// No description provided for @memorySectionWork. + /// + /// In zh, this message translates to: + /// **'工作记忆'** + String get memorySectionWork; + + /// No description provided for @memoryUserProfile. + /// + /// In zh, this message translates to: + /// **'个人偏好'** + String get memoryUserProfile; + + /// No description provided for @memoryWorkProfile. + /// + /// In zh, this message translates to: + /// **'工作画像'** + String get memoryWorkProfile; + + /// No description provided for @memoryNoInfo. + /// + /// In zh, this message translates to: + /// **'暂无信息'** + String get memoryNoInfo; + + /// No description provided for @memoryStatContacts. + /// + /// In zh, this message translates to: + /// **'联系人'** + String get memoryStatContacts; + + /// No description provided for @memoryStatPlaces. + /// + /// In zh, this message translates to: + /// **'地点'** + String get memoryStatPlaces; + + /// No description provided for @memoryStatInterests. + /// + /// In zh, this message translates to: + /// **'兴趣'** + String get memoryStatInterests; + + /// No description provided for @memoryStatSchedule. + /// + /// In zh, this message translates to: + /// **'日程'** + String get memoryStatSchedule; + + /// No description provided for @memoryStatExpertise. + /// + /// In zh, this message translates to: + /// **'专长'** + String get memoryStatExpertise; + + /// No description provided for @memoryStatTools. + /// + /// In zh, this message translates to: + /// **'工具'** + String get memoryStatTools; + + /// No description provided for @memoryStatProjects. + /// + /// In zh, this message translates to: + /// **'项目'** + String get memoryStatProjects; + + /// No description provided for @memoryStatTeam. + /// + /// In zh, this message translates to: + /// **'团队'** + String get memoryStatTeam; + + /// No description provided for @memorySummaryContactsCount. + /// + /// In zh, this message translates to: + /// **'{count} 位联系人'** + String memorySummaryContactsCount(int count); + + /// No description provided for @memorySummaryPlacesCount. + /// + /// In zh, this message translates to: + /// **'{count} 个地点'** + String memorySummaryPlacesCount(int count); + + /// No description provided for @memorySummaryInterestsCount. + /// + /// In zh, this message translates to: + /// **'{count} 个兴趣'** + String memorySummaryInterestsCount(int count); + + /// No description provided for @memorySummaryExpertiseCount. + /// + /// In zh, this message translates to: + /// **'{count} 项专长'** + String memorySummaryExpertiseCount(int count); + + /// No description provided for @memorySummaryProjectsCount. + /// + /// In zh, this message translates to: + /// **'{count} 个项目'** + String memorySummaryProjectsCount(int count); + + /// No description provided for @memorySummaryTeamMembersCount. + /// + /// In zh, this message translates to: + /// **'{count} 位团队成员'** + String memorySummaryTeamMembersCount(int count); + + /// No description provided for @toolCalendarRead. + /// + /// In zh, this message translates to: + /// **'读取日程'** + String get toolCalendarRead; + + /// No description provided for @toolCalendarWrite. + /// + /// In zh, this message translates to: + /// **'写入日程'** + String get toolCalendarWrite; + + /// No description provided for @toolCalendarShare. + /// + /// In zh, this message translates to: + /// **'共享日程'** + String get toolCalendarShare; + + /// No description provided for @toolUserLookup. + /// + /// In zh, this message translates to: + /// **'查找联系人'** + String get toolUserLookup; + + /// No description provided for @toolMemoryWrite. + /// + /// In zh, this message translates to: + /// **'写入记忆'** + String get toolMemoryWrite; + + /// No description provided for @toolMemoryForget. + /// + /// In zh, this message translates to: + /// **'清理记忆'** + String get toolMemoryForget; + + /// No description provided for @settingsTitle. + /// + /// In zh, this message translates to: + /// **'设置'** + String get settingsTitle; + + /// No description provided for @settingsUnset. + /// + /// In zh, this message translates to: + /// **'未设置'** + String get settingsUnset; + + /// No description provided for @settingsFreeBadge. + /// + /// In zh, this message translates to: + /// **'免费'** + String get settingsFreeBadge; + + /// No description provided for @settingsNoContacts. + /// + /// In zh, this message translates to: + /// **'暂无联系人'** + String get settingsNoContacts; + + /// No description provided for @settingsContactsAddedOne. + /// + /// In zh, this message translates to: + /// **'已添加 1 位:{name}'** + String settingsContactsAddedOne(Object name); + + /// No description provided for @settingsContactsAddedMany. + /// + /// In zh, this message translates to: + /// **'已添加 {count} 位联系人'** + String settingsContactsAddedMany(int count); + + /// No description provided for @settingsNoEnabledPlans. + /// + /// In zh, this message translates to: + /// **'暂无启用计划'** + String get settingsNoEnabledPlans; + + /// No description provided for @settingsEnabledPlanOne. + /// + /// In zh, this message translates to: + /// **'已启用:{title}'** + String settingsEnabledPlanOne(Object title); + + /// No description provided for @settingsEnabledPlanMany. + /// + /// In zh, this message translates to: + /// **'已启用 {count} 个计划'** + String settingsEnabledPlanMany(int count); + + /// No description provided for @settingsUpgradeProTitle. + /// + /// In zh, this message translates to: + /// **'升级到 Pro'** + String get settingsUpgradeProTitle; + + /// No description provided for @settingsUpgradeProDesc. + /// + /// In zh, this message translates to: + /// **'解锁更多高级功能'** + String get settingsUpgradeProDesc; + + /// No description provided for @settingsUpgradeButton. + /// + /// In zh, this message translates to: + /// **'升级'** + String get settingsUpgradeButton; + + /// No description provided for @settingsMenuNotifications. + /// + /// In zh, this message translates to: + /// **'提醒设置'** + String get settingsMenuNotifications; + + /// No description provided for @settingsMenuCheckUpdates. + /// + /// In zh, this message translates to: + /// **'检查更新'** + String get settingsMenuCheckUpdates; + + /// No description provided for @settingsLogoutTitle. + /// + /// In zh, this message translates to: + /// **'退出登录'** + String get settingsLogoutTitle; + + /// No description provided for @settingsLogoutConfirmMessage. + /// + /// In zh, this message translates to: + /// **'确定退出当前账户吗?'** + String get settingsLogoutConfirmMessage; + + /// No description provided for @settingsLogoutConfirm. + /// + /// In zh, this message translates to: + /// **'确认退出'** + String get settingsLogoutConfirm; + + /// No description provided for @settingsLogoutFailed. + /// + /// In zh, this message translates to: + /// **'退出失败,请稍后重试'** + String get settingsLogoutFailed; + + /// No description provided for @settingsLatestVersion. + /// + /// In zh, this message translates to: + /// **'当前已是最新版本'** + String get settingsLatestVersion; + + /// No description provided for @settingsUpdateRequired. + /// + /// In zh, this message translates to: + /// **'有新版本可用 ({version}),请立即更新'** + String settingsUpdateRequired(Object version); + + /// No description provided for @settingsUpdateOptional. + /// + /// In zh, this message translates to: + /// **'发现新版本 ({version}),是否更新?'** + String settingsUpdateOptional(Object version); + + /// No description provided for @settingsUpdateDialogTitle. + /// + /// In zh, this message translates to: + /// **'检查更新'** + String get settingsUpdateDialogTitle; + + /// No description provided for @settingsUpdateAction. + /// + /// In zh, this message translates to: + /// **'更新'** + String get settingsUpdateAction; + + /// No description provided for @settingsDownloadLink. + /// + /// In zh, this message translates to: + /// **'下载链接: {url}'** + String settingsDownloadLink(Object url); + + /// No description provided for @settingsUpdateCheckFailed. + /// + /// In zh, this message translates to: + /// **'检查更新失败'** + String get settingsUpdateCheckFailed; + + /// No description provided for @settingsJobDetailTitle. + /// + /// In zh, this message translates to: + /// **'任务详情'** + String get settingsJobDetailTitle; + + /// No description provided for @settingsJobCreatePageTitle. + /// + /// In zh, this message translates to: + /// **'新建周期计划'** + String get settingsJobCreatePageTitle; + + /// No description provided for @settingsJobLoadFailed. + /// + /// In zh, this message translates to: + /// **'加载失败'** + String get settingsJobLoadFailed; + + /// No description provided for @settingsJobRetry. + /// + /// In zh, this message translates to: + /// **'重试'** + String get settingsJobRetry; + + /// No description provided for @settingsJobPlanConfig. + /// + /// In zh, this message translates to: + /// **'计划配置'** + String get settingsJobPlanConfig; + + /// No description provided for @settingsJobCycle. + /// + /// In zh, this message translates to: + /// **'周期'** + String get settingsJobCycle; + + /// No description provided for @settingsJobRunAt. + /// + /// In zh, this message translates to: + /// **'执行时间'** + String get settingsJobRunAt; + + /// No description provided for @settingsJobTimezone. + /// + /// In zh, this message translates to: + /// **'时区'** + String get settingsJobTimezone; + + /// No description provided for @settingsJobStatusLabel. + /// + /// In zh, this message translates to: + /// **'状态'** + String get settingsJobStatusLabel; + + /// No description provided for @settingsJobInputTemplate. + /// + /// In zh, this message translates to: + /// **'输入模板'** + String get settingsJobInputTemplate; + + /// No description provided for @settingsJobEnabledTools. + /// + /// In zh, this message translates to: + /// **'启用工具'** + String get settingsJobEnabledTools; + + /// No description provided for @settingsJobContextMode. + /// + /// In zh, this message translates to: + /// **'上下文消息模式'** + String get settingsJobContextMode; + + /// No description provided for @settingsJobContextSource. + /// + /// In zh, this message translates to: + /// **'来源'** + String get settingsJobContextSource; + + /// No description provided for @settingsJobWindowMode. + /// + /// In zh, this message translates to: + /// **'窗口模式'** + String get settingsJobWindowMode; + + /// No description provided for @settingsJobWindowCount. + /// + /// In zh, this message translates to: + /// **'窗口数量'** + String get settingsJobWindowCount; + + /// No description provided for @settingsJobWindowCountValue. + /// + /// In zh, this message translates to: + /// **'{count}'** + String settingsJobWindowCountValue(int count); + + /// No description provided for @settingsJobDeleteTitle. + /// + /// In zh, this message translates to: + /// **'删除周期计划'** + String get settingsJobDeleteTitle; + + /// No description provided for @settingsJobDeleteMessage. + /// + /// In zh, this message translates to: + /// **'删除后将无法恢复,是否继续?'** + String get settingsJobDeleteMessage; + + /// No description provided for @settingsJobDeleteConfirm. + /// + /// In zh, this message translates to: + /// **'确认删除'** + String get settingsJobDeleteConfirm; + + /// No description provided for @settingsJobDeleteSuccess. + /// + /// In zh, this message translates to: + /// **'删除成功'** + String get settingsJobDeleteSuccess; + + /// No description provided for @settingsJobBasicInfo. + /// + /// In zh, this message translates to: + /// **'基本信息'** + String get settingsJobBasicInfo; + + /// No description provided for @settingsJobName. + /// + /// In zh, this message translates to: + /// **'任务名称'** + String get settingsJobName; + + /// No description provided for @settingsJobNameHint. + /// + /// In zh, this message translates to: + /// **'请输入任务名称'** + String get settingsJobNameHint; + + /// No description provided for @settingsJobTemplateHint. + /// + /// In zh, this message translates to: + /// **'例如:请总结今天的记忆内容'** + String get settingsJobTemplateHint; + + /// No description provided for @settingsJobExecutionRules. + /// + /// In zh, this message translates to: + /// **'执行规则'** + String get settingsJobExecutionRules; + + /// No description provided for @settingsJobToolSelection. + /// + /// In zh, this message translates to: + /// **'工具选择'** + String get settingsJobToolSelection; + + /// No description provided for @settingsJobCounterValue. + /// + /// In zh, this message translates to: + /// **'{label}:{value}'** + String settingsJobCounterValue(Object label, int value); + + /// No description provided for @settingsJobWeekdayMon. + /// + /// In zh, this message translates to: + /// **'周一'** + String get settingsJobWeekdayMon; + + /// No description provided for @settingsJobWeekdayTue. + /// + /// In zh, this message translates to: + /// **'周二'** + String get settingsJobWeekdayTue; + + /// No description provided for @settingsJobWeekdayWed. + /// + /// In zh, this message translates to: + /// **'周三'** + String get settingsJobWeekdayWed; + + /// No description provided for @settingsJobWeekdayThu. + /// + /// In zh, this message translates to: + /// **'周四'** + String get settingsJobWeekdayThu; + + /// No description provided for @settingsJobWeekdayFri. + /// + /// In zh, this message translates to: + /// **'周五'** + String get settingsJobWeekdayFri; + + /// No description provided for @settingsJobWeekdaySat. + /// + /// In zh, this message translates to: + /// **'周六'** + String get settingsJobWeekdaySat; + + /// No description provided for @settingsJobWeekdaySun. + /// + /// In zh, this message translates to: + /// **'周日'** + String get settingsJobWeekdaySun; + + /// No description provided for @settingsJobRunDays. + /// + /// In zh, this message translates to: + /// **'执行日'** + String get settingsJobRunDays; + + /// No description provided for @settingsJobNoToolsEnabled. + /// + /// In zh, this message translates to: + /// **'未启用工具'** + String get settingsJobNoToolsEnabled; + + /// No description provided for @settingsJobPickCycle. + /// + /// In zh, this message translates to: + /// **'选择周期'** + String get settingsJobPickCycle; + + /// No description provided for @settingsJobScheduleDaily. + /// + /// In zh, this message translates to: + /// **'每日'** + String get settingsJobScheduleDaily; + + /// No description provided for @settingsJobScheduleWeekly. + /// + /// In zh, this message translates to: + /// **'每周'** + String get settingsJobScheduleWeekly; + + /// No description provided for @settingsJobPickTimezone. + /// + /// In zh, this message translates to: + /// **'选择时区'** + String get settingsJobPickTimezone; + + /// No description provided for @settingsJobPickContextSource. + /// + /// In zh, this message translates to: + /// **'选择上下文来源'** + String get settingsJobPickContextSource; + + /// No description provided for @settingsJobContextSourceLatestChat. + /// + /// In zh, this message translates to: + /// **'最近聊天'** + String get settingsJobContextSourceLatestChat; + + /// No description provided for @settingsJobPickWindowMode. + /// + /// In zh, this message translates to: + /// **'选择窗口模式'** + String get settingsJobPickWindowMode; + + /// No description provided for @settingsJobWindowModeByDay. + /// + /// In zh, this message translates to: + /// **'按天数'** + String get settingsJobWindowModeByDay; + + /// No description provided for @settingsJobWindowModeByNumber. + /// + /// In zh, this message translates to: + /// **'按消息数'** + String get settingsJobWindowModeByNumber; + + /// No description provided for @settingsJobFillRequired. + /// + /// In zh, this message translates to: + /// **'请填写完整信息'** + String get settingsJobFillRequired; + + /// No description provided for @settingsJobCreateSuccess. + /// + /// In zh, this message translates to: + /// **'创建成功'** + String get settingsJobCreateSuccess; + + /// No description provided for @settingsMemorySaveSuccess. + /// + /// In zh, this message translates to: + /// **'保存成功'** + String get settingsMemorySaveSuccess; + + /// No description provided for @settingsMemorySaveFailed. + /// + /// In zh, this message translates to: + /// **'保存失败'** + String get settingsMemorySaveFailed; + + /// No description provided for @settingsMemoryInputHint. + /// + /// In zh, this message translates to: + /// **'输入{label}'** + String settingsMemoryInputHint(Object label); + + /// No description provided for @settingsMemoryInputContent. + /// + /// In zh, this message translates to: + /// **'输入内容'** + String get settingsMemoryInputContent; + + /// No description provided for @settingsUserMemoryEditTitle. + /// + /// In zh, this message translates to: + /// **'编辑个人偏好'** + String get settingsUserMemoryEditTitle; + + /// No description provided for @settingsUserMemoryEmptyProfile. + /// + /// In zh, this message translates to: + /// **'暂无个人偏好信息'** + String get settingsUserMemoryEmptyProfile; + + /// No description provided for @settingsUserMemorySectionBasic. + /// + /// In zh, this message translates to: + /// **'基本信息'** + String get settingsUserMemorySectionBasic; + + /// No description provided for @settingsUserMemorySectionPreferences. + /// + /// In zh, this message translates to: + /// **'偏好设置'** + String get settingsUserMemorySectionPreferences; + + /// No description provided for @settingsUserMemorySectionSchedule. + /// + /// In zh, this message translates to: + /// **'日程偏好'** + String get settingsUserMemorySectionSchedule; + + /// No description provided for @settingsUserMemorySectionContacts. + /// + /// In zh, this message translates to: + /// **'联系人'** + String get settingsUserMemorySectionContacts; + + /// No description provided for @settingsUserMemorySectionPlaces. + /// + /// In zh, this message translates to: + /// **'地点'** + String get settingsUserMemorySectionPlaces; + + /// No description provided for @settingsUserMemorySectionInterests. + /// + /// In zh, this message translates to: + /// **'兴趣'** + String get settingsUserMemorySectionInterests; + + /// No description provided for @settingsUserMemorySectionAvoidTopics. + /// + /// In zh, this message translates to: + /// **'回避话题'** + String get settingsUserMemorySectionAvoidTopics; + + /// No description provided for @settingsUserMemorySectionCustomRules. + /// + /// In zh, this message translates to: + /// **'自定义规则'** + String get settingsUserMemorySectionCustomRules; + + /// No description provided for @settingsUserMemorySectionRoutines. + /// + /// In zh, this message translates to: + /// **'周期习惯'** + String get settingsUserMemorySectionRoutines; + + /// No description provided for @settingsUserMemoryFieldOccupation. + /// + /// In zh, this message translates to: + /// **'职业'** + String get settingsUserMemoryFieldOccupation; + + /// No description provided for @settingsUserMemoryFieldTimezone. + /// + /// In zh, this message translates to: + /// **'时区'** + String get settingsUserMemoryFieldTimezone; + + /// No description provided for @settingsUserMemoryFieldPrimaryLanguage. + /// + /// In zh, this message translates to: + /// **'主要语言'** + String get settingsUserMemoryFieldPrimaryLanguage; + + /// No description provided for @settingsUserMemoryFieldCommunicationStyle. + /// + /// In zh, this message translates to: + /// **'沟通风格'** + String get settingsUserMemoryFieldCommunicationStyle; + + /// No description provided for @settingsUserMemoryFieldLocationPreference. + /// + /// In zh, this message translates to: + /// **'位置偏好'** + String get settingsUserMemoryFieldLocationPreference; + + /// No description provided for @settingsUserMemoryFieldWorkLifestyle. + /// + /// In zh, this message translates to: + /// **'工作生活方式'** + String get settingsUserMemoryFieldWorkLifestyle; + + /// No description provided for @settingsUserMemoryFieldLanguagePreference. + /// + /// In zh, this message translates to: + /// **'语言偏好'** + String get settingsUserMemoryFieldLanguagePreference; + + /// No description provided for @settingsUserMemoryFieldNotificationPreference. + /// + /// In zh, this message translates to: + /// **'通知偏好'** + String get settingsUserMemoryFieldNotificationPreference; + + /// No description provided for @settingsUserMemoryFieldMeetingBuffer. + /// + /// In zh, this message translates to: + /// **'会议缓冲时间'** + String get settingsUserMemoryFieldMeetingBuffer; + + /// No description provided for @settingsUserMemoryFieldMaxMeetingsPerDay. + /// + /// In zh, this message translates to: + /// **'每日最多会议'** + String get settingsUserMemoryFieldMaxMeetingsPerDay; + + /// No description provided for @settingsUserMemoryFieldPreferredMeetingDuration. + /// + /// In zh, this message translates to: + /// **'偏好会议时长'** + String get settingsUserMemoryFieldPreferredMeetingDuration; + + /// No description provided for @settingsUserMemoryFieldNotes. + /// + /// In zh, this message translates to: + /// **'备注'** + String get settingsUserMemoryFieldNotes; + + /// No description provided for @settingsUserMemoryFieldName. + /// + /// In zh, this message translates to: + /// **'名称'** + String get settingsUserMemoryFieldName; + + /// No description provided for @settingsUserMemoryFieldRelationship. + /// + /// In zh, this message translates to: + /// **'关系'** + String get settingsUserMemoryFieldRelationship; + + /// No description provided for @settingsUserMemoryFieldRole. + /// + /// In zh, this message translates to: + /// **'角色'** + String get settingsUserMemoryFieldRole; + + /// No description provided for @settingsUserMemoryFieldContact. + /// + /// In zh, this message translates to: + /// **'联系方式'** + String get settingsUserMemoryFieldContact; + + /// No description provided for @settingsUserMemoryFieldCategory. + /// + /// In zh, this message translates to: + /// **'类别'** + String get settingsUserMemoryFieldCategory; + + /// No description provided for @settingsUserMemoryFieldPreference. + /// + /// In zh, this message translates to: + /// **'偏好'** + String get settingsUserMemoryFieldPreference; + + /// No description provided for @settingsUserMemoryFieldAddress. + /// + /// In zh, this message translates to: + /// **'地址'** + String get settingsUserMemoryFieldAddress; + + /// No description provided for @settingsUserMemoryFieldDescription. + /// + /// In zh, this message translates to: + /// **'描述'** + String get settingsUserMemoryFieldDescription; + + /// No description provided for @settingsUserMemoryFieldCadence. + /// + /// In zh, this message translates to: + /// **'周期'** + String get settingsUserMemoryFieldCadence; + + /// No description provided for @settingsUserMemoryMinute. + /// + /// In zh, this message translates to: + /// **'分钟'** + String get settingsUserMemoryMinute; + + /// No description provided for @settingsUserMemoryMinutesValue. + /// + /// In zh, this message translates to: + /// **'{minutes} 分钟'** + String settingsUserMemoryMinutesValue(int minutes); + + /// No description provided for @settingsUserMemoryEmptyContacts. + /// + /// In zh, this message translates to: + /// **'暂无联系人'** + String get settingsUserMemoryEmptyContacts; + + /// No description provided for @settingsUserMemoryEmptyPlaces. + /// + /// In zh, this message translates to: + /// **'暂无地点'** + String get settingsUserMemoryEmptyPlaces; + + /// No description provided for @settingsUserMemoryEmptyRoutines. + /// + /// In zh, this message translates to: + /// **'暂无周期习惯'** + String get settingsUserMemoryEmptyRoutines; + + /// No description provided for @settingsUserMemoryAddContact. + /// + /// In zh, this message translates to: + /// **'添加联系人'** + String get settingsUserMemoryAddContact; + + /// No description provided for @settingsUserMemoryNewContact. + /// + /// In zh, this message translates to: + /// **'新联系人'** + String get settingsUserMemoryNewContact; + + /// No description provided for @settingsUserMemoryAddPlace. + /// + /// In zh, this message translates to: + /// **'添加地点'** + String get settingsUserMemoryAddPlace; + + /// No description provided for @settingsUserMemoryNewPlace. + /// + /// In zh, this message translates to: + /// **'新地点'** + String get settingsUserMemoryNewPlace; + + /// No description provided for @settingsUserMemoryAddRoutine. + /// + /// In zh, this message translates to: + /// **'添加习惯'** + String get settingsUserMemoryAddRoutine; + + /// No description provided for @settingsUserMemoryNewRoutine. + /// + /// In zh, this message translates to: + /// **'新习惯'** + String get settingsUserMemoryNewRoutine; + + /// No description provided for @settingsWorkMemoryEditTitle. + /// + /// In zh, this message translates to: + /// **'编辑工作画像'** + String get settingsWorkMemoryEditTitle; + + /// No description provided for @settingsWorkMemoryEmptyProfile. + /// + /// In zh, this message translates to: + /// **'暂无工作信息'** + String get settingsWorkMemoryEmptyProfile; + + /// No description provided for @settingsWorkMemorySectionBasic. + /// + /// In zh, this message translates to: + /// **'基本信息'** + String get settingsWorkMemorySectionBasic; + + /// No description provided for @settingsWorkMemorySectionExpertise. + /// + /// In zh, this message translates to: + /// **'专长'** + String get settingsWorkMemorySectionExpertise; + + /// No description provided for @settingsWorkMemorySectionPreferredTools. + /// + /// In zh, this message translates to: + /// **'偏好工具'** + String get settingsWorkMemorySectionPreferredTools; + + /// No description provided for @settingsWorkMemorySectionCurrentProjects. + /// + /// In zh, this message translates to: + /// **'当前项目'** + String get settingsWorkMemorySectionCurrentProjects; + + /// No description provided for @settingsWorkMemorySectionTeamMembers. + /// + /// In zh, this message translates to: + /// **'团队成员'** + String get settingsWorkMemorySectionTeamMembers; + + /// No description provided for @settingsWorkMemorySectionWorkHabits. + /// + /// In zh, this message translates to: + /// **'工作习惯'** + String get settingsWorkMemorySectionWorkHabits; + + /// No description provided for @settingsWorkMemorySectionTeamContext. + /// + /// In zh, this message translates to: + /// **'团队背景'** + String get settingsWorkMemorySectionTeamContext; + + /// No description provided for @settingsWorkMemorySectionWorkRules. + /// + /// In zh, this message translates to: + /// **'工作规则'** + String get settingsWorkMemorySectionWorkRules; + + /// No description provided for @settingsWorkMemoryFieldOccupation. + /// + /// In zh, this message translates to: + /// **'职业'** + String get settingsWorkMemoryFieldOccupation; + + /// No description provided for @settingsWorkMemoryFieldAvailableHours. + /// + /// In zh, this message translates to: + /// **'可用时段'** + String get settingsWorkMemoryFieldAvailableHours; + + /// No description provided for @settingsWorkMemoryFieldDeepWorkBlocks. + /// + /// In zh, this message translates to: + /// **'深度工作时段'** + String get settingsWorkMemoryFieldDeepWorkBlocks; + + /// No description provided for @settingsWorkMemoryFieldPreferredMeetingWindows. + /// + /// In zh, this message translates to: + /// **'偏好会议时段'** + String get settingsWorkMemoryFieldPreferredMeetingWindows; + + /// No description provided for @settingsWorkMemoryFieldNoMeetingWindows. + /// + /// In zh, this message translates to: + /// **'免打扰时段'** + String get settingsWorkMemoryFieldNoMeetingWindows; + + /// No description provided for @settingsWorkMemoryFieldPreferredMeetingDuration. + /// + /// In zh, this message translates to: + /// **'偏好会议时长'** + String get settingsWorkMemoryFieldPreferredMeetingDuration; + + /// No description provided for @settingsWorkMemoryFieldNotificationChannel. + /// + /// In zh, this message translates to: + /// **'通知渠道'** + String get settingsWorkMemoryFieldNotificationChannel; + + /// No description provided for @settingsWorkMemoryFieldNotes. + /// + /// In zh, this message translates to: + /// **'备注'** + String get settingsWorkMemoryFieldNotes; + + /// No description provided for @settingsWorkMemoryFieldTeamContext. + /// + /// In zh, this message translates to: + /// **'团队背景描述'** + String get settingsWorkMemoryFieldTeamContext; + + /// No description provided for @settingsWorkMemoryFieldProjectName. + /// + /// In zh, this message translates to: + /// **'项目名称'** + String get settingsWorkMemoryFieldProjectName; + + /// No description provided for @settingsWorkMemoryFieldStatus. + /// + /// In zh, this message translates to: + /// **'状态'** + String get settingsWorkMemoryFieldStatus; + + /// No description provided for @settingsWorkMemoryFieldPriority. + /// + /// In zh, this message translates to: + /// **'优先级'** + String get settingsWorkMemoryFieldPriority; + + /// No description provided for @settingsWorkMemoryFieldDeadline. + /// + /// In zh, this message translates to: + /// **'截止日期'** + String get settingsWorkMemoryFieldDeadline; + + /// No description provided for @settingsWorkMemoryFieldCollaborators. + /// + /// In zh, this message translates to: + /// **'协作人'** + String get settingsWorkMemoryFieldCollaborators; + + /// No description provided for @settingsWorkMemoryFieldMilestones. + /// + /// In zh, this message translates to: + /// **'关键里程碑'** + String get settingsWorkMemoryFieldMilestones; + + /// No description provided for @settingsWorkMemoryMinute. + /// + /// In zh, this message translates to: + /// **'分钟'** + String get settingsWorkMemoryMinute; + + /// No description provided for @settingsWorkMemoryMilestoneCount. + /// + /// In zh, this message translates to: + /// **'{count} 项'** + String settingsWorkMemoryMilestoneCount(int count); + + /// No description provided for @settingsWorkMemoryEmptyProjects. + /// + /// In zh, this message translates to: + /// **'暂无项目'** + String get settingsWorkMemoryEmptyProjects; + + /// No description provided for @settingsWorkMemoryEmptyTeamMembers. + /// + /// In zh, this message translates to: + /// **'暂无团队成员'** + String get settingsWorkMemoryEmptyTeamMembers; + + /// No description provided for @settingsWorkMemoryTimeWindowCount. + /// + /// In zh, this message translates to: + /// **'{count} 个时段'** + String settingsWorkMemoryTimeWindowCount(int count); + + /// No description provided for @settingsWorkMemoryAddProject. + /// + /// In zh, this message translates to: + /// **'添加项目'** + String get settingsWorkMemoryAddProject; + + /// No description provided for @settingsWorkMemoryNewProject. + /// + /// In zh, this message translates to: + /// **'新项目'** + String get settingsWorkMemoryNewProject; + + /// No description provided for @settingsWorkMemoryAddMember. + /// + /// In zh, this message translates to: + /// **'添加成员'** + String get settingsWorkMemoryAddMember; + + /// No description provided for @settingsWorkMemoryNewMember. + /// + /// In zh, this message translates to: + /// **'新成员'** + String get settingsWorkMemoryNewMember; + + /// No description provided for @calendarDetailTitle. + /// + /// In zh, this message translates to: + /// **'日程详情'** + String get calendarDetailTitle; + + /// No description provided for @calendarDetailNotFoundTitle. + /// + /// In zh, this message translates to: + /// **'未找到该日程'** + String get calendarDetailNotFoundTitle; + + /// No description provided for @calendarDetailNotFoundDesc. + /// + /// In zh, this message translates to: + /// **'可能已被删除,或你没有访问权限。'** + String get calendarDetailNotFoundDesc; + + /// No description provided for @calendarDetailTimeArrangement. + /// + /// In zh, this message translates to: + /// **'时间安排'** + String get calendarDetailTimeArrangement; + + /// No description provided for @calendarDetailDateLabel. + /// + /// In zh, this message translates to: + /// **'{year}年{month}月{day}日 {weekday}'** + String calendarDetailDateLabel(int year, int month, int day, Object weekday); + + /// No description provided for @calendarDetailBasicInfo. + /// + /// In zh, this message translates to: + /// **'基础信息'** + String get calendarDetailBasicInfo; + + /// No description provided for @calendarDetailDate. + /// + /// In zh, this message translates to: + /// **'日期'** + String get calendarDetailDate; + + /// No description provided for @calendarDetailReminder. + /// + /// In zh, this message translates to: + /// **'提醒'** + String get calendarDetailReminder; + + /// No description provided for @calendarDetailColor. + /// + /// In zh, this message translates to: + /// **'颜色'** + String get calendarDetailColor; + + /// No description provided for @calendarDetailExtraInfo. + /// + /// In zh, this message translates to: + /// **'补充信息'** + String get calendarDetailExtraInfo; + + /// No description provided for @calendarDetailLocation. + /// + /// In zh, this message translates to: + /// **'地点'** + String get calendarDetailLocation; + + /// No description provided for @calendarDetailDescription. + /// + /// In zh, this message translates to: + /// **'描述'** + String get calendarDetailDescription; + + /// No description provided for @calendarDetailNotes. + /// + /// In zh, this message translates to: + /// **'备注'** + String get calendarDetailNotes; + + /// No description provided for @calendarDetailReminderNone. + /// + /// In zh, this message translates to: + /// **'无'** + String get calendarDetailReminderNone; + + /// No description provided for @calendarDetailReminderOnTime. + /// + /// In zh, this message translates to: + /// **'准时提醒'** + String get calendarDetailReminderOnTime; + + /// No description provided for @calendarDetailReminderBeforeMinutes. + /// + /// In zh, this message translates to: + /// **'开始前{minutes}分钟'** + String calendarDetailReminderBeforeMinutes(int minutes); + + /// No description provided for @calendarWeekdayMon. + /// + /// In zh, this message translates to: + /// **'周一'** + String get calendarWeekdayMon; + + /// No description provided for @calendarWeekdayTue. + /// + /// In zh, this message translates to: + /// **'周二'** + String get calendarWeekdayTue; + + /// No description provided for @calendarWeekdayWed. + /// + /// In zh, this message translates to: + /// **'周三'** + String get calendarWeekdayWed; + + /// No description provided for @calendarWeekdayThu. + /// + /// In zh, this message translates to: + /// **'周四'** + String get calendarWeekdayThu; + + /// No description provided for @calendarWeekdayFri. + /// + /// In zh, this message translates to: + /// **'周五'** + String get calendarWeekdayFri; + + /// No description provided for @calendarWeekdaySat. + /// + /// In zh, this message translates to: + /// **'周六'** + String get calendarWeekdaySat; + + /// No description provided for @calendarWeekdaySun. + /// + /// In zh, this message translates to: + /// **'周日'** + String get calendarWeekdaySun; + + /// No description provided for @calendarDetailDeleteTitle. + /// + /// In zh, this message translates to: + /// **'删除日程'** + String get calendarDetailDeleteTitle; + + /// No description provided for @calendarDetailDeleteMessage. + /// + /// In zh, this message translates to: + /// **'确定要删除这个日程吗?'** + String get calendarDetailDeleteMessage; + + /// No description provided for @calendarDetailDeleteConfirm. + /// + /// In zh, this message translates to: + /// **'确认删除'** + String get calendarDetailDeleteConfirm; + + /// No description provided for @calendarDetailArchiveTitle. + /// + /// In zh, this message translates to: + /// **'归档日程'** + String get calendarDetailArchiveTitle; + + /// No description provided for @calendarDetailArchiveMessage. + /// + /// In zh, this message translates to: + /// **'归档后此日程将标记为过期,确定要归档吗?'** + String get calendarDetailArchiveMessage; + + /// No description provided for @calendarDetailArchiveConfirm. + /// + /// In zh, this message translates to: + /// **'确认归档'** + String get calendarDetailArchiveConfirm; + + /// No description provided for @calendarDetailArchiveFailed. + /// + /// In zh, this message translates to: + /// **'归档失败'** + String get calendarDetailArchiveFailed; + + /// No description provided for @calendarDetailDateTimeShort. + /// + /// In zh, this message translates to: + /// **'{month}月{day}日 {weekday} {time}'** + String calendarDetailDateTimeShort( + int month, + int day, + Object weekday, + Object time, + ); + + /// No description provided for @calendarDetailRangeWithStartEnd. + /// + /// In zh, this message translates to: + /// **'开始: {start}\n结束: {end}'** + String calendarDetailRangeWithStartEnd(Object start, Object end); + + /// No description provided for @calendarDetailStatusExpired. + /// + /// In zh, this message translates to: + /// **'已过期'** + String get calendarDetailStatusExpired; + + /// No description provided for @calendarCreateEditTitle. + /// + /// In zh, this message translates to: + /// **'编辑日程'** + String get calendarCreateEditTitle; + + /// No description provided for @calendarCreateNewTitle. + /// + /// In zh, this message translates to: + /// **'新建日程'** + String get calendarCreateNewTitle; + + /// No description provided for @calendarCreateTabBasic. + /// + /// In zh, this message translates to: + /// **'基础'** + String get calendarCreateTabBasic; + + /// No description provided for @calendarCreateTabAdvanced. + /// + /// In zh, this message translates to: + /// **'进阶'** + String get calendarCreateTabAdvanced; + + /// No description provided for @calendarCreateFieldTitle. + /// + /// In zh, this message translates to: + /// **'标题'** + String get calendarCreateFieldTitle; + + /// No description provided for @calendarCreateFieldTitleHint. + /// + /// In zh, this message translates to: + /// **'请输入日程标题'** + String get calendarCreateFieldTitleHint; + + /// No description provided for @calendarCreateFieldStart. + /// + /// In zh, this message translates to: + /// **'开始'** + String get calendarCreateFieldStart; + + /// No description provided for @calendarCreateFieldEnd. + /// + /// In zh, this message translates to: + /// **'结束'** + String get calendarCreateFieldEnd; + + /// No description provided for @calendarCreateFieldDescription. + /// + /// In zh, this message translates to: + /// **'描述'** + String get calendarCreateFieldDescription; + + /// No description provided for @calendarCreateFieldDescriptionHint. + /// + /// In zh, this message translates to: + /// **'请输入描述'** + String get calendarCreateFieldDescriptionHint; + + /// No description provided for @calendarCreateFieldLocation. + /// + /// In zh, this message translates to: + /// **'地点'** + String get calendarCreateFieldLocation; + + /// No description provided for @calendarCreateFieldLocationHint. + /// + /// In zh, this message translates to: + /// **'请输入地点'** + String get calendarCreateFieldLocationHint; + + /// No description provided for @calendarCreateFieldNotesHint. + /// + /// In zh, this message translates to: + /// **'请输入备注'** + String get calendarCreateFieldNotesHint; + + /// No description provided for @calendarCreateOptionalField. + /// + /// In zh, this message translates to: + /// **'{label}(可选)'** + String calendarCreateOptionalField(Object label); + + /// No description provided for @calendarCreateDateTimeLabel. + /// + /// In zh, this message translates to: + /// **'{year}年{month}月{day}日 {hour}:{minute}'** + String calendarCreateDateTimeLabel( + int year, + int month, + int day, + Object hour, + Object minute, + ); + + /// No description provided for @calendarCreateReminderNone. + /// + /// In zh, this message translates to: + /// **'无提醒'** + String get calendarCreateReminderNone; + + /// No description provided for @calendarCreateReminderTime. + /// + /// In zh, this message translates to: + /// **'提醒时间'** + String get calendarCreateReminderTime; + + /// No description provided for @calendarCreatePickReminderTime. + /// + /// In zh, this message translates to: + /// **'选择提醒时间'** + String get calendarCreatePickReminderTime; + + /// No description provided for @calendarCreateReminderPermissionFailed. + /// + /// In zh, this message translates to: + /// **'提醒创建失败,请检查通知权限'** + String get calendarCreateReminderPermissionFailed; + + /// No description provided for @settingsEditProfileLoadFailed. + /// + /// In zh, this message translates to: + /// **'加载用户信息失败'** + String get settingsEditProfileLoadFailed; + + /// No description provided for @settingsEditProfileAvatarUploadSuccess. + /// + /// In zh, this message translates to: + /// **'头像上传成功'** + String get settingsEditProfileAvatarUploadSuccess; + + /// No description provided for @settingsEditProfileAvatarUploadFailed. + /// + /// In zh, this message translates to: + /// **'头像上传失败,请重试'** + String get settingsEditProfileAvatarUploadFailed; + + /// No description provided for @settingsEditProfileUsernameRequired. + /// + /// In zh, this message translates to: + /// **'用户名不能为空'** + String get settingsEditProfileUsernameRequired; + + /// No description provided for @settingsEditProfileUsernameLengthInvalid. + /// + /// In zh, this message translates to: + /// **'用户名需要3-30个字符'** + String get settingsEditProfileUsernameLengthInvalid; + + /// No description provided for @settingsEditProfileSaveSuccess. + /// + /// In zh, this message translates to: + /// **'保存成功'** + String get settingsEditProfileSaveSuccess; + + /// No description provided for @settingsEditProfileSaveFailed. + /// + /// In zh, this message translates to: + /// **'保存失败,请重试'** + String get settingsEditProfileSaveFailed; + + /// No description provided for @settingsEditProfileTitle. + /// + /// In zh, this message translates to: + /// **'编辑资料'** + String get settingsEditProfileTitle; + + /// No description provided for @settingsEditProfileSaveChanges. + /// + /// In zh, this message translates to: + /// **'保存修改'** + String get settingsEditProfileSaveChanges; + + /// No description provided for @settingsEditProfileBasicInfo. + /// + /// In zh, this message translates to: + /// **'基础信息'** + String get settingsEditProfileBasicInfo; + + /// No description provided for @settingsEditProfileUsername. + /// + /// In zh, this message translates to: + /// **'用户名'** + String get settingsEditProfileUsername; + + /// No description provided for @settingsEditProfileUsernameHint. + /// + /// In zh, this message translates to: + /// **'请输入用户名'** + String get settingsEditProfileUsernameHint; + + /// No description provided for @settingsEditProfileBio. + /// + /// In zh, this message translates to: + /// **'个人简介'** + String get settingsEditProfileBio; + + /// No description provided for @settingsEditProfileBioContent. + /// + /// In zh, this message translates to: + /// **'简介内容'** + String get settingsEditProfileBioContent; + + /// No description provided for @settingsEditProfileBioHint. + /// + /// In zh, this message translates to: + /// **'介绍一下自己吧'** + String get settingsEditProfileBioHint; + + /// No description provided for @calendarSharePhoneRequired. + /// + /// In zh, this message translates to: + /// **'请输入手机号'** + String get calendarSharePhoneRequired; + + /// No description provided for @calendarShareInviteSent. + /// + /// In zh, this message translates to: + /// **'邀请已发送'** + String get calendarShareInviteSent; + + /// No description provided for @calendarShareInviteFailed. + /// + /// In zh, this message translates to: + /// **'发送邀请失败'** + String get calendarShareInviteFailed; + + /// No description provided for @calendarShareTitle. + /// + /// In zh, this message translates to: + /// **'分享日历'** + String get calendarShareTitle; + + /// No description provided for @calendarSharePhoneLabel. + /// + /// In zh, this message translates to: + /// **'手机号'** + String get calendarSharePhoneLabel; + + /// No description provided for @calendarSharePhoneHint. + /// + /// In zh, this message translates to: + /// **'输入对方的 +86 手机号'** + String get calendarSharePhoneHint; + + /// No description provided for @calendarSharePermissionTitle. + /// + /// In zh, this message translates to: + /// **'权限设置'** + String get calendarSharePermissionTitle; + + /// No description provided for @calendarSharePermissionView. + /// + /// In zh, this message translates to: + /// **'查看'** + String get calendarSharePermissionView; + + /// No description provided for @calendarSharePermissionViewDesc. + /// + /// In zh, this message translates to: + /// **'可以查看此日历事件(必选)'** + String get calendarSharePermissionViewDesc; + + /// No description provided for @calendarSharePermissionEdit. + /// + /// In zh, this message translates to: + /// **'编辑'** + String get calendarSharePermissionEdit; + + /// No description provided for @calendarSharePermissionEditDesc. + /// + /// In zh, this message translates to: + /// **'可以编辑此日历事件'** + String get calendarSharePermissionEditDesc; + + /// No description provided for @calendarSharePermissionInvite. + /// + /// In zh, this message translates to: + /// **'邀请'** + String get calendarSharePermissionInvite; + + /// No description provided for @calendarSharePermissionInviteDesc. + /// + /// In zh, this message translates to: + /// **'可以邀请其他人'** + String get calendarSharePermissionInviteDesc; + + /// No description provided for @calendarShareSendInvite. + /// + /// In zh, this message translates to: + /// **'发送邀请'** + String get calendarShareSendInvite; + + /// No description provided for @calendarMonthHeader. + /// + /// In zh, this message translates to: + /// **'{month}月'** + String calendarMonthHeader(int month); + + /// No description provided for @calendarMonthToday. + /// + /// In zh, this message translates to: + /// **'今天'** + String get calendarMonthToday; + + /// No description provided for @calendarMonthWeekdaySunShort. + /// + /// In zh, this message translates to: + /// **'日'** + String get calendarMonthWeekdaySunShort; + + /// No description provided for @calendarMonthWeekdayMonShort. + /// + /// In zh, this message translates to: + /// **'一'** + String get calendarMonthWeekdayMonShort; + + /// No description provided for @calendarMonthWeekdayTueShort. + /// + /// In zh, this message translates to: + /// **'二'** + String get calendarMonthWeekdayTueShort; + + /// No description provided for @calendarMonthWeekdayWedShort. + /// + /// In zh, this message translates to: + /// **'三'** + String get calendarMonthWeekdayWedShort; + + /// No description provided for @calendarMonthWeekdayThuShort. + /// + /// In zh, this message translates to: + /// **'四'** + String get calendarMonthWeekdayThuShort; + + /// No description provided for @calendarMonthWeekdayFriShort. + /// + /// In zh, this message translates to: + /// **'五'** + String get calendarMonthWeekdayFriShort; + + /// No description provided for @calendarMonthWeekdaySatShort. + /// + /// In zh, this message translates to: + /// **'六'** + String get calendarMonthWeekdaySatShort; + + /// No description provided for @calendarMonthYearLabel. + /// + /// In zh, this message translates to: + /// **'{year}年'** + String calendarMonthYearLabel(int year); + + /// No description provided for @calendarDateTimePickerDateLabel. + /// + /// In zh, this message translates to: + /// **'日期'** + String get calendarDateTimePickerDateLabel; + + /// No description provided for @calendarDateTimePickerYearUnit. + /// + /// In zh, this message translates to: + /// **'年'** + String get calendarDateTimePickerYearUnit; + + /// No description provided for @calendarDateTimePickerMonthUnit. + /// + /// In zh, this message translates to: + /// **'月'** + String get calendarDateTimePickerMonthUnit; + + /// No description provided for @calendarDateTimePickerDayUnit. + /// + /// In zh, this message translates to: + /// **'日'** + String get calendarDateTimePickerDayUnit; + + /// No description provided for @calendarDateTimePickerTimeLabel. + /// + /// In zh, this message translates to: + /// **'时间'** + String get calendarDateTimePickerTimeLabel; + + /// No description provided for @calendarDateTimePickerTitle. + /// + /// In zh, this message translates to: + /// **'选择时间'** + String get calendarDateTimePickerTitle; + + /// No description provided for @messagesCalendarCardInviteTitle. + /// + /// In zh, this message translates to: + /// **'日历邀请'** + String get messagesCalendarCardInviteTitle; + + /// No description provided for @messagesCalendarCardInviteWithTitle. + /// + /// In zh, this message translates to: + /// **'邀请你访问 \"{title}\"'** + String messagesCalendarCardInviteWithTitle(Object title); + + /// No description provided for @messagesCalendarCardInviteWithoutTitle. + /// + /// In zh, this message translates to: + /// **'邀请你访问日历'** + String get messagesCalendarCardInviteWithoutTitle; + + /// No description provided for @messagesCalendarCardUpdatedWithTitle. + /// + /// In zh, this message translates to: + /// **'{title} 已更新'** + String messagesCalendarCardUpdatedWithTitle(Object title); + + /// No description provided for @messagesCalendarCardUpdatedWithoutTitle. + /// + /// In zh, this message translates to: + /// **'日历事件已更新'** + String get messagesCalendarCardUpdatedWithoutTitle; + + /// No description provided for @messagesCalendarCardTimeMinutesAgo. + /// + /// In zh, this message translates to: + /// **'{minutes}分钟前'** + String messagesCalendarCardTimeMinutesAgo(int minutes); + + /// No description provided for @messagesCalendarCardTimeHoursAgo. + /// + /// In zh, this message translates to: + /// **'{hours}小时前'** + String messagesCalendarCardTimeHoursAgo(int hours); + + /// No description provided for @messagesCalendarCardTimeDaysAgo. + /// + /// In zh, this message translates to: + /// **'{days}天前'** + String messagesCalendarCardTimeDaysAgo(int days); + + /// No description provided for @messagesCalendarCardTimeDate. + /// + /// In zh, this message translates to: + /// **'{month}月{day}日'** + String messagesCalendarCardTimeDate(int month, int day); + + /// No description provided for @messagesCalendarCardDeletedWithTitle. + /// + /// In zh, this message translates to: + /// **'{title} 已删除'** + String messagesCalendarCardDeletedWithTitle(Object title); + + /// No description provided for @messagesCalendarCardDeletedWithoutTitle. + /// + /// In zh, this message translates to: + /// **'日历事件已删除'** + String get messagesCalendarCardDeletedWithoutTitle; +} + +class _AppLocalizationsDelegate + extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => + ['en', 'zh'].contains(locale.languageCode); + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} + +AppLocalizations lookupAppLocalizations(Locale locale) { + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'en': + return AppLocalizationsEn(); + case 'zh': + return AppLocalizationsZh(); + } + + throw FlutterError( + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.', + ); +} diff --git a/apps/lib/l10n/app_localizations_en.dart b/apps/lib/l10n/app_localizations_en.dart new file mode 100644 index 0000000..8db3820 --- /dev/null +++ b/apps/lib/l10n/app_localizations_en.dart @@ -0,0 +1,1840 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for English (`en`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsEn([String locale = 'en']) : super(locale); + + @override + String get appTitle => 'Linksy'; + + @override + String get commonConfirm => 'Confirm'; + + @override + String get commonCancel => 'Cancel'; + + @override + String get commonSave => 'Save'; + + @override + String get commonDone => 'Done'; + + @override + String get commonRetry => 'Retry'; + + @override + String get commonRefreshing => 'Refreshing'; + + @override + String get commonLoading => 'Loading...'; + + @override + String get commonEdit => 'Edit'; + + @override + String get commonDelete => 'Delete'; + + @override + String get commonShare => 'Share'; + + @override + String get commonArchive => 'Archive'; + + @override + String get commonCopySuccess => 'Copied'; + + @override + String commonLoadFailed(Object error) { + return 'Load failed: $error'; + } + + @override + String get commonUnknown => 'Unknown'; + + @override + String get toastLabelSuccess => 'Success'; + + @override + String get toastLabelWarning => 'Warning'; + + @override + String get toastLabelError => 'Error'; + + @override + String get toastLabelInfo => 'Info'; + + @override + String get errorGenericSafe => 'Request failed, please try again later'; + + @override + String get errorForbidden => + 'You do not have permission to perform this action'; + + @override + String get errorNotFound => 'Requested resource was not found'; + + @override + String get errorTooManyRequests => + 'Too many requests, please try again later'; + + @override + String get errorServer => 'Server error, please try again later'; + + @override + String get errorAgentSseConnectionLimit => + 'Too many connections, please try again later'; + + @override + String get errorAgentAttachmentEmpty => 'Attachment is empty'; + + @override + String get errorAgentAttachmentTooLarge => 'Attachment is too large'; + + @override + String get errorAgentAudioEmpty => 'Audio content is empty'; + + @override + String get errorAgentAudioTooLarge => 'Audio file is too large'; + + @override + String get errorAgentAudioUnsupportedFormat => 'Unsupported audio format'; + + @override + String get errorAgentAsrUnavailable => + 'Speech service is temporarily unavailable'; + + @override + String get errorAgentInvalidLastEventId => + 'Invalid event cursor, please refresh and retry'; + + @override + String get errorAgentInvalidBinaryUrl => + 'Invalid image link, please upload again'; + + @override + String get errorRequestFailed => 'Request failed'; + + @override + String get errorNetwork => 'Network error'; + + @override + String get errorReLogin => 'Please sign in again'; + + @override + String get errorNetworkTimeout => + 'Network timeout. Ensure your device and server are on the same network and retry.'; + + @override + String get errorNetworkUnavailable => + 'Cannot connect to server. Please enable network access for this app in iPhone settings.'; + + @override + String get homeViewHistory => 'View History'; + + @override + String get homeNoEarlierHistory => 'No earlier history'; + + @override + String get homeSheetTakePhoto => 'Take Photo'; + + @override + String get homeSheetPhotoLibrary => 'Photo Library'; + + @override + String homeDateLabelWithYear(int year, int month, int day, Object weekday) { + return '$year-$month-$day $weekday'; + } + + @override + String homeDateLabelNoYear(int month, int day, Object weekday) { + return '$month-$day $weekday'; + } + + @override + String get homeRecordingReleaseCancel => 'Release to cancel'; + + @override + String get homeRecordingReleaseSend => 'Release to send'; + + @override + String get homeRecordingHintReleaseCancel => 'Release to cancel'; + + @override + String get homeRecordingHintReleaseSend => + 'Release to send, slide up to cancel'; + + @override + String get homeHoldToSpeakText => 'Hold to speak'; + + @override + String get homeInputHint => 'Type a message...'; + + @override + String get homeTranscribing => 'Transcribing voice...'; + + @override + String get homeRecordingCanceled => 'Canceled'; + + @override + String get homeToolPreparing => 'Preparing tool'; + + @override + String get homeToolExecuting => 'Running task'; + + @override + String get homeToolExecutionFailed => 'Execution failed'; + + @override + String get homeToolCompleted => 'Completed'; + + @override + String get homeRecorderPluginUnavailable => + 'Recorder plugin is unavailable. Fully restart the app and retry.'; + + @override + String get homeRecorderPermissionDenied => + 'Microphone permission is not granted'; + + @override + String get homeStopRequested => 'Stop requested'; + + @override + String get homeNoValidSpeech => + 'No valid speech detected. Please move closer to the microphone and retry.'; + + @override + String get agentStageRouting => 'Analyzing intent'; + + @override + String get agentStageExecution => 'Executing task'; + + @override + String get agentStageMemory => 'Loading memory'; + + @override + String get agentStageProcessing => 'Processing task'; + + @override + String get agUiEventRunStarted => 'Run started'; + + @override + String get agUiEventRunFinished => 'Run finished'; + + @override + String get agUiEventRunError => 'Run failed'; + + @override + String get agUiEventStepStarted => 'Step started'; + + @override + String get agUiEventStepFinished => 'Step finished'; + + @override + String get agUiEventTextMessageEnd => 'Text output completed'; + + @override + String get agUiEventToolCallStart => 'Tool call started'; + + @override + String get agUiEventToolCallArgs => 'Tool arguments updated'; + + @override + String get agUiEventToolCallEnd => 'Tool call ended'; + + @override + String get agUiEventToolCallResult => 'Tool result received'; + + @override + String get agUiEventToolCallError => 'Tool call failed'; + + @override + String get agUiEventUnknown => 'Unknown event'; + + @override + String get chatRunCanceled => 'This run was canceled'; + + @override + String get chatRunFailed => 'This run failed'; + + @override + String get chatSseInterruptedRetry => + 'Connection interrupted, please try again'; + + @override + String get chatTimestampToday => 'Today'; + + @override + String get chatTimestampYesterday => 'Yesterday'; + + @override + String chatTimestampMonthDay(int month, int day) { + return '$month/$day'; + } + + @override + String homeUnreadMessages(int count) { + return '$count new messages'; + } + + @override + String get calendarToday => 'Today'; + + @override + String get calendarEventNoAccessOrMissing => + 'Event not found or no permission'; + + @override + String calendarDayWeekMonthYearLabel(int year, int month) { + return '$year-$month'; + } + + @override + String get validatorPhoneRequired => 'Please enter phone number'; + + @override + String get validatorPhoneInvalid86 => 'Please enter a valid +86 phone number'; + + @override + String get validatorPasswordRequired => 'Please enter password'; + + @override + String get validatorPasswordMin8 => 'Password must be at least 8 characters'; + + @override + String validatorRequired(Object fieldName) { + return 'Please enter $fieldName'; + } + + @override + String get validatorNicknameRequired => 'Please enter nickname'; + + @override + String get validatorNicknameMin2 => 'Nickname must be at least 2 characters'; + + @override + String get authAgreementTitle => 'Please agree to the policies'; + + @override + String get authAgreementMessage => + 'Before using our services, please read and agree to the User Agreement and Privacy Policy.\n\nWe can only provide services after your consent.'; + + @override + String get authAgreementSemantics => + 'Agree to User Agreement and Privacy Policy'; + + @override + String get authAgreementPrefix => 'I have read and agree to'; + + @override + String get authAgreementTerms => 'User Agreement'; + + @override + String get authAgreementAnd => 'and'; + + @override + String get authAgreementPrivacy => 'Privacy Policy'; + + @override + String get authPhoneHint => 'Enter phone number'; + + @override + String get authCodeHint => 'Enter verification code'; + + @override + String get authSendCode => 'Send Code'; + + @override + String get authShowPassword => 'Show password'; + + @override + String get authHidePassword => 'Hide password'; + + @override + String get authLoginFailed => 'Login failed'; + + @override + String get authCheckInput => 'Please check your input'; + + @override + String get authLoginOrRegister => 'Login / Register'; + + @override + String get authInvalidPhone => 'Please enter a valid phone number'; + + @override + String get authSendCodeFailed => 'Failed to send verification code'; + + @override + String get inputUsernameRequired => 'Please enter username'; + + @override + String get inputUsernameMin => 'Username must be at least 3 characters'; + + @override + String get inputUsernameMax => 'Username must be at most 30 characters'; + + @override + String get inputPhoneRequired => 'Please enter phone number'; + + @override + String get inputPhoneInvalid => 'Invalid phone number format'; + + @override + String get inputPasswordRequired => 'Please enter password'; + + @override + String get inputPasswordMin => 'Password must be at least 6 characters'; + + @override + String get inputCodeRequired => 'Please enter verification code'; + + @override + String get inputCodeInvalid => 'Verification code must be 6 digits'; + + @override + String get uiSchemaInvalid => 'Invalid UI schema'; + + @override + String uiSchemaUnsupportedLayout(Object type) { + return 'Unsupported layout node: $type'; + } + + @override + String uiSchemaUnknownNode(Object type) { + return 'Unknown node: $type'; + } + + @override + String get uiSchemaActionFallback => 'Action'; + + @override + String get uiSchemaActionNotImplemented => 'This action is not available yet'; + + @override + String get uiSchemaNavigationInvalidParams => 'Invalid navigation params'; + + @override + String get uiSchemaNavigationInvalidPath => 'Invalid navigation path'; + + @override + String notificationSnoozeMinutes(int minutes) { + return '$minutes min'; + } + + @override + String get notificationSnoozeLater => 'Remind later'; + + @override + String get notificationChannelName => 'Schedule alarm'; + + @override + String get notificationChannelDescription => + 'Alarm-style notifications for scheduled events'; + + @override + String get notificationStartsNow => 'Event starts now'; + + @override + String notificationStartsInMinutes(int minutes) { + return 'Event starts in $minutes minutes'; + } + + @override + String notificationLocation(Object location) { + return 'Location: $location'; + } + + @override + String notificationNotes(Object notes) { + return 'Notes: $notes'; + } + + @override + String get todoScreenTitle => 'To-Do'; + + @override + String get todoDetailTitle => 'To-Do Details'; + + @override + String get todoCreateTitle => 'Create To-Do'; + + @override + String get todoEditTitle => 'Edit To-Do'; + + @override + String get todoMoveFailed => 'Move failed'; + + @override + String get todoRefreshFailed => 'Refresh failed, please try again'; + + @override + String todoCompleteFailed(Object error) { + return 'Failed to complete: $error'; + } + + @override + String get todoNotFound => 'To-do not found'; + + @override + String get todoCalendarEventCards => 'Calendar Event Cards'; + + @override + String get todoPriorityQuadrant => 'Quadrant'; + + @override + String get todoLinkedCalendarEvents => 'Linked Calendar Events'; + + @override + String get todoStatus => 'Status'; + + @override + String get todoStatusDone => 'Done'; + + @override + String get todoStatusInProgress => 'In progress'; + + @override + String todoQuadrantOrder(int order) { + return 'Order in quadrant #$order'; + } + + @override + String todoSplitToEvents(int count) { + return 'Split into $count calendar events'; + } + + @override + String get todoNoLinkedEvents => 'No linked calendar events'; + + @override + String get todoDeleteTitle => 'Delete To-Do'; + + @override + String get todoDeleteMessage => 'Are you sure you want to delete this to-do?'; + + @override + String get todoDeleteConfirm => 'Delete'; + + @override + String todoDeleteFailed(Object error) { + return 'Delete failed: $error'; + } + + @override + String get todoQuadrantImportantUrgent => 'Important & Urgent'; + + @override + String get todoQuadrantUrgentNotImportant => 'Urgent, Not Important'; + + @override + String get todoQuadrantImportantNotUrgent => 'Important, Not Urgent'; + + @override + String get todoQuadrantNotUrgentNotImportant => 'Not Urgent, Not Important'; + + @override + String get todoNoItems => 'No to-dos'; + + @override + String todoItemCount(int count) { + return '$count items'; + } + + @override + String get todoInfoTitle => 'To-Do Info'; + + @override + String get todoInfoDescCreate => + 'After creation, you can view it in quadrants and continue adjusting priority and linked events.'; + + @override + String get todoInfoDescDone => + 'This to-do is completed. You can still adjust content and reorganize linked events.'; + + @override + String get todoInfoDescDefault => + 'Adjust title, priority, and linked events to keep tasks organized.'; + + @override + String get todoFieldTitle => 'Title'; + + @override + String get todoFieldTitleHint => 'Enter to-do title'; + + @override + String get todoFieldDescriptionOptional => 'Description (optional)'; + + @override + String get todoFieldDescriptionHint => 'Add details or notes'; + + @override + String get todoPriority => 'Priority'; + + @override + String get todoNoSelectableCalendarEvents => + 'No calendar events available to link'; + + @override + String get todoSaveInProgress => 'Saving...'; + + @override + String get todoCreateButton => 'Create To-Do'; + + @override + String get todoSaveChanges => 'Save Changes'; + + @override + String get todoEnterTitle => 'Please enter a title'; + + @override + String todoSaveFailed(Object error) { + return 'Save failed: $error'; + } + + @override + String get contactsTitle => 'Contacts'; + + @override + String get contactsSearchHint => 'Enter username or phone number'; + + @override + String get contactsSearchEmptyQuery => + 'Please enter username or phone number'; + + @override + String get contactsSearchFailed => 'Search failed, please try again'; + + @override + String get contactsSearchNoUser => 'User not found'; + + @override + String get contactsFriendRequestSent => 'Friend request sent'; + + @override + String get contactsSendFailed => 'Send failed, please try again'; + + @override + String get contactsSectionNew => 'New Contacts'; + + @override + String get contactsSectionAll => 'All Contacts'; + + @override + String get contactsStatusAlreadyFriend => 'Already friends'; + + @override + String get contactsStatusSent => 'Sent'; + + @override + String get contactsAdd => 'Add'; + + @override + String get contactsEmptyTitle => 'No contacts'; + + @override + String get contactsEmptyDesc => + 'Search by phone to add friends and start chatting'; + + @override + String get contactsPendingConfirm => 'Waiting for confirmation'; + + @override + String contactsAddSheetTitle(Object username) { + return 'Add $username'; + } + + @override + String get contactsAddSheetDesc => + 'Send a verification message so the other person can confirm your identity'; + + @override + String get contactsAddSheetMessageHint => 'Hi, I am...'; + + @override + String get contactsSend => 'Send'; + + @override + String get contactEditTitle => 'Edit Contact'; + + @override + String get contactAddTitle => 'Add Contact'; + + @override + String get contactNickname => 'Nickname'; + + @override + String get contactNicknameHint => 'Enter nickname'; + + @override + String get contactPhone => 'Phone'; + + @override + String get contactPhoneHint => '+86 Enter 11-digit phone number'; + + @override + String get contactRemark => 'Remark'; + + @override + String get contactRemarkHint => 'Enter remark'; + + @override + String get contactDelete => 'Delete Contact'; + + @override + String get contactFillRequired => 'Please fill nickname and phone'; + + @override + String get contactDeleteConfirmTitle => 'Delete Contact'; + + @override + String get contactDeleteConfirmMessage => + 'Are you sure to delete this contact?'; + + @override + String get messagesLoadFailed => 'Failed to load messages, please try again'; + + @override + String get messagesSenderLoadFailed => + 'Failed to load sender info, pull to retry'; + + @override + String get messagesFriendRequestMissing => 'Missing friend request data'; + + @override + String get messagesAcceptedFriendRequest => 'Friend request accepted'; + + @override + String get messagesRejectedFriendRequest => 'Friend request rejected'; + + @override + String get messagesActionFailed => 'Action failed, please try again'; + + @override + String get messagesTabUnread => 'Unread'; + + @override + String get messagesTabRead => 'Read'; + + @override + String get messagesEmptyUnreadTitle => 'No unread messages'; + + @override + String get messagesEmptyReadTitle => 'No read messages'; + + @override + String get messagesEmptyUnreadDesc => 'New messages will appear here'; + + @override + String get messagesEmptyReadDesc => 'Processed messages will appear here'; + + @override + String get messagesFriendRequestLoadFailed => + 'Failed to load friend request info'; + + @override + String messagesFriendRequestTitle(Object username) { + return '$username wants to add you as a friend'; + } + + @override + String get messagesCalendarInvite => 'Calendar invite'; + + @override + String get messagesSystemMessage => 'System message'; + + @override + String get messagesTapToView => 'Tap to view details'; + + @override + String get messagesInviteJoinCalendar => 'Invites you to join calendar'; + + @override + String get messagesInviteAccepted => 'Calendar invite accepted'; + + @override + String get messagesInviteRejected => 'Calendar invite rejected'; + + @override + String get messagesCalendarUpdated => 'Updated calendar event'; + + @override + String get messagesInviteStatusAccepted => 'Accepted'; + + @override + String get messagesInviteStatusRejected => 'Rejected'; + + @override + String get messagesInviteStatusHandled => 'Handled'; + + @override + String get messagesInviteDetailNotFound => 'Invite not found or expired'; + + @override + String get messagesInviteAcceptedToast => 'Invite accepted'; + + @override + String get messagesInviteRejectedToast => 'Invite rejected'; + + @override + String get messagesInviteOperationFailed => + 'Operation failed, please try again'; + + @override + String get messagesInviteDetailTitle => 'Calendar Invite Details'; + + @override + String messagesInviteEvent(Object title) { + return 'Event: $title'; + } + + @override + String get messagesInviteUnnamedEvent => 'Unnamed schedule'; + + @override + String messagesInviteSender(Object name) { + return 'Sender: $name'; + } + + @override + String get messagesInviteUnknownUser => 'Unknown user'; + + @override + String messagesInviteTime(Object time) { + return 'Time: $time'; + } + + @override + String messagesInviteStatus(Object status) { + return 'Status: $status'; + } + + @override + String messagesInviteId(Object id) { + return 'Invite ID: $id'; + } + + @override + String get messagesInviteTip => + 'Accept to join this calendar event. Reject to mark this invite as handled.'; + + @override + String get messagesInviteAlreadyHandled => 'This invite has been handled'; + + @override + String get messagesReject => 'Reject'; + + @override + String get messagesAccept => 'Accept'; + + @override + String get messagesStatusPending => 'Pending'; + + @override + String get settingsFeaturesTitle => 'Recurring Plans'; + + @override + String get settingsSectionDaily => 'Daily'; + + @override + String get settingsSectionWeekly => 'Weekly'; + + @override + String get settingsNoDailyPlans => 'No daily plans'; + + @override + String get settingsNoWeeklyPlans => 'No weekly plans'; + + @override + String get settingsSystemJobReadonly => + 'System preset jobs cannot be changed'; + + @override + String get settingsJobStatusEnabled => 'Enabled'; + + @override + String get settingsJobStatusDisabled => 'Disabled'; + + @override + String get settingsJobSourceSystem => 'System preset'; + + @override + String get settingsJobSourceCustom => 'Custom'; + + @override + String get settingsCreateJob => 'Create Job'; + + @override + String get memoryTitle => 'My Memory'; + + @override + String get memoryLoadFailedRetry => 'Load failed, please retry'; + + @override + String get memorySmartTitle => 'Smart Memory'; + + @override + String get memorySmartDesc => + 'Continuously learns your preferences and habits'; + + @override + String get memoryReload => 'Reload'; + + @override + String get memorySectionUser => 'User Memory'; + + @override + String get memorySectionWork => 'Work Memory'; + + @override + String get memoryUserProfile => 'Personal Preferences'; + + @override + String get memoryWorkProfile => 'Work Profile'; + + @override + String get memoryNoInfo => 'No info'; + + @override + String get memoryStatContacts => 'Contacts'; + + @override + String get memoryStatPlaces => 'Places'; + + @override + String get memoryStatInterests => 'Interests'; + + @override + String get memoryStatSchedule => 'Schedule'; + + @override + String get memoryStatExpertise => 'Expertise'; + + @override + String get memoryStatTools => 'Tools'; + + @override + String get memoryStatProjects => 'Projects'; + + @override + String get memoryStatTeam => 'Team'; + + @override + String memorySummaryContactsCount(int count) { + return '$count contacts'; + } + + @override + String memorySummaryPlacesCount(int count) { + return '$count places'; + } + + @override + String memorySummaryInterestsCount(int count) { + return '$count interests'; + } + + @override + String memorySummaryExpertiseCount(int count) { + return '$count expertise areas'; + } + + @override + String memorySummaryProjectsCount(int count) { + return '$count projects'; + } + + @override + String memorySummaryTeamMembersCount(int count) { + return '$count team members'; + } + + @override + String get toolCalendarRead => 'Read Calendar'; + + @override + String get toolCalendarWrite => 'Write Calendar'; + + @override + String get toolCalendarShare => 'Share Calendar'; + + @override + String get toolUserLookup => 'Lookup Contact'; + + @override + String get toolMemoryWrite => 'Write Memory'; + + @override + String get toolMemoryForget => 'Forget Memory'; + + @override + String get settingsTitle => 'Settings'; + + @override + String get settingsUnset => 'Not set'; + + @override + String get settingsFreeBadge => 'Free'; + + @override + String get settingsNoContacts => 'No contacts'; + + @override + String settingsContactsAddedOne(Object name) { + return 'Added 1 contact: $name'; + } + + @override + String settingsContactsAddedMany(int count) { + return 'Added $count contacts'; + } + + @override + String get settingsNoEnabledPlans => 'No enabled plans'; + + @override + String settingsEnabledPlanOne(Object title) { + return 'Enabled: $title'; + } + + @override + String settingsEnabledPlanMany(int count) { + return 'Enabled $count plans'; + } + + @override + String get settingsUpgradeProTitle => 'Upgrade to Pro'; + + @override + String get settingsUpgradeProDesc => 'Unlock more advanced features'; + + @override + String get settingsUpgradeButton => 'Upgrade'; + + @override + String get settingsMenuNotifications => 'Reminder Settings'; + + @override + String get settingsMenuCheckUpdates => 'Check for Updates'; + + @override + String get settingsLogoutTitle => 'Log Out'; + + @override + String get settingsLogoutConfirmMessage => + 'Are you sure you want to log out of this account?'; + + @override + String get settingsLogoutConfirm => 'Confirm Logout'; + + @override + String get settingsLogoutFailed => 'Logout failed, please try again later'; + + @override + String get settingsLatestVersion => 'You already have the latest version'; + + @override + String settingsUpdateRequired(Object version) { + return 'A new version is available ($version), please update now'; + } + + @override + String settingsUpdateOptional(Object version) { + return 'New version found ($version), update now?'; + } + + @override + String get settingsUpdateDialogTitle => 'Check for Updates'; + + @override + String get settingsUpdateAction => 'Update'; + + @override + String settingsDownloadLink(Object url) { + return 'Download link: $url'; + } + + @override + String get settingsUpdateCheckFailed => 'Failed to check updates'; + + @override + String get settingsJobDetailTitle => 'Job Detail'; + + @override + String get settingsJobCreatePageTitle => 'Create Recurring Plan'; + + @override + String get settingsJobLoadFailed => 'Load failed'; + + @override + String get settingsJobRetry => 'Retry'; + + @override + String get settingsJobPlanConfig => 'Plan Configuration'; + + @override + String get settingsJobCycle => 'Cycle'; + + @override + String get settingsJobRunAt => 'Run Time'; + + @override + String get settingsJobTimezone => 'Timezone'; + + @override + String get settingsJobStatusLabel => 'Status'; + + @override + String get settingsJobInputTemplate => 'Input Template'; + + @override + String get settingsJobEnabledTools => 'Enabled Tools'; + + @override + String get settingsJobContextMode => 'Context Message Mode'; + + @override + String get settingsJobContextSource => 'Source'; + + @override + String get settingsJobWindowMode => 'Window Mode'; + + @override + String get settingsJobWindowCount => 'Window Count'; + + @override + String settingsJobWindowCountValue(int count) { + return '$count'; + } + + @override + String get settingsJobDeleteTitle => 'Delete Recurring Plan'; + + @override + String get settingsJobDeleteMessage => + 'This action cannot be undone. Continue?'; + + @override + String get settingsJobDeleteConfirm => 'Confirm Delete'; + + @override + String get settingsJobDeleteSuccess => 'Deleted successfully'; + + @override + String get settingsJobBasicInfo => 'Basic Info'; + + @override + String get settingsJobName => 'Job Name'; + + @override + String get settingsJobNameHint => 'Enter job name'; + + @override + String get settingsJobTemplateHint => 'Example: summarize today memory'; + + @override + String get settingsJobExecutionRules => 'Execution Rules'; + + @override + String get settingsJobToolSelection => 'Tool Selection'; + + @override + String settingsJobCounterValue(Object label, int value) { + return '$label: $value'; + } + + @override + String get settingsJobWeekdayMon => 'Mon'; + + @override + String get settingsJobWeekdayTue => 'Tue'; + + @override + String get settingsJobWeekdayWed => 'Wed'; + + @override + String get settingsJobWeekdayThu => 'Thu'; + + @override + String get settingsJobWeekdayFri => 'Fri'; + + @override + String get settingsJobWeekdaySat => 'Sat'; + + @override + String get settingsJobWeekdaySun => 'Sun'; + + @override + String get settingsJobRunDays => 'Run Days'; + + @override + String get settingsJobNoToolsEnabled => 'No tools enabled'; + + @override + String get settingsJobPickCycle => 'Choose Cycle'; + + @override + String get settingsJobScheduleDaily => 'Daily'; + + @override + String get settingsJobScheduleWeekly => 'Weekly'; + + @override + String get settingsJobPickTimezone => 'Choose Timezone'; + + @override + String get settingsJobPickContextSource => 'Choose Context Source'; + + @override + String get settingsJobContextSourceLatestChat => 'Latest chat'; + + @override + String get settingsJobPickWindowMode => 'Choose Window Mode'; + + @override + String get settingsJobWindowModeByDay => 'By day'; + + @override + String get settingsJobWindowModeByNumber => 'By message count'; + + @override + String get settingsJobFillRequired => 'Please fill all required fields'; + + @override + String get settingsJobCreateSuccess => 'Created successfully'; + + @override + String get settingsMemorySaveSuccess => 'Saved successfully'; + + @override + String get settingsMemorySaveFailed => 'Failed to save'; + + @override + String settingsMemoryInputHint(Object label) { + return 'Enter $label'; + } + + @override + String get settingsMemoryInputContent => 'Enter content'; + + @override + String get settingsUserMemoryEditTitle => 'Edit Personal Preferences'; + + @override + String get settingsUserMemoryEmptyProfile => 'No personal preferences'; + + @override + String get settingsUserMemorySectionBasic => 'Basic Info'; + + @override + String get settingsUserMemorySectionPreferences => 'Preferences'; + + @override + String get settingsUserMemorySectionSchedule => 'Schedule Preferences'; + + @override + String get settingsUserMemorySectionContacts => 'Contacts'; + + @override + String get settingsUserMemorySectionPlaces => 'Places'; + + @override + String get settingsUserMemorySectionInterests => 'Interests'; + + @override + String get settingsUserMemorySectionAvoidTopics => 'Avoid Topics'; + + @override + String get settingsUserMemorySectionCustomRules => 'Custom Rules'; + + @override + String get settingsUserMemorySectionRoutines => 'Recurring Routines'; + + @override + String get settingsUserMemoryFieldOccupation => 'Occupation'; + + @override + String get settingsUserMemoryFieldTimezone => 'Timezone'; + + @override + String get settingsUserMemoryFieldPrimaryLanguage => 'Primary Language'; + + @override + String get settingsUserMemoryFieldCommunicationStyle => 'Communication Style'; + + @override + String get settingsUserMemoryFieldLocationPreference => 'Location Preference'; + + @override + String get settingsUserMemoryFieldWorkLifestyle => 'Work Lifestyle'; + + @override + String get settingsUserMemoryFieldLanguagePreference => 'Language Preference'; + + @override + String get settingsUserMemoryFieldNotificationPreference => + 'Notification Preference'; + + @override + String get settingsUserMemoryFieldMeetingBuffer => 'Meeting Buffer'; + + @override + String get settingsUserMemoryFieldMaxMeetingsPerDay => 'Max Meetings per Day'; + + @override + String get settingsUserMemoryFieldPreferredMeetingDuration => + 'Preferred Meeting Duration'; + + @override + String get settingsUserMemoryFieldNotes => 'Notes'; + + @override + String get settingsUserMemoryFieldName => 'Name'; + + @override + String get settingsUserMemoryFieldRelationship => 'Relationship'; + + @override + String get settingsUserMemoryFieldRole => 'Role'; + + @override + String get settingsUserMemoryFieldContact => 'Contact'; + + @override + String get settingsUserMemoryFieldCategory => 'Category'; + + @override + String get settingsUserMemoryFieldPreference => 'Preference'; + + @override + String get settingsUserMemoryFieldAddress => 'Address'; + + @override + String get settingsUserMemoryFieldDescription => 'Description'; + + @override + String get settingsUserMemoryFieldCadence => 'Cadence'; + + @override + String get settingsUserMemoryMinute => 'min'; + + @override + String settingsUserMemoryMinutesValue(int minutes) { + return '$minutes min'; + } + + @override + String get settingsUserMemoryEmptyContacts => 'No contacts'; + + @override + String get settingsUserMemoryEmptyPlaces => 'No places'; + + @override + String get settingsUserMemoryEmptyRoutines => 'No routines'; + + @override + String get settingsUserMemoryAddContact => 'Add Contact'; + + @override + String get settingsUserMemoryNewContact => 'New Contact'; + + @override + String get settingsUserMemoryAddPlace => 'Add Place'; + + @override + String get settingsUserMemoryNewPlace => 'New Place'; + + @override + String get settingsUserMemoryAddRoutine => 'Add Routine'; + + @override + String get settingsUserMemoryNewRoutine => 'New Routine'; + + @override + String get settingsWorkMemoryEditTitle => 'Edit Work Profile'; + + @override + String get settingsWorkMemoryEmptyProfile => 'No work info'; + + @override + String get settingsWorkMemorySectionBasic => 'Basic Info'; + + @override + String get settingsWorkMemorySectionExpertise => 'Expertise'; + + @override + String get settingsWorkMemorySectionPreferredTools => 'Preferred Tools'; + + @override + String get settingsWorkMemorySectionCurrentProjects => 'Current Projects'; + + @override + String get settingsWorkMemorySectionTeamMembers => 'Team Members'; + + @override + String get settingsWorkMemorySectionWorkHabits => 'Work Habits'; + + @override + String get settingsWorkMemorySectionTeamContext => 'Team Context'; + + @override + String get settingsWorkMemorySectionWorkRules => 'Work Rules'; + + @override + String get settingsWorkMemoryFieldOccupation => 'Occupation'; + + @override + String get settingsWorkMemoryFieldAvailableHours => 'Available Hours'; + + @override + String get settingsWorkMemoryFieldDeepWorkBlocks => 'Deep Work Blocks'; + + @override + String get settingsWorkMemoryFieldPreferredMeetingWindows => + 'Preferred Meeting Windows'; + + @override + String get settingsWorkMemoryFieldNoMeetingWindows => 'No-Meeting Windows'; + + @override + String get settingsWorkMemoryFieldPreferredMeetingDuration => + 'Preferred Meeting Duration'; + + @override + String get settingsWorkMemoryFieldNotificationChannel => + 'Notification Channel'; + + @override + String get settingsWorkMemoryFieldNotes => 'Notes'; + + @override + String get settingsWorkMemoryFieldTeamContext => 'Team Context'; + + @override + String get settingsWorkMemoryFieldProjectName => 'Project Name'; + + @override + String get settingsWorkMemoryFieldStatus => 'Status'; + + @override + String get settingsWorkMemoryFieldPriority => 'Priority'; + + @override + String get settingsWorkMemoryFieldDeadline => 'Deadline'; + + @override + String get settingsWorkMemoryFieldCollaborators => 'Collaborators'; + + @override + String get settingsWorkMemoryFieldMilestones => 'Milestones'; + + @override + String get settingsWorkMemoryMinute => 'min'; + + @override + String settingsWorkMemoryMilestoneCount(int count) { + return '$count items'; + } + + @override + String get settingsWorkMemoryEmptyProjects => 'No projects'; + + @override + String get settingsWorkMemoryEmptyTeamMembers => 'No team members'; + + @override + String settingsWorkMemoryTimeWindowCount(int count) { + return '$count windows'; + } + + @override + String get settingsWorkMemoryAddProject => 'Add Project'; + + @override + String get settingsWorkMemoryNewProject => 'New Project'; + + @override + String get settingsWorkMemoryAddMember => 'Add Member'; + + @override + String get settingsWorkMemoryNewMember => 'New Member'; + + @override + String get calendarDetailTitle => 'Event Details'; + + @override + String get calendarDetailNotFoundTitle => 'Event not found'; + + @override + String get calendarDetailNotFoundDesc => + 'It may have been deleted, or you do not have access.'; + + @override + String get calendarDetailTimeArrangement => 'Time Arrangement'; + + @override + String calendarDetailDateLabel(int year, int month, int day, Object weekday) { + return '$year-$month-$day $weekday'; + } + + @override + String get calendarDetailBasicInfo => 'Basic Info'; + + @override + String get calendarDetailDate => 'Date'; + + @override + String get calendarDetailReminder => 'Reminder'; + + @override + String get calendarDetailColor => 'Color'; + + @override + String get calendarDetailExtraInfo => 'Extra Info'; + + @override + String get calendarDetailLocation => 'Location'; + + @override + String get calendarDetailDescription => 'Description'; + + @override + String get calendarDetailNotes => 'Notes'; + + @override + String get calendarDetailReminderNone => 'None'; + + @override + String get calendarDetailReminderOnTime => 'On time'; + + @override + String calendarDetailReminderBeforeMinutes(int minutes) { + return '$minutes min before start'; + } + + @override + String get calendarWeekdayMon => 'Mon'; + + @override + String get calendarWeekdayTue => 'Tue'; + + @override + String get calendarWeekdayWed => 'Wed'; + + @override + String get calendarWeekdayThu => 'Thu'; + + @override + String get calendarWeekdayFri => 'Fri'; + + @override + String get calendarWeekdaySat => 'Sat'; + + @override + String get calendarWeekdaySun => 'Sun'; + + @override + String get calendarDetailDeleteTitle => 'Delete Event'; + + @override + String get calendarDetailDeleteMessage => + 'Are you sure you want to delete this event?'; + + @override + String get calendarDetailDeleteConfirm => 'Delete'; + + @override + String get calendarDetailArchiveTitle => 'Archive Event'; + + @override + String get calendarDetailArchiveMessage => + 'This will mark the event as expired. Continue?'; + + @override + String get calendarDetailArchiveConfirm => 'Archive'; + + @override + String get calendarDetailArchiveFailed => 'Archive failed'; + + @override + String calendarDetailDateTimeShort( + int month, + int day, + Object weekday, + Object time, + ) { + return '$month/$day $weekday $time'; + } + + @override + String calendarDetailRangeWithStartEnd(Object start, Object end) { + return 'Start: $start\nEnd: $end'; + } + + @override + String get calendarDetailStatusExpired => 'Expired'; + + @override + String get calendarCreateEditTitle => 'Edit Event'; + + @override + String get calendarCreateNewTitle => 'New Event'; + + @override + String get calendarCreateTabBasic => 'Basic'; + + @override + String get calendarCreateTabAdvanced => 'Advanced'; + + @override + String get calendarCreateFieldTitle => 'Title'; + + @override + String get calendarCreateFieldTitleHint => 'Enter event title'; + + @override + String get calendarCreateFieldStart => 'Start'; + + @override + String get calendarCreateFieldEnd => 'End'; + + @override + String get calendarCreateFieldDescription => 'Description'; + + @override + String get calendarCreateFieldDescriptionHint => 'Enter description'; + + @override + String get calendarCreateFieldLocation => 'Location'; + + @override + String get calendarCreateFieldLocationHint => 'Enter location'; + + @override + String get calendarCreateFieldNotesHint => 'Enter notes'; + + @override + String calendarCreateOptionalField(Object label) { + return '$label (optional)'; + } + + @override + String calendarCreateDateTimeLabel( + int year, + int month, + int day, + Object hour, + Object minute, + ) { + return '$year-$month-$day $hour:$minute'; + } + + @override + String get calendarCreateReminderNone => 'No reminder'; + + @override + String get calendarCreateReminderTime => 'Reminder Time'; + + @override + String get calendarCreatePickReminderTime => 'Select Reminder Time'; + + @override + String get calendarCreateReminderPermissionFailed => + 'Failed to create reminder, check notification permission'; + + @override + String get settingsEditProfileLoadFailed => 'Failed to load user profile'; + + @override + String get settingsEditProfileAvatarUploadSuccess => + 'Avatar uploaded successfully'; + + @override + String get settingsEditProfileAvatarUploadFailed => + 'Failed to upload avatar, please try again'; + + @override + String get settingsEditProfileUsernameRequired => 'Username is required'; + + @override + String get settingsEditProfileUsernameLengthInvalid => + 'Username must be 3-30 characters'; + + @override + String get settingsEditProfileSaveSuccess => 'Saved successfully'; + + @override + String get settingsEditProfileSaveFailed => 'Save failed, please try again'; + + @override + String get settingsEditProfileTitle => 'Edit Profile'; + + @override + String get settingsEditProfileSaveChanges => 'Save Changes'; + + @override + String get settingsEditProfileBasicInfo => 'Basic Info'; + + @override + String get settingsEditProfileUsername => 'Username'; + + @override + String get settingsEditProfileUsernameHint => 'Enter username'; + + @override + String get settingsEditProfileBio => 'Bio'; + + @override + String get settingsEditProfileBioContent => 'Bio Content'; + + @override + String get settingsEditProfileBioHint => 'Tell us about yourself'; + + @override + String get calendarSharePhoneRequired => 'Please enter a phone number'; + + @override + String get calendarShareInviteSent => 'Invite sent'; + + @override + String get calendarShareInviteFailed => 'Failed to send invite'; + + @override + String get calendarShareTitle => 'Share Calendar'; + + @override + String get calendarSharePhoneLabel => 'Phone'; + + @override + String get calendarSharePhoneHint => 'Enter recipient\'s +86 phone number'; + + @override + String get calendarSharePermissionTitle => 'Permissions'; + + @override + String get calendarSharePermissionView => 'View'; + + @override + String get calendarSharePermissionViewDesc => + 'Can view this calendar event (required)'; + + @override + String get calendarSharePermissionEdit => 'Edit'; + + @override + String get calendarSharePermissionEditDesc => 'Can edit this calendar event'; + + @override + String get calendarSharePermissionInvite => 'Invite'; + + @override + String get calendarSharePermissionInviteDesc => 'Can invite others'; + + @override + String get calendarShareSendInvite => 'Send Invite'; + + @override + String calendarMonthHeader(int month) { + return '$month'; + } + + @override + String get calendarMonthToday => 'Today'; + + @override + String get calendarMonthWeekdaySunShort => 'S'; + + @override + String get calendarMonthWeekdayMonShort => 'M'; + + @override + String get calendarMonthWeekdayTueShort => 'T'; + + @override + String get calendarMonthWeekdayWedShort => 'W'; + + @override + String get calendarMonthWeekdayThuShort => 'T'; + + @override + String get calendarMonthWeekdayFriShort => 'F'; + + @override + String get calendarMonthWeekdaySatShort => 'S'; + + @override + String calendarMonthYearLabel(int year) { + return '$year'; + } + + @override + String get calendarDateTimePickerDateLabel => 'Date'; + + @override + String get calendarDateTimePickerYearUnit => 'Y'; + + @override + String get calendarDateTimePickerMonthUnit => 'M'; + + @override + String get calendarDateTimePickerDayUnit => 'D'; + + @override + String get calendarDateTimePickerTimeLabel => 'Time'; + + @override + String get calendarDateTimePickerTitle => 'Select Time'; + + @override + String get messagesCalendarCardInviteTitle => 'Calendar Invite'; + + @override + String messagesCalendarCardInviteWithTitle(Object title) { + return 'Invites you to access \"$title\"'; + } + + @override + String get messagesCalendarCardInviteWithoutTitle => + 'Invites you to access a calendar'; + + @override + String messagesCalendarCardUpdatedWithTitle(Object title) { + return '$title updated'; + } + + @override + String get messagesCalendarCardUpdatedWithoutTitle => + 'Calendar event updated'; + + @override + String messagesCalendarCardTimeMinutesAgo(int minutes) { + return '${minutes}m ago'; + } + + @override + String messagesCalendarCardTimeHoursAgo(int hours) { + return '${hours}h ago'; + } + + @override + String messagesCalendarCardTimeDaysAgo(int days) { + return '${days}d ago'; + } + + @override + String messagesCalendarCardTimeDate(int month, int day) { + return '$month/$day'; + } + + @override + String messagesCalendarCardDeletedWithTitle(Object title) { + return '$title deleted'; + } + + @override + String get messagesCalendarCardDeletedWithoutTitle => + 'Calendar event deleted'; +} diff --git a/apps/lib/l10n/app_localizations_zh.dart b/apps/lib/l10n/app_localizations_zh.dart new file mode 100644 index 0000000..333c40d --- /dev/null +++ b/apps/lib/l10n/app_localizations_zh.dart @@ -0,0 +1,1793 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Chinese (`zh`). +class AppLocalizationsZh extends AppLocalizations { + AppLocalizationsZh([String locale = 'zh']) : super(locale); + + @override + String get appTitle => 'Linksy'; + + @override + String get commonConfirm => '确认'; + + @override + String get commonCancel => '取消'; + + @override + String get commonSave => '保存'; + + @override + String get commonDone => '完成'; + + @override + String get commonRetry => '重试'; + + @override + String get commonRefreshing => '正在刷新'; + + @override + String get commonLoading => '加载中...'; + + @override + String get commonEdit => '编辑'; + + @override + String get commonDelete => '删除'; + + @override + String get commonShare => '分享'; + + @override + String get commonArchive => '归档'; + + @override + String get commonCopySuccess => '已复制'; + + @override + String commonLoadFailed(Object error) { + return '加载失败: $error'; + } + + @override + String get commonUnknown => '未知'; + + @override + String get toastLabelSuccess => '成功'; + + @override + String get toastLabelWarning => '提醒'; + + @override + String get toastLabelError => '错误'; + + @override + String get toastLabelInfo => '提示'; + + @override + String get errorGenericSafe => '请求失败,请稍后重试'; + + @override + String get errorForbidden => '没有权限执行此操作'; + + @override + String get errorNotFound => '请求的资源不存在'; + + @override + String get errorTooManyRequests => '请求过于频繁,请稍后再试'; + + @override + String get errorServer => '服务器错误,请稍后再试'; + + @override + String get errorAgentSseConnectionLimit => '连接过于频繁,请稍后重试'; + + @override + String get errorAgentAttachmentEmpty => '附件内容为空'; + + @override + String get errorAgentAttachmentTooLarge => '附件过大,请压缩后重试'; + + @override + String get errorAgentAudioEmpty => '音频内容为空'; + + @override + String get errorAgentAudioTooLarge => '音频文件过大'; + + @override + String get errorAgentAudioUnsupportedFormat => '音频格式不支持'; + + @override + String get errorAgentAsrUnavailable => '语音服务暂不可用,请稍后重试'; + + @override + String get errorAgentInvalidLastEventId => '事件游标无效,请刷新后重试'; + + @override + String get errorAgentInvalidBinaryUrl => '图片链接无效,请重新上传'; + + @override + String get errorRequestFailed => '请求失败'; + + @override + String get errorNetwork => '网络错误'; + + @override + String get errorReLogin => '请重新登录'; + + @override + String get errorNetworkTimeout => '网络超时,请确认手机与服务端在同一网络后重试'; + + @override + String get errorNetworkUnavailable => + '无法连接服务器。请在 iPhone 设置中为本应用开启无线数据,并确认本地网络权限已开启。'; + + @override + String get homeViewHistory => '查看历史'; + + @override + String get homeNoEarlierHistory => '没有更早的历史记录了'; + + @override + String get homeSheetTakePhoto => '拍照'; + + @override + String get homeSheetPhotoLibrary => '相册'; + + @override + String homeDateLabelWithYear(int year, int month, int day, Object weekday) { + return '$year年$month月$day日 $weekday'; + } + + @override + String homeDateLabelNoYear(int month, int day, Object weekday) { + return '$month月$day日 $weekday'; + } + + @override + String get homeRecordingReleaseCancel => '松手取消'; + + @override + String get homeRecordingReleaseSend => '松手发送'; + + @override + String get homeRecordingHintReleaseCancel => '松开取消'; + + @override + String get homeRecordingHintReleaseSend => '松开发送,上滑取消'; + + @override + String get homeHoldToSpeakText => '按住说话'; + + @override + String get homeInputHint => '输入消息...'; + + @override + String get homeTranscribing => '语音识别中...'; + + @override + String get homeRecordingCanceled => '已取消'; + + @override + String get homeToolPreparing => '工具准备中'; + + @override + String get homeToolExecuting => '任务执行中'; + + @override + String get homeToolExecutionFailed => '执行失败'; + + @override + String get homeToolCompleted => '已完成'; + + @override + String get homeRecorderPluginUnavailable => '录音组件未加载,请完全重启 App 后重试'; + + @override + String get homeRecorderPermissionDenied => '录音权限未授权'; + + @override + String get homeStopRequested => '已请求停止'; + + @override + String get homeNoValidSpeech => '未识别到有效语音,请靠近麦克风并连续说话后重试'; + + @override + String get agentStageRouting => '意图识别中'; + + @override + String get agentStageExecution => '任务执行中'; + + @override + String get agentStageMemory => '记忆提取中'; + + @override + String get agentStageProcessing => '任务处理中'; + + @override + String get agUiEventRunStarted => '运行开始'; + + @override + String get agUiEventRunFinished => '运行完成'; + + @override + String get agUiEventRunError => '运行失败'; + + @override + String get agUiEventStepStarted => '阶段开始'; + + @override + String get agUiEventStepFinished => '阶段完成'; + + @override + String get agUiEventTextMessageEnd => '文本输出完成'; + + @override + String get agUiEventToolCallStart => '工具调用开始'; + + @override + String get agUiEventToolCallArgs => '工具参数更新'; + + @override + String get agUiEventToolCallEnd => '工具调用结束'; + + @override + String get agUiEventToolCallResult => '工具结果返回'; + + @override + String get agUiEventToolCallError => '工具调用失败'; + + @override + String get agUiEventUnknown => '未知事件'; + + @override + String get chatRunCanceled => '本次运行已取消'; + + @override + String get chatRunFailed => '本次运行已失败'; + + @override + String get chatSseInterruptedRetry => '连接中断,请重试'; + + @override + String get chatTimestampToday => '今天'; + + @override + String get chatTimestampYesterday => '昨天'; + + @override + String chatTimestampMonthDay(int month, int day) { + return '$month月$day日'; + } + + @override + String homeUnreadMessages(int count) { + return '有$count条新消息'; + } + + @override + String get calendarToday => '今天'; + + @override + String get calendarEventNoAccessOrMissing => '日程不存在或无权限'; + + @override + String calendarDayWeekMonthYearLabel(int year, int month) { + return '$year年$month月'; + } + + @override + String get validatorPhoneRequired => '请输入手机号'; + + @override + String get validatorPhoneInvalid86 => '请输入有效的 +86 手机号'; + + @override + String get validatorPasswordRequired => '请输入密码'; + + @override + String get validatorPasswordMin8 => '密码至少需要8位'; + + @override + String validatorRequired(Object fieldName) { + return '请输入$fieldName'; + } + + @override + String get validatorNicknameRequired => '请输入昵称'; + + @override + String get validatorNicknameMin2 => '昵称至少需要2个字符'; + + @override + String get authAgreementTitle => '请先同意协议'; + + @override + String get authAgreementMessage => + '在使用我们的服务之前,请先阅读并同意《用户协议》和《隐私政策》。\n\n只有您同意上述协议,我们才能为您提供服务。'; + + @override + String get authAgreementSemantics => '同意用户协议与隐私政策'; + + @override + String get authAgreementPrefix => '我已同意'; + + @override + String get authAgreementTerms => '《用户协议》'; + + @override + String get authAgreementAnd => '与'; + + @override + String get authAgreementPrivacy => '《隐私政策》'; + + @override + String get authPhoneHint => '输入手机号'; + + @override + String get authCodeHint => '输入验证码'; + + @override + String get authSendCode => '发送验证码'; + + @override + String get authShowPassword => '显示密码'; + + @override + String get authHidePassword => '隐藏密码'; + + @override + String get authLoginFailed => '登录失败'; + + @override + String get authCheckInput => '请检查输入'; + + @override + String get authLoginOrRegister => '登录/注册'; + + @override + String get authInvalidPhone => '请输入有效手机号'; + + @override + String get authSendCodeFailed => '验证码发送失败'; + + @override + String get inputUsernameRequired => '请输入用户名'; + + @override + String get inputUsernameMin => '用户名至少 3 个字符'; + + @override + String get inputUsernameMax => '用户名最多 30 个字符'; + + @override + String get inputPhoneRequired => '请输入手机号'; + + @override + String get inputPhoneInvalid => '手机号格式不正确'; + + @override + String get inputPasswordRequired => '请输入密码'; + + @override + String get inputPasswordMin => '密码至少 6 个字符'; + + @override + String get inputCodeRequired => '请输入验证码'; + + @override + String get inputCodeInvalid => '验证码必须是 6 位数字'; + + @override + String get uiSchemaInvalid => '无效 UI Schema'; + + @override + String uiSchemaUnsupportedLayout(Object type) { + return '不支持的布局节点: $type'; + } + + @override + String uiSchemaUnknownNode(Object type) { + return '未知节点: $type'; + } + + @override + String get uiSchemaActionFallback => '操作'; + + @override + String get uiSchemaActionNotImplemented => '该操作暂未接入'; + + @override + String get uiSchemaNavigationInvalidParams => '导航参数无效'; + + @override + String get uiSchemaNavigationInvalidPath => '导航路径无效'; + + @override + String notificationSnoozeMinutes(int minutes) { + return '$minutes 分钟'; + } + + @override + String get notificationSnoozeLater => '稍后提醒'; + + @override + String get notificationChannelName => '日程闹钟提醒'; + + @override + String get notificationChannelDescription => '日程到点闹钟式提醒通知'; + + @override + String get notificationStartsNow => '日程现在开始'; + + @override + String notificationStartsInMinutes(int minutes) { + return '日程即将开始(提前$minutes分钟)'; + } + + @override + String notificationLocation(Object location) { + return '地点:$location'; + } + + @override + String notificationNotes(Object notes) { + return '备注:$notes'; + } + + @override + String get todoScreenTitle => '待办事项'; + + @override + String get todoDetailTitle => '待办详情'; + + @override + String get todoCreateTitle => '新建待办'; + + @override + String get todoEditTitle => '编辑待办'; + + @override + String get todoMoveFailed => '移动失败'; + + @override + String get todoRefreshFailed => '刷新失败,请稍后重试'; + + @override + String todoCompleteFailed(Object error) { + return '完成失败: $error'; + } + + @override + String get todoNotFound => '待办不存在'; + + @override + String get todoCalendarEventCards => '日历事件卡片'; + + @override + String get todoPriorityQuadrant => '所属象限'; + + @override + String get todoLinkedCalendarEvents => '关联日历事件'; + + @override + String get todoStatus => '状态'; + + @override + String get todoStatusDone => '已完成'; + + @override + String get todoStatusInProgress => '进行中'; + + @override + String todoQuadrantOrder(int order) { + return '象限内顺序 #$order'; + } + + @override + String todoSplitToEvents(int count) { + return '已拆分为$count个日历事件'; + } + + @override + String get todoNoLinkedEvents => '未关联日历事件'; + + @override + String get todoDeleteTitle => '删除待办'; + + @override + String get todoDeleteMessage => '确定要删除这个待办吗?'; + + @override + String get todoDeleteConfirm => '确认删除'; + + @override + String todoDeleteFailed(Object error) { + return '删除失败: $error'; + } + + @override + String get todoQuadrantImportantUrgent => '重要紧急'; + + @override + String get todoQuadrantUrgentNotImportant => '紧急不重要'; + + @override + String get todoQuadrantImportantNotUrgent => '重要不紧急'; + + @override + String get todoQuadrantNotUrgentNotImportant => '不紧急不重要'; + + @override + String get todoNoItems => '暂无待办'; + + @override + String todoItemCount(int count) { + return '$count项'; + } + + @override + String get todoInfoTitle => '待办信息'; + + @override + String get todoInfoDescCreate => '创建后可在四象限中查看并继续调整优先级与关联事件。'; + + @override + String get todoInfoDescDone => '该待办已完成,你仍可调整内容并重新组织关联事件。'; + + @override + String get todoInfoDescDefault => '调整标题、优先级和关联事件,保持任务结构清晰。'; + + @override + String get todoFieldTitle => '标题'; + + @override + String get todoFieldTitleHint => '输入待办标题'; + + @override + String get todoFieldDescriptionOptional => '描述(可选)'; + + @override + String get todoFieldDescriptionHint => '补充细节或备注'; + + @override + String get todoPriority => '优先级'; + + @override + String get todoNoSelectableCalendarEvents => '暂无可关联的日历事件'; + + @override + String get todoSaveInProgress => '保存中...'; + + @override + String get todoCreateButton => '创建待办'; + + @override + String get todoSaveChanges => '保存修改'; + + @override + String get todoEnterTitle => '请输入标题'; + + @override + String todoSaveFailed(Object error) { + return '保存失败: $error'; + } + + @override + String get contactsTitle => '联系人'; + + @override + String get contactsSearchHint => '输入用户名或手机号'; + + @override + String get contactsSearchEmptyQuery => '请输入用户名或手机号'; + + @override + String get contactsSearchFailed => '搜索失败,请稍后重试'; + + @override + String get contactsSearchNoUser => '未找到该用户'; + + @override + String get contactsFriendRequestSent => '好友请求已发送'; + + @override + String get contactsSendFailed => '发送失败,请稍后重试'; + + @override + String get contactsSectionNew => '新的联系人'; + + @override + String get contactsSectionAll => '全部联系人'; + + @override + String get contactsStatusAlreadyFriend => '已是好友'; + + @override + String get contactsStatusSent => '已发送'; + + @override + String get contactsAdd => '添加'; + + @override + String get contactsEmptyTitle => '暂无联系人'; + + @override + String get contactsEmptyDesc => '搜索手机号添加好友开始聊天吧'; + + @override + String get contactsPendingConfirm => '等待对方确认'; + + @override + String contactsAddSheetTitle(Object username) { + return '添加 $username'; + } + + @override + String get contactsAddSheetDesc => '发送一条验证信息,方便对方确认你的身份'; + + @override + String get contactsAddSheetMessageHint => '你好,我是...'; + + @override + String get contactsSend => '发送'; + + @override + String get contactEditTitle => '编辑联系人'; + + @override + String get contactAddTitle => '添加联系人'; + + @override + String get contactNickname => '昵称'; + + @override + String get contactNicknameHint => '请输入昵称'; + + @override + String get contactPhone => '手机号'; + + @override + String get contactPhoneHint => '+86 请输入 11 位手机号'; + + @override + String get contactRemark => '备注'; + + @override + String get contactRemarkHint => '请输入备注'; + + @override + String get contactDelete => '删除联系人'; + + @override + String get contactFillRequired => '请填写昵称和手机号'; + + @override + String get contactDeleteConfirmTitle => '删除联系人'; + + @override + String get contactDeleteConfirmMessage => '确定要删除此联系人吗?'; + + @override + String get messagesLoadFailed => '消息加载失败,请稍后重试'; + + @override + String get messagesSenderLoadFailed => '发送者信息加载失败,请下拉重试'; + + @override + String get messagesFriendRequestMissing => '好友请求数据缺失'; + + @override + String get messagesAcceptedFriendRequest => '已接受好友请求'; + + @override + String get messagesRejectedFriendRequest => '已拒绝好友请求'; + + @override + String get messagesActionFailed => '处理失败,请稍后重试'; + + @override + String get messagesTabUnread => '未读'; + + @override + String get messagesTabRead => '已读'; + + @override + String get messagesEmptyUnreadTitle => '暂无未读消息'; + + @override + String get messagesEmptyReadTitle => '暂无已读消息'; + + @override + String get messagesEmptyUnreadDesc => '有新消息时会在这里显示'; + + @override + String get messagesEmptyReadDesc => '处理过的消息会显示在这里'; + + @override + String get messagesFriendRequestLoadFailed => '好友请求信息加载失败'; + + @override + String messagesFriendRequestTitle(Object username) { + return '$username 请求添加您为好友'; + } + + @override + String get messagesCalendarInvite => '日历邀请'; + + @override + String get messagesSystemMessage => '系统消息'; + + @override + String get messagesTapToView => '点击查看详情'; + + @override + String get messagesInviteJoinCalendar => '邀请您加入日历'; + + @override + String get messagesInviteAccepted => '已接受日历邀请'; + + @override + String get messagesInviteRejected => '已拒绝日历邀请'; + + @override + String get messagesCalendarUpdated => '更新了日历事件'; + + @override + String get messagesInviteStatusAccepted => '已接受'; + + @override + String get messagesInviteStatusRejected => '已拒绝'; + + @override + String get messagesInviteStatusHandled => '已处理'; + + @override + String get messagesInviteDetailNotFound => '邀请不存在或已失效'; + + @override + String get messagesInviteAcceptedToast => '已接受邀请'; + + @override + String get messagesInviteRejectedToast => '已拒绝邀请'; + + @override + String get messagesInviteOperationFailed => '操作失败,请稍后重试'; + + @override + String get messagesInviteDetailTitle => '日历邀请详情'; + + @override + String messagesInviteEvent(Object title) { + return '事件:$title'; + } + + @override + String get messagesInviteUnnamedEvent => '未命名日程'; + + @override + String messagesInviteSender(Object name) { + return '邀请人:$name'; + } + + @override + String get messagesInviteUnknownUser => '未知用户'; + + @override + String messagesInviteTime(Object time) { + return '消息时间:$time'; + } + + @override + String messagesInviteStatus(Object status) { + return '状态:$status'; + } + + @override + String messagesInviteId(Object id) { + return '邀请ID:$id'; + } + + @override + String get messagesInviteTip => '同意后将加入该日历事件,拒绝后该邀请会被标记为已处理'; + + @override + String get messagesInviteAlreadyHandled => '该邀请已处理,无需重复操作'; + + @override + String get messagesReject => '拒绝'; + + @override + String get messagesAccept => '同意'; + + @override + String get messagesStatusPending => '待处理'; + + @override + String get settingsFeaturesTitle => '周期计划'; + + @override + String get settingsSectionDaily => '每日'; + + @override + String get settingsSectionWeekly => '每周'; + + @override + String get settingsNoDailyPlans => '暂无每日计划'; + + @override + String get settingsNoWeeklyPlans => '暂无每周计划'; + + @override + String get settingsSystemJobReadonly => '系统预置任务状态不可修改'; + + @override + String get settingsJobStatusEnabled => '已启用'; + + @override + String get settingsJobStatusDisabled => '未启用'; + + @override + String get settingsJobSourceSystem => '系统预置'; + + @override + String get settingsJobSourceCustom => '自定义'; + + @override + String get settingsCreateJob => '创建任务'; + + @override + String get memoryTitle => '我的记忆'; + + @override + String get memoryLoadFailedRetry => '加载失败,请重试'; + + @override + String get memorySmartTitle => '智能记忆'; + + @override + String get memorySmartDesc => '持续学习你的偏好和习惯'; + + @override + String get memoryReload => '重新加载'; + + @override + String get memorySectionUser => '用户记忆'; + + @override + String get memorySectionWork => '工作记忆'; + + @override + String get memoryUserProfile => '个人偏好'; + + @override + String get memoryWorkProfile => '工作画像'; + + @override + String get memoryNoInfo => '暂无信息'; + + @override + String get memoryStatContacts => '联系人'; + + @override + String get memoryStatPlaces => '地点'; + + @override + String get memoryStatInterests => '兴趣'; + + @override + String get memoryStatSchedule => '日程'; + + @override + String get memoryStatExpertise => '专长'; + + @override + String get memoryStatTools => '工具'; + + @override + String get memoryStatProjects => '项目'; + + @override + String get memoryStatTeam => '团队'; + + @override + String memorySummaryContactsCount(int count) { + return '$count 位联系人'; + } + + @override + String memorySummaryPlacesCount(int count) { + return '$count 个地点'; + } + + @override + String memorySummaryInterestsCount(int count) { + return '$count 个兴趣'; + } + + @override + String memorySummaryExpertiseCount(int count) { + return '$count 项专长'; + } + + @override + String memorySummaryProjectsCount(int count) { + return '$count 个项目'; + } + + @override + String memorySummaryTeamMembersCount(int count) { + return '$count 位团队成员'; + } + + @override + String get toolCalendarRead => '读取日程'; + + @override + String get toolCalendarWrite => '写入日程'; + + @override + String get toolCalendarShare => '共享日程'; + + @override + String get toolUserLookup => '查找联系人'; + + @override + String get toolMemoryWrite => '写入记忆'; + + @override + String get toolMemoryForget => '清理记忆'; + + @override + String get settingsTitle => '设置'; + + @override + String get settingsUnset => '未设置'; + + @override + String get settingsFreeBadge => '免费'; + + @override + String get settingsNoContacts => '暂无联系人'; + + @override + String settingsContactsAddedOne(Object name) { + return '已添加 1 位:$name'; + } + + @override + String settingsContactsAddedMany(int count) { + return '已添加 $count 位联系人'; + } + + @override + String get settingsNoEnabledPlans => '暂无启用计划'; + + @override + String settingsEnabledPlanOne(Object title) { + return '已启用:$title'; + } + + @override + String settingsEnabledPlanMany(int count) { + return '已启用 $count 个计划'; + } + + @override + String get settingsUpgradeProTitle => '升级到 Pro'; + + @override + String get settingsUpgradeProDesc => '解锁更多高级功能'; + + @override + String get settingsUpgradeButton => '升级'; + + @override + String get settingsMenuNotifications => '提醒设置'; + + @override + String get settingsMenuCheckUpdates => '检查更新'; + + @override + String get settingsLogoutTitle => '退出登录'; + + @override + String get settingsLogoutConfirmMessage => '确定退出当前账户吗?'; + + @override + String get settingsLogoutConfirm => '确认退出'; + + @override + String get settingsLogoutFailed => '退出失败,请稍后重试'; + + @override + String get settingsLatestVersion => '当前已是最新版本'; + + @override + String settingsUpdateRequired(Object version) { + return '有新版本可用 ($version),请立即更新'; + } + + @override + String settingsUpdateOptional(Object version) { + return '发现新版本 ($version),是否更新?'; + } + + @override + String get settingsUpdateDialogTitle => '检查更新'; + + @override + String get settingsUpdateAction => '更新'; + + @override + String settingsDownloadLink(Object url) { + return '下载链接: $url'; + } + + @override + String get settingsUpdateCheckFailed => '检查更新失败'; + + @override + String get settingsJobDetailTitle => '任务详情'; + + @override + String get settingsJobCreatePageTitle => '新建周期计划'; + + @override + String get settingsJobLoadFailed => '加载失败'; + + @override + String get settingsJobRetry => '重试'; + + @override + String get settingsJobPlanConfig => '计划配置'; + + @override + String get settingsJobCycle => '周期'; + + @override + String get settingsJobRunAt => '执行时间'; + + @override + String get settingsJobTimezone => '时区'; + + @override + String get settingsJobStatusLabel => '状态'; + + @override + String get settingsJobInputTemplate => '输入模板'; + + @override + String get settingsJobEnabledTools => '启用工具'; + + @override + String get settingsJobContextMode => '上下文消息模式'; + + @override + String get settingsJobContextSource => '来源'; + + @override + String get settingsJobWindowMode => '窗口模式'; + + @override + String get settingsJobWindowCount => '窗口数量'; + + @override + String settingsJobWindowCountValue(int count) { + return '$count'; + } + + @override + String get settingsJobDeleteTitle => '删除周期计划'; + + @override + String get settingsJobDeleteMessage => '删除后将无法恢复,是否继续?'; + + @override + String get settingsJobDeleteConfirm => '确认删除'; + + @override + String get settingsJobDeleteSuccess => '删除成功'; + + @override + String get settingsJobBasicInfo => '基本信息'; + + @override + String get settingsJobName => '任务名称'; + + @override + String get settingsJobNameHint => '请输入任务名称'; + + @override + String get settingsJobTemplateHint => '例如:请总结今天的记忆内容'; + + @override + String get settingsJobExecutionRules => '执行规则'; + + @override + String get settingsJobToolSelection => '工具选择'; + + @override + String settingsJobCounterValue(Object label, int value) { + return '$label:$value'; + } + + @override + String get settingsJobWeekdayMon => '周一'; + + @override + String get settingsJobWeekdayTue => '周二'; + + @override + String get settingsJobWeekdayWed => '周三'; + + @override + String get settingsJobWeekdayThu => '周四'; + + @override + String get settingsJobWeekdayFri => '周五'; + + @override + String get settingsJobWeekdaySat => '周六'; + + @override + String get settingsJobWeekdaySun => '周日'; + + @override + String get settingsJobRunDays => '执行日'; + + @override + String get settingsJobNoToolsEnabled => '未启用工具'; + + @override + String get settingsJobPickCycle => '选择周期'; + + @override + String get settingsJobScheduleDaily => '每日'; + + @override + String get settingsJobScheduleWeekly => '每周'; + + @override + String get settingsJobPickTimezone => '选择时区'; + + @override + String get settingsJobPickContextSource => '选择上下文来源'; + + @override + String get settingsJobContextSourceLatestChat => '最近聊天'; + + @override + String get settingsJobPickWindowMode => '选择窗口模式'; + + @override + String get settingsJobWindowModeByDay => '按天数'; + + @override + String get settingsJobWindowModeByNumber => '按消息数'; + + @override + String get settingsJobFillRequired => '请填写完整信息'; + + @override + String get settingsJobCreateSuccess => '创建成功'; + + @override + String get settingsMemorySaveSuccess => '保存成功'; + + @override + String get settingsMemorySaveFailed => '保存失败'; + + @override + String settingsMemoryInputHint(Object label) { + return '输入$label'; + } + + @override + String get settingsMemoryInputContent => '输入内容'; + + @override + String get settingsUserMemoryEditTitle => '编辑个人偏好'; + + @override + String get settingsUserMemoryEmptyProfile => '暂无个人偏好信息'; + + @override + String get settingsUserMemorySectionBasic => '基本信息'; + + @override + String get settingsUserMemorySectionPreferences => '偏好设置'; + + @override + String get settingsUserMemorySectionSchedule => '日程偏好'; + + @override + String get settingsUserMemorySectionContacts => '联系人'; + + @override + String get settingsUserMemorySectionPlaces => '地点'; + + @override + String get settingsUserMemorySectionInterests => '兴趣'; + + @override + String get settingsUserMemorySectionAvoidTopics => '回避话题'; + + @override + String get settingsUserMemorySectionCustomRules => '自定义规则'; + + @override + String get settingsUserMemorySectionRoutines => '周期习惯'; + + @override + String get settingsUserMemoryFieldOccupation => '职业'; + + @override + String get settingsUserMemoryFieldTimezone => '时区'; + + @override + String get settingsUserMemoryFieldPrimaryLanguage => '主要语言'; + + @override + String get settingsUserMemoryFieldCommunicationStyle => '沟通风格'; + + @override + String get settingsUserMemoryFieldLocationPreference => '位置偏好'; + + @override + String get settingsUserMemoryFieldWorkLifestyle => '工作生活方式'; + + @override + String get settingsUserMemoryFieldLanguagePreference => '语言偏好'; + + @override + String get settingsUserMemoryFieldNotificationPreference => '通知偏好'; + + @override + String get settingsUserMemoryFieldMeetingBuffer => '会议缓冲时间'; + + @override + String get settingsUserMemoryFieldMaxMeetingsPerDay => '每日最多会议'; + + @override + String get settingsUserMemoryFieldPreferredMeetingDuration => '偏好会议时长'; + + @override + String get settingsUserMemoryFieldNotes => '备注'; + + @override + String get settingsUserMemoryFieldName => '名称'; + + @override + String get settingsUserMemoryFieldRelationship => '关系'; + + @override + String get settingsUserMemoryFieldRole => '角色'; + + @override + String get settingsUserMemoryFieldContact => '联系方式'; + + @override + String get settingsUserMemoryFieldCategory => '类别'; + + @override + String get settingsUserMemoryFieldPreference => '偏好'; + + @override + String get settingsUserMemoryFieldAddress => '地址'; + + @override + String get settingsUserMemoryFieldDescription => '描述'; + + @override + String get settingsUserMemoryFieldCadence => '周期'; + + @override + String get settingsUserMemoryMinute => '分钟'; + + @override + String settingsUserMemoryMinutesValue(int minutes) { + return '$minutes 分钟'; + } + + @override + String get settingsUserMemoryEmptyContacts => '暂无联系人'; + + @override + String get settingsUserMemoryEmptyPlaces => '暂无地点'; + + @override + String get settingsUserMemoryEmptyRoutines => '暂无周期习惯'; + + @override + String get settingsUserMemoryAddContact => '添加联系人'; + + @override + String get settingsUserMemoryNewContact => '新联系人'; + + @override + String get settingsUserMemoryAddPlace => '添加地点'; + + @override + String get settingsUserMemoryNewPlace => '新地点'; + + @override + String get settingsUserMemoryAddRoutine => '添加习惯'; + + @override + String get settingsUserMemoryNewRoutine => '新习惯'; + + @override + String get settingsWorkMemoryEditTitle => '编辑工作画像'; + + @override + String get settingsWorkMemoryEmptyProfile => '暂无工作信息'; + + @override + String get settingsWorkMemorySectionBasic => '基本信息'; + + @override + String get settingsWorkMemorySectionExpertise => '专长'; + + @override + String get settingsWorkMemorySectionPreferredTools => '偏好工具'; + + @override + String get settingsWorkMemorySectionCurrentProjects => '当前项目'; + + @override + String get settingsWorkMemorySectionTeamMembers => '团队成员'; + + @override + String get settingsWorkMemorySectionWorkHabits => '工作习惯'; + + @override + String get settingsWorkMemorySectionTeamContext => '团队背景'; + + @override + String get settingsWorkMemorySectionWorkRules => '工作规则'; + + @override + String get settingsWorkMemoryFieldOccupation => '职业'; + + @override + String get settingsWorkMemoryFieldAvailableHours => '可用时段'; + + @override + String get settingsWorkMemoryFieldDeepWorkBlocks => '深度工作时段'; + + @override + String get settingsWorkMemoryFieldPreferredMeetingWindows => '偏好会议时段'; + + @override + String get settingsWorkMemoryFieldNoMeetingWindows => '免打扰时段'; + + @override + String get settingsWorkMemoryFieldPreferredMeetingDuration => '偏好会议时长'; + + @override + String get settingsWorkMemoryFieldNotificationChannel => '通知渠道'; + + @override + String get settingsWorkMemoryFieldNotes => '备注'; + + @override + String get settingsWorkMemoryFieldTeamContext => '团队背景描述'; + + @override + String get settingsWorkMemoryFieldProjectName => '项目名称'; + + @override + String get settingsWorkMemoryFieldStatus => '状态'; + + @override + String get settingsWorkMemoryFieldPriority => '优先级'; + + @override + String get settingsWorkMemoryFieldDeadline => '截止日期'; + + @override + String get settingsWorkMemoryFieldCollaborators => '协作人'; + + @override + String get settingsWorkMemoryFieldMilestones => '关键里程碑'; + + @override + String get settingsWorkMemoryMinute => '分钟'; + + @override + String settingsWorkMemoryMilestoneCount(int count) { + return '$count 项'; + } + + @override + String get settingsWorkMemoryEmptyProjects => '暂无项目'; + + @override + String get settingsWorkMemoryEmptyTeamMembers => '暂无团队成员'; + + @override + String settingsWorkMemoryTimeWindowCount(int count) { + return '$count 个时段'; + } + + @override + String get settingsWorkMemoryAddProject => '添加项目'; + + @override + String get settingsWorkMemoryNewProject => '新项目'; + + @override + String get settingsWorkMemoryAddMember => '添加成员'; + + @override + String get settingsWorkMemoryNewMember => '新成员'; + + @override + String get calendarDetailTitle => '日程详情'; + + @override + String get calendarDetailNotFoundTitle => '未找到该日程'; + + @override + String get calendarDetailNotFoundDesc => '可能已被删除,或你没有访问权限。'; + + @override + String get calendarDetailTimeArrangement => '时间安排'; + + @override + String calendarDetailDateLabel(int year, int month, int day, Object weekday) { + return '$year年$month月$day日 $weekday'; + } + + @override + String get calendarDetailBasicInfo => '基础信息'; + + @override + String get calendarDetailDate => '日期'; + + @override + String get calendarDetailReminder => '提醒'; + + @override + String get calendarDetailColor => '颜色'; + + @override + String get calendarDetailExtraInfo => '补充信息'; + + @override + String get calendarDetailLocation => '地点'; + + @override + String get calendarDetailDescription => '描述'; + + @override + String get calendarDetailNotes => '备注'; + + @override + String get calendarDetailReminderNone => '无'; + + @override + String get calendarDetailReminderOnTime => '准时提醒'; + + @override + String calendarDetailReminderBeforeMinutes(int minutes) { + return '开始前$minutes分钟'; + } + + @override + String get calendarWeekdayMon => '周一'; + + @override + String get calendarWeekdayTue => '周二'; + + @override + String get calendarWeekdayWed => '周三'; + + @override + String get calendarWeekdayThu => '周四'; + + @override + String get calendarWeekdayFri => '周五'; + + @override + String get calendarWeekdaySat => '周六'; + + @override + String get calendarWeekdaySun => '周日'; + + @override + String get calendarDetailDeleteTitle => '删除日程'; + + @override + String get calendarDetailDeleteMessage => '确定要删除这个日程吗?'; + + @override + String get calendarDetailDeleteConfirm => '确认删除'; + + @override + String get calendarDetailArchiveTitle => '归档日程'; + + @override + String get calendarDetailArchiveMessage => '归档后此日程将标记为过期,确定要归档吗?'; + + @override + String get calendarDetailArchiveConfirm => '确认归档'; + + @override + String get calendarDetailArchiveFailed => '归档失败'; + + @override + String calendarDetailDateTimeShort( + int month, + int day, + Object weekday, + Object time, + ) { + return '$month月$day日 $weekday $time'; + } + + @override + String calendarDetailRangeWithStartEnd(Object start, Object end) { + return '开始: $start\n结束: $end'; + } + + @override + String get calendarDetailStatusExpired => '已过期'; + + @override + String get calendarCreateEditTitle => '编辑日程'; + + @override + String get calendarCreateNewTitle => '新建日程'; + + @override + String get calendarCreateTabBasic => '基础'; + + @override + String get calendarCreateTabAdvanced => '进阶'; + + @override + String get calendarCreateFieldTitle => '标题'; + + @override + String get calendarCreateFieldTitleHint => '请输入日程标题'; + + @override + String get calendarCreateFieldStart => '开始'; + + @override + String get calendarCreateFieldEnd => '结束'; + + @override + String get calendarCreateFieldDescription => '描述'; + + @override + String get calendarCreateFieldDescriptionHint => '请输入描述'; + + @override + String get calendarCreateFieldLocation => '地点'; + + @override + String get calendarCreateFieldLocationHint => '请输入地点'; + + @override + String get calendarCreateFieldNotesHint => '请输入备注'; + + @override + String calendarCreateOptionalField(Object label) { + return '$label(可选)'; + } + + @override + String calendarCreateDateTimeLabel( + int year, + int month, + int day, + Object hour, + Object minute, + ) { + return '$year年$month月$day日 $hour:$minute'; + } + + @override + String get calendarCreateReminderNone => '无提醒'; + + @override + String get calendarCreateReminderTime => '提醒时间'; + + @override + String get calendarCreatePickReminderTime => '选择提醒时间'; + + @override + String get calendarCreateReminderPermissionFailed => '提醒创建失败,请检查通知权限'; + + @override + String get settingsEditProfileLoadFailed => '加载用户信息失败'; + + @override + String get settingsEditProfileAvatarUploadSuccess => '头像上传成功'; + + @override + String get settingsEditProfileAvatarUploadFailed => '头像上传失败,请重试'; + + @override + String get settingsEditProfileUsernameRequired => '用户名不能为空'; + + @override + String get settingsEditProfileUsernameLengthInvalid => '用户名需要3-30个字符'; + + @override + String get settingsEditProfileSaveSuccess => '保存成功'; + + @override + String get settingsEditProfileSaveFailed => '保存失败,请重试'; + + @override + String get settingsEditProfileTitle => '编辑资料'; + + @override + String get settingsEditProfileSaveChanges => '保存修改'; + + @override + String get settingsEditProfileBasicInfo => '基础信息'; + + @override + String get settingsEditProfileUsername => '用户名'; + + @override + String get settingsEditProfileUsernameHint => '请输入用户名'; + + @override + String get settingsEditProfileBio => '个人简介'; + + @override + String get settingsEditProfileBioContent => '简介内容'; + + @override + String get settingsEditProfileBioHint => '介绍一下自己吧'; + + @override + String get calendarSharePhoneRequired => '请输入手机号'; + + @override + String get calendarShareInviteSent => '邀请已发送'; + + @override + String get calendarShareInviteFailed => '发送邀请失败'; + + @override + String get calendarShareTitle => '分享日历'; + + @override + String get calendarSharePhoneLabel => '手机号'; + + @override + String get calendarSharePhoneHint => '输入对方的 +86 手机号'; + + @override + String get calendarSharePermissionTitle => '权限设置'; + + @override + String get calendarSharePermissionView => '查看'; + + @override + String get calendarSharePermissionViewDesc => '可以查看此日历事件(必选)'; + + @override + String get calendarSharePermissionEdit => '编辑'; + + @override + String get calendarSharePermissionEditDesc => '可以编辑此日历事件'; + + @override + String get calendarSharePermissionInvite => '邀请'; + + @override + String get calendarSharePermissionInviteDesc => '可以邀请其他人'; + + @override + String get calendarShareSendInvite => '发送邀请'; + + @override + String calendarMonthHeader(int month) { + return '$month月'; + } + + @override + String get calendarMonthToday => '今天'; + + @override + String get calendarMonthWeekdaySunShort => '日'; + + @override + String get calendarMonthWeekdayMonShort => '一'; + + @override + String get calendarMonthWeekdayTueShort => '二'; + + @override + String get calendarMonthWeekdayWedShort => '三'; + + @override + String get calendarMonthWeekdayThuShort => '四'; + + @override + String get calendarMonthWeekdayFriShort => '五'; + + @override + String get calendarMonthWeekdaySatShort => '六'; + + @override + String calendarMonthYearLabel(int year) { + return '$year年'; + } + + @override + String get calendarDateTimePickerDateLabel => '日期'; + + @override + String get calendarDateTimePickerYearUnit => '年'; + + @override + String get calendarDateTimePickerMonthUnit => '月'; + + @override + String get calendarDateTimePickerDayUnit => '日'; + + @override + String get calendarDateTimePickerTimeLabel => '时间'; + + @override + String get calendarDateTimePickerTitle => '选择时间'; + + @override + String get messagesCalendarCardInviteTitle => '日历邀请'; + + @override + String messagesCalendarCardInviteWithTitle(Object title) { + return '邀请你访问 \"$title\"'; + } + + @override + String get messagesCalendarCardInviteWithoutTitle => '邀请你访问日历'; + + @override + String messagesCalendarCardUpdatedWithTitle(Object title) { + return '$title 已更新'; + } + + @override + String get messagesCalendarCardUpdatedWithoutTitle => '日历事件已更新'; + + @override + String messagesCalendarCardTimeMinutesAgo(int minutes) { + return '$minutes分钟前'; + } + + @override + String messagesCalendarCardTimeHoursAgo(int hours) { + return '$hours小时前'; + } + + @override + String messagesCalendarCardTimeDaysAgo(int days) { + return '$days天前'; + } + + @override + String messagesCalendarCardTimeDate(int month, int day) { + return '$month月$day日'; + } + + @override + String messagesCalendarCardDeletedWithTitle(Object title) { + return '$title 已删除'; + } + + @override + String get messagesCalendarCardDeletedWithoutTitle => '日历事件已删除'; +} diff --git a/apps/lib/l10n/app_zh.arb b/apps/lib/l10n/app_zh.arb new file mode 100644 index 0000000..211862d --- /dev/null +++ b/apps/lib/l10n/app_zh.arb @@ -0,0 +1,775 @@ +{ + "@@locale": "zh", + "appTitle": "Linksy", + "commonConfirm": "确认", + "commonCancel": "取消", + "commonSave": "保存", + "commonDone": "完成", + "commonRetry": "重试", + "commonRefreshing": "正在刷新", + "commonLoading": "加载中...", + "commonEdit": "编辑", + "commonDelete": "删除", + "commonShare": "分享", + "commonArchive": "归档", + "commonCopySuccess": "已复制", + "commonLoadFailed": "加载失败: {error}", + "@commonLoadFailed": { + "placeholders": { + "error": {} + } + }, + "commonUnknown": "未知", + "toastLabelSuccess": "成功", + "toastLabelWarning": "提醒", + "toastLabelError": "错误", + "toastLabelInfo": "提示", + "errorGenericSafe": "请求失败,请稍后重试", + "errorForbidden": "没有权限执行此操作", + "errorNotFound": "请求的资源不存在", + "errorTooManyRequests": "请求过于频繁,请稍后再试", + "errorServer": "服务器错误,请稍后再试", + "errorAgentSseConnectionLimit": "连接过于频繁,请稍后重试", + "errorAgentAttachmentEmpty": "附件内容为空", + "errorAgentAttachmentTooLarge": "附件过大,请压缩后重试", + "errorAgentAudioEmpty": "音频内容为空", + "errorAgentAudioTooLarge": "音频文件过大", + "errorAgentAudioUnsupportedFormat": "音频格式不支持", + "errorAgentAsrUnavailable": "语音服务暂不可用,请稍后重试", + "errorAgentInvalidLastEventId": "事件游标无效,请刷新后重试", + "errorAgentInvalidBinaryUrl": "图片链接无效,请重新上传", + "errorRequestFailed": "请求失败", + "errorNetwork": "网络错误", + "errorReLogin": "请重新登录", + "errorNetworkTimeout": "网络超时,请确认手机与服务端在同一网络后重试", + "errorNetworkUnavailable": "无法连接服务器。请在 iPhone 设置中为本应用开启无线数据,并确认本地网络权限已开启。", + "homeViewHistory": "查看历史", + "homeNoEarlierHistory": "没有更早的历史记录了", + "homeSheetTakePhoto": "拍照", + "homeSheetPhotoLibrary": "相册", + "homeDateLabelWithYear": "{year}年{month}月{day}日 {weekday}", + "@homeDateLabelWithYear": {"placeholders": {"year": {"type": "int"}, "month": {"type": "int"}, "day": {"type": "int"}, "weekday": {}}}, + "homeDateLabelNoYear": "{month}月{day}日 {weekday}", + "@homeDateLabelNoYear": {"placeholders": {"month": {"type": "int"}, "day": {"type": "int"}, "weekday": {}}}, + "homeRecordingReleaseCancel": "松手取消", + "homeRecordingReleaseSend": "松手发送", + "homeRecordingHintReleaseCancel": "松开取消", + "homeRecordingHintReleaseSend": "松开发送,上滑取消", + "homeHoldToSpeakText": "按住说话", + "homeInputHint": "输入消息...", + "homeTranscribing": "语音识别中...", + "homeRecordingCanceled": "已取消", + "homeToolPreparing": "工具准备中", + "homeToolExecuting": "任务执行中", + "homeToolExecutionFailed": "执行失败", + "homeToolCompleted": "已完成", + "homeRecorderPluginUnavailable": "录音组件未加载,请完全重启 App 后重试", + "homeRecorderPermissionDenied": "录音权限未授权", + "homeStopRequested": "已请求停止", + "homeNoValidSpeech": "未识别到有效语音,请靠近麦克风并连续说话后重试", + "agentStageRouting": "意图识别中", + "agentStageExecution": "任务执行中", + "agentStageMemory": "记忆提取中", + "agentStageProcessing": "任务处理中", + "agUiEventRunStarted": "运行开始", + "agUiEventRunFinished": "运行完成", + "agUiEventRunError": "运行失败", + "agUiEventStepStarted": "阶段开始", + "agUiEventStepFinished": "阶段完成", + "agUiEventTextMessageEnd": "文本输出完成", + "agUiEventToolCallStart": "工具调用开始", + "agUiEventToolCallArgs": "工具参数更新", + "agUiEventToolCallEnd": "工具调用结束", + "agUiEventToolCallResult": "工具结果返回", + "agUiEventToolCallError": "工具调用失败", + "agUiEventUnknown": "未知事件", + "chatRunCanceled": "本次运行已取消", + "chatRunFailed": "本次运行已失败", + "chatSseInterruptedRetry": "连接中断,请重试", + "chatTimestampToday": "今天", + "chatTimestampYesterday": "昨天", + "chatTimestampMonthDay": "{month}月{day}日", + "@chatTimestampMonthDay": { + "placeholders": { + "month": { + "type": "int" + }, + "day": { + "type": "int" + } + } + }, + "homeUnreadMessages": "有{count}条新消息", + "@homeUnreadMessages": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "calendarToday": "今天", + "calendarEventNoAccessOrMissing": "日程不存在或无权限", + "calendarDayWeekMonthYearLabel": "{year}年{month}月", + "@calendarDayWeekMonthYearLabel": {"placeholders": {"year": {"type": "int"}, "month": {"type": "int"}}}, + "validatorPhoneRequired": "请输入手机号", + "validatorPhoneInvalid86": "请输入有效的 +86 手机号", + "validatorPasswordRequired": "请输入密码", + "validatorPasswordMin8": "密码至少需要8位", + "validatorRequired": "请输入{fieldName}", + "@validatorRequired": {"placeholders": {"fieldName": {}}}, + "validatorNicknameRequired": "请输入昵称", + "validatorNicknameMin2": "昵称至少需要2个字符", + "authAgreementTitle": "请先同意协议", + "authAgreementMessage": "在使用我们的服务之前,请先阅读并同意《用户协议》和《隐私政策》。\n\n只有您同意上述协议,我们才能为您提供服务。", + "authAgreementSemantics": "同意用户协议与隐私政策", + "authAgreementPrefix": "我已同意", + "authAgreementTerms": "《用户协议》", + "authAgreementAnd": "与", + "authAgreementPrivacy": "《隐私政策》", + "authPhoneHint": "输入手机号", + "authCodeHint": "输入验证码", + "authSendCode": "发送验证码", + "authShowPassword": "显示密码", + "authHidePassword": "隐藏密码", + "authLoginFailed": "登录失败", + "authCheckInput": "请检查输入", + "authLoginOrRegister": "登录/注册", + "authInvalidPhone": "请输入有效手机号", + "authSendCodeFailed": "验证码发送失败", + "inputUsernameRequired": "请输入用户名", + "inputUsernameMin": "用户名至少 3 个字符", + "inputUsernameMax": "用户名最多 30 个字符", + "inputPhoneRequired": "请输入手机号", + "inputPhoneInvalid": "手机号格式不正确", + "inputPasswordRequired": "请输入密码", + "inputPasswordMin": "密码至少 6 个字符", + "inputCodeRequired": "请输入验证码", + "inputCodeInvalid": "验证码必须是 6 位数字", + "uiSchemaInvalid": "无效 UI Schema", + "uiSchemaUnsupportedLayout": "不支持的布局节点: {type}", + "@uiSchemaUnsupportedLayout": { + "placeholders": { + "type": {} + } + }, + "uiSchemaUnknownNode": "未知节点: {type}", + "@uiSchemaUnknownNode": { + "placeholders": { + "type": {} + } + }, + "uiSchemaActionFallback": "操作", + "uiSchemaActionNotImplemented": "该操作暂未接入", + "uiSchemaNavigationInvalidParams": "导航参数无效", + "uiSchemaNavigationInvalidPath": "导航路径无效", + "notificationSnoozeMinutes": "{minutes} 分钟", + "@notificationSnoozeMinutes": { + "placeholders": { + "minutes": { + "type": "int" + } + } + }, + "notificationSnoozeLater": "稍后提醒", + "notificationChannelName": "日程闹钟提醒", + "notificationChannelDescription": "日程到点闹钟式提醒通知", + "notificationStartsNow": "日程现在开始", + "notificationStartsInMinutes": "日程即将开始(提前{minutes}分钟)", + "@notificationStartsInMinutes": { + "placeholders": { + "minutes": { + "type": "int" + } + } + }, + "notificationLocation": "地点:{location}", + "@notificationLocation": { + "placeholders": { + "location": {} + } + }, + "notificationNotes": "备注:{notes}", + "@notificationNotes": { + "placeholders": { + "notes": {} + } + }, + "todoScreenTitle": "待办事项", + "todoDetailTitle": "待办详情", + "todoCreateTitle": "新建待办", + "todoEditTitle": "编辑待办", + "todoMoveFailed": "移动失败", + "todoRefreshFailed": "刷新失败,请稍后重试", + "todoCompleteFailed": "完成失败: {error}", + "@todoCompleteFailed": { + "placeholders": { + "error": {} + } + }, + "todoNotFound": "待办不存在", + "todoCalendarEventCards": "日历事件卡片", + "todoPriorityQuadrant": "所属象限", + "todoLinkedCalendarEvents": "关联日历事件", + "todoStatus": "状态", + "todoStatusDone": "已完成", + "todoStatusInProgress": "进行中", + "todoQuadrantOrder": "象限内顺序 #{order}", + "@todoQuadrantOrder": { + "placeholders": { + "order": { + "type": "int" + } + } + }, + "todoSplitToEvents": "已拆分为{count}个日历事件", + "@todoSplitToEvents": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "todoNoLinkedEvents": "未关联日历事件", + "todoDeleteTitle": "删除待办", + "todoDeleteMessage": "确定要删除这个待办吗?", + "todoDeleteConfirm": "确认删除", + "todoDeleteFailed": "删除失败: {error}", + "@todoDeleteFailed": { + "placeholders": { + "error": {} + } + }, + "todoQuadrantImportantUrgent": "重要紧急", + "todoQuadrantUrgentNotImportant": "紧急不重要", + "todoQuadrantImportantNotUrgent": "重要不紧急", + "todoQuadrantNotUrgentNotImportant": "不紧急不重要", + "todoNoItems": "暂无待办", + "todoItemCount": "{count}项", + "@todoItemCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "todoInfoTitle": "待办信息", + "todoInfoDescCreate": "创建后可在四象限中查看并继续调整优先级与关联事件。", + "todoInfoDescDone": "该待办已完成,你仍可调整内容并重新组织关联事件。", + "todoInfoDescDefault": "调整标题、优先级和关联事件,保持任务结构清晰。", + "todoFieldTitle": "标题", + "todoFieldTitleHint": "输入待办标题", + "todoFieldDescriptionOptional": "描述(可选)", + "todoFieldDescriptionHint": "补充细节或备注", + "todoPriority": "优先级", + "todoNoSelectableCalendarEvents": "暂无可关联的日历事件", + "todoSaveInProgress": "保存中...", + "todoCreateButton": "创建待办", + "todoSaveChanges": "保存修改", + "todoEnterTitle": "请输入标题", + "todoSaveFailed": "保存失败: {error}", + "@todoSaveFailed": { + "placeholders": { + "error": {} + } + }, + "contactsTitle": "联系人", + "contactsSearchHint": "输入用户名或手机号", + "contactsSearchEmptyQuery": "请输入用户名或手机号", + "contactsSearchFailed": "搜索失败,请稍后重试", + "contactsSearchNoUser": "未找到该用户", + "contactsFriendRequestSent": "好友请求已发送", + "contactsSendFailed": "发送失败,请稍后重试", + "contactsSectionNew": "新的联系人", + "contactsSectionAll": "全部联系人", + "contactsStatusAlreadyFriend": "已是好友", + "contactsStatusSent": "已发送", + "contactsAdd": "添加", + "contactsEmptyTitle": "暂无联系人", + "contactsEmptyDesc": "搜索手机号添加好友开始聊天吧", + "contactsPendingConfirm": "等待对方确认", + "contactsAddSheetTitle": "添加 {username}", + "@contactsAddSheetTitle": {"placeholders": {"username": {}}}, + "contactsAddSheetDesc": "发送一条验证信息,方便对方确认你的身份", + "contactsAddSheetMessageHint": "你好,我是...", + "contactsSend": "发送", + "contactEditTitle": "编辑联系人", + "contactAddTitle": "添加联系人", + "contactNickname": "昵称", + "contactNicknameHint": "请输入昵称", + "contactPhone": "手机号", + "contactPhoneHint": "+86 请输入 11 位手机号", + "contactRemark": "备注", + "contactRemarkHint": "请输入备注", + "contactDelete": "删除联系人", + "contactFillRequired": "请填写昵称和手机号", + "contactDeleteConfirmTitle": "删除联系人", + "contactDeleteConfirmMessage": "确定要删除此联系人吗?", + "messagesLoadFailed": "消息加载失败,请稍后重试", + "messagesSenderLoadFailed": "发送者信息加载失败,请下拉重试", + "messagesFriendRequestMissing": "好友请求数据缺失", + "messagesAcceptedFriendRequest": "已接受好友请求", + "messagesRejectedFriendRequest": "已拒绝好友请求", + "messagesActionFailed": "处理失败,请稍后重试", + "messagesTabUnread": "未读", + "messagesTabRead": "已读", + "messagesEmptyUnreadTitle": "暂无未读消息", + "messagesEmptyReadTitle": "暂无已读消息", + "messagesEmptyUnreadDesc": "有新消息时会在这里显示", + "messagesEmptyReadDesc": "处理过的消息会显示在这里", + "messagesFriendRequestLoadFailed": "好友请求信息加载失败", + "messagesFriendRequestTitle": "{username} 请求添加您为好友", + "@messagesFriendRequestTitle": {"placeholders": {"username": {}}}, + "messagesCalendarInvite": "日历邀请", + "messagesSystemMessage": "系统消息", + "messagesTapToView": "点击查看详情", + "messagesInviteJoinCalendar": "邀请您加入日历", + "messagesInviteAccepted": "已接受日历邀请", + "messagesInviteRejected": "已拒绝日历邀请", + "messagesCalendarUpdated": "更新了日历事件", + "messagesInviteStatusAccepted": "已接受", + "messagesInviteStatusRejected": "已拒绝", + "messagesInviteStatusHandled": "已处理", + "messagesInviteDetailNotFound": "邀请不存在或已失效", + "messagesInviteAcceptedToast": "已接受邀请", + "messagesInviteRejectedToast": "已拒绝邀请", + "messagesInviteOperationFailed": "操作失败,请稍后重试", + "messagesInviteDetailTitle": "日历邀请详情", + "messagesInviteEvent": "事件:{title}", + "@messagesInviteEvent": {"placeholders": {"title": {}}}, + "messagesInviteUnnamedEvent": "未命名日程", + "messagesInviteSender": "邀请人:{name}", + "@messagesInviteSender": {"placeholders": {"name": {}}}, + "messagesInviteUnknownUser": "未知用户", + "messagesInviteTime": "消息时间:{time}", + "@messagesInviteTime": {"placeholders": {"time": {}}}, + "messagesInviteStatus": "状态:{status}", + "@messagesInviteStatus": {"placeholders": {"status": {}}}, + "messagesInviteId": "邀请ID:{id}", + "@messagesInviteId": {"placeholders": {"id": {}}}, + "messagesInviteTip": "同意后将加入该日历事件,拒绝后该邀请会被标记为已处理", + "messagesInviteAlreadyHandled": "该邀请已处理,无需重复操作", + "messagesReject": "拒绝", + "messagesAccept": "同意", + "messagesStatusPending": "待处理", + "settingsFeaturesTitle": "周期计划", + "settingsSectionDaily": "每日", + "settingsSectionWeekly": "每周", + "settingsNoDailyPlans": "暂无每日计划", + "settingsNoWeeklyPlans": "暂无每周计划", + "settingsSystemJobReadonly": "系统预置任务状态不可修改", + "settingsJobStatusEnabled": "已启用", + "settingsJobStatusDisabled": "未启用", + "settingsJobSourceSystem": "系统预置", + "settingsJobSourceCustom": "自定义", + "settingsCreateJob": "创建任务", + "memoryTitle": "我的记忆", + "memoryLoadFailedRetry": "加载失败,请重试", + "memorySmartTitle": "智能记忆", + "memorySmartDesc": "持续学习你的偏好和习惯", + "memoryReload": "重新加载", + "memorySectionUser": "用户记忆", + "memorySectionWork": "工作记忆", + "memoryUserProfile": "个人偏好", + "memoryWorkProfile": "工作画像", + "memoryNoInfo": "暂无信息", + "memoryStatContacts": "联系人", + "memoryStatPlaces": "地点", + "memoryStatInterests": "兴趣", + "memoryStatSchedule": "日程", + "memoryStatExpertise": "专长", + "memoryStatTools": "工具", + "memoryStatProjects": "项目", + "memoryStatTeam": "团队", + "memorySummaryContactsCount": "{count} 位联系人", + "@memorySummaryContactsCount": {"placeholders": {"count": {"type": "int"}}}, + "memorySummaryPlacesCount": "{count} 个地点", + "@memorySummaryPlacesCount": {"placeholders": {"count": {"type": "int"}}}, + "memorySummaryInterestsCount": "{count} 个兴趣", + "@memorySummaryInterestsCount": {"placeholders": {"count": {"type": "int"}}}, + "memorySummaryExpertiseCount": "{count} 项专长", + "@memorySummaryExpertiseCount": {"placeholders": {"count": {"type": "int"}}}, + "memorySummaryProjectsCount": "{count} 个项目", + "@memorySummaryProjectsCount": {"placeholders": {"count": {"type": "int"}}}, + "memorySummaryTeamMembersCount": "{count} 位团队成员", + "@memorySummaryTeamMembersCount": {"placeholders": {"count": {"type": "int"}}}, + "toolCalendarRead": "读取日程", + "toolCalendarWrite": "写入日程", + "toolCalendarShare": "共享日程", + "toolUserLookup": "查找联系人", + "toolMemoryWrite": "写入记忆", + "toolMemoryForget": "清理记忆", + "settingsTitle": "设置", + "settingsUnset": "未设置", + "settingsFreeBadge": "免费", + "settingsNoContacts": "暂无联系人", + "settingsContactsAddedOne": "已添加 1 位:{name}", + "@settingsContactsAddedOne": {"placeholders": {"name": {}}}, + "settingsContactsAddedMany": "已添加 {count} 位联系人", + "@settingsContactsAddedMany": {"placeholders": {"count": {"type": "int"}}}, + "settingsNoEnabledPlans": "暂无启用计划", + "settingsEnabledPlanOne": "已启用:{title}", + "@settingsEnabledPlanOne": {"placeholders": {"title": {}}}, + "settingsEnabledPlanMany": "已启用 {count} 个计划", + "@settingsEnabledPlanMany": {"placeholders": {"count": {"type": "int"}}}, + "settingsUpgradeProTitle": "升级到 Pro", + "settingsUpgradeProDesc": "解锁更多高级功能", + "settingsUpgradeButton": "升级", + "settingsMenuNotifications": "提醒设置", + "settingsMenuCheckUpdates": "检查更新", + "settingsLogoutTitle": "退出登录", + "settingsLogoutConfirmMessage": "确定退出当前账户吗?", + "settingsLogoutConfirm": "确认退出", + "settingsLogoutFailed": "退出失败,请稍后重试", + "settingsLatestVersion": "当前已是最新版本", + "settingsUpdateRequired": "有新版本可用 ({version}),请立即更新", + "@settingsUpdateRequired": {"placeholders": {"version": {}}}, + "settingsUpdateOptional": "发现新版本 ({version}),是否更新?", + "@settingsUpdateOptional": {"placeholders": {"version": {}}}, + "settingsUpdateDialogTitle": "检查更新", + "settingsUpdateAction": "更新", + "settingsDownloadLink": "下载链接: {url}", + "@settingsDownloadLink": {"placeholders": {"url": {}}}, + "settingsUpdateCheckFailed": "检查更新失败", + "settingsJobDetailTitle": "任务详情", + "settingsJobCreatePageTitle": "新建周期计划", + "settingsJobLoadFailed": "加载失败", + "settingsJobRetry": "重试", + "settingsJobPlanConfig": "计划配置", + "settingsJobCycle": "周期", + "settingsJobRunAt": "执行时间", + "settingsJobTimezone": "时区", + "settingsJobStatusLabel": "状态", + "settingsJobInputTemplate": "输入模板", + "settingsJobEnabledTools": "启用工具", + "settingsJobContextMode": "上下文消息模式", + "settingsJobContextSource": "来源", + "settingsJobWindowMode": "窗口模式", + "settingsJobWindowCount": "窗口数量", + "settingsJobWindowCountValue": "{count}", + "@settingsJobWindowCountValue": {"placeholders": {"count": {"type": "int"}}}, + "settingsJobDeleteTitle": "删除周期计划", + "settingsJobDeleteMessage": "删除后将无法恢复,是否继续?", + "settingsJobDeleteConfirm": "确认删除", + "settingsJobDeleteSuccess": "删除成功", + "settingsJobBasicInfo": "基本信息", + "settingsJobName": "任务名称", + "settingsJobNameHint": "请输入任务名称", + "settingsJobTemplateHint": "例如:请总结今天的记忆内容", + "settingsJobExecutionRules": "执行规则", + "settingsJobToolSelection": "工具选择", + "settingsJobCounterValue": "{label}:{value}", + "@settingsJobCounterValue": { + "placeholders": { + "label": {}, + "value": {"type": "int"} + } + }, + "settingsJobWeekdayMon": "周一", + "settingsJobWeekdayTue": "周二", + "settingsJobWeekdayWed": "周三", + "settingsJobWeekdayThu": "周四", + "settingsJobWeekdayFri": "周五", + "settingsJobWeekdaySat": "周六", + "settingsJobWeekdaySun": "周日", + "settingsJobRunDays": "执行日", + "settingsJobNoToolsEnabled": "未启用工具", + "settingsJobPickCycle": "选择周期", + "settingsJobScheduleDaily": "每日", + "settingsJobScheduleWeekly": "每周", + "settingsJobPickTimezone": "选择时区", + "settingsJobPickContextSource": "选择上下文来源", + "settingsJobContextSourceLatestChat": "最近聊天", + "settingsJobPickWindowMode": "选择窗口模式", + "settingsJobWindowModeByDay": "按天数", + "settingsJobWindowModeByNumber": "按消息数", + "settingsJobFillRequired": "请填写完整信息", + "settingsJobCreateSuccess": "创建成功", + "settingsMemorySaveSuccess": "保存成功", + "settingsMemorySaveFailed": "保存失败", + "settingsMemoryInputHint": "输入{label}", + "@settingsMemoryInputHint": {"placeholders": {"label": {}}}, + "settingsMemoryInputContent": "输入内容", + "settingsUserMemoryEditTitle": "编辑个人偏好", + "settingsUserMemoryEmptyProfile": "暂无个人偏好信息", + "settingsUserMemorySectionBasic": "基本信息", + "settingsUserMemorySectionPreferences": "偏好设置", + "settingsUserMemorySectionSchedule": "日程偏好", + "settingsUserMemorySectionContacts": "联系人", + "settingsUserMemorySectionPlaces": "地点", + "settingsUserMemorySectionInterests": "兴趣", + "settingsUserMemorySectionAvoidTopics": "回避话题", + "settingsUserMemorySectionCustomRules": "自定义规则", + "settingsUserMemorySectionRoutines": "周期习惯", + "settingsUserMemoryFieldOccupation": "职业", + "settingsUserMemoryFieldTimezone": "时区", + "settingsUserMemoryFieldPrimaryLanguage": "主要语言", + "settingsUserMemoryFieldCommunicationStyle": "沟通风格", + "settingsUserMemoryFieldLocationPreference": "位置偏好", + "settingsUserMemoryFieldWorkLifestyle": "工作生活方式", + "settingsUserMemoryFieldLanguagePreference": "语言偏好", + "settingsUserMemoryFieldNotificationPreference": "通知偏好", + "settingsUserMemoryFieldMeetingBuffer": "会议缓冲时间", + "settingsUserMemoryFieldMaxMeetingsPerDay": "每日最多会议", + "settingsUserMemoryFieldPreferredMeetingDuration": "偏好会议时长", + "settingsUserMemoryFieldNotes": "备注", + "settingsUserMemoryFieldName": "名称", + "settingsUserMemoryFieldRelationship": "关系", + "settingsUserMemoryFieldRole": "角色", + "settingsUserMemoryFieldContact": "联系方式", + "settingsUserMemoryFieldCategory": "类别", + "settingsUserMemoryFieldPreference": "偏好", + "settingsUserMemoryFieldAddress": "地址", + "settingsUserMemoryFieldDescription": "描述", + "settingsUserMemoryFieldCadence": "周期", + "settingsUserMemoryMinute": "分钟", + "settingsUserMemoryMinutesValue": "{minutes} 分钟", + "@settingsUserMemoryMinutesValue": { + "placeholders": { + "minutes": {"type": "int"} + } + }, + "settingsUserMemoryEmptyContacts": "暂无联系人", + "settingsUserMemoryEmptyPlaces": "暂无地点", + "settingsUserMemoryEmptyRoutines": "暂无周期习惯", + "settingsUserMemoryAddContact": "添加联系人", + "settingsUserMemoryNewContact": "新联系人", + "settingsUserMemoryAddPlace": "添加地点", + "settingsUserMemoryNewPlace": "新地点", + "settingsUserMemoryAddRoutine": "添加习惯", + "settingsUserMemoryNewRoutine": "新习惯", + "settingsWorkMemoryEditTitle": "编辑工作画像", + "settingsWorkMemoryEmptyProfile": "暂无工作信息", + "settingsWorkMemorySectionBasic": "基本信息", + "settingsWorkMemorySectionExpertise": "专长", + "settingsWorkMemorySectionPreferredTools": "偏好工具", + "settingsWorkMemorySectionCurrentProjects": "当前项目", + "settingsWorkMemorySectionTeamMembers": "团队成员", + "settingsWorkMemorySectionWorkHabits": "工作习惯", + "settingsWorkMemorySectionTeamContext": "团队背景", + "settingsWorkMemorySectionWorkRules": "工作规则", + "settingsWorkMemoryFieldOccupation": "职业", + "settingsWorkMemoryFieldAvailableHours": "可用时段", + "settingsWorkMemoryFieldDeepWorkBlocks": "深度工作时段", + "settingsWorkMemoryFieldPreferredMeetingWindows": "偏好会议时段", + "settingsWorkMemoryFieldNoMeetingWindows": "免打扰时段", + "settingsWorkMemoryFieldPreferredMeetingDuration": "偏好会议时长", + "settingsWorkMemoryFieldNotificationChannel": "通知渠道", + "settingsWorkMemoryFieldNotes": "备注", + "settingsWorkMemoryFieldTeamContext": "团队背景描述", + "settingsWorkMemoryFieldProjectName": "项目名称", + "settingsWorkMemoryFieldStatus": "状态", + "settingsWorkMemoryFieldPriority": "优先级", + "settingsWorkMemoryFieldDeadline": "截止日期", + "settingsWorkMemoryFieldCollaborators": "协作人", + "settingsWorkMemoryFieldMilestones": "关键里程碑", + "settingsWorkMemoryMinute": "分钟", + "settingsWorkMemoryMilestoneCount": "{count} 项", + "@settingsWorkMemoryMilestoneCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "settingsWorkMemoryEmptyProjects": "暂无项目", + "settingsWorkMemoryEmptyTeamMembers": "暂无团队成员", + "settingsWorkMemoryTimeWindowCount": "{count} 个时段", + "@settingsWorkMemoryTimeWindowCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "settingsWorkMemoryAddProject": "添加项目", + "settingsWorkMemoryNewProject": "新项目", + "settingsWorkMemoryAddMember": "添加成员", + "settingsWorkMemoryNewMember": "新成员", + "calendarDetailTitle": "日程详情", + "calendarDetailNotFoundTitle": "未找到该日程", + "calendarDetailNotFoundDesc": "可能已被删除,或你没有访问权限。", + "calendarDetailTimeArrangement": "时间安排", + "calendarDetailDateLabel": "{year}年{month}月{day}日 {weekday}", + "@calendarDetailDateLabel": { + "placeholders": { + "year": {"type": "int"}, + "month": {"type": "int"}, + "day": {"type": "int"}, + "weekday": {} + } + }, + "calendarDetailBasicInfo": "基础信息", + "calendarDetailDate": "日期", + "calendarDetailReminder": "提醒", + "calendarDetailColor": "颜色", + "calendarDetailExtraInfo": "补充信息", + "calendarDetailLocation": "地点", + "calendarDetailDescription": "描述", + "calendarDetailNotes": "备注", + "calendarDetailReminderNone": "无", + "calendarDetailReminderOnTime": "准时提醒", + "calendarDetailReminderBeforeMinutes": "开始前{minutes}分钟", + "@calendarDetailReminderBeforeMinutes": { + "placeholders": { + "minutes": {"type": "int"} + } + }, + "calendarWeekdayMon": "周一", + "calendarWeekdayTue": "周二", + "calendarWeekdayWed": "周三", + "calendarWeekdayThu": "周四", + "calendarWeekdayFri": "周五", + "calendarWeekdaySat": "周六", + "calendarWeekdaySun": "周日", + "calendarDetailDeleteTitle": "删除日程", + "calendarDetailDeleteMessage": "确定要删除这个日程吗?", + "calendarDetailDeleteConfirm": "确认删除", + "calendarDetailArchiveTitle": "归档日程", + "calendarDetailArchiveMessage": "归档后此日程将标记为过期,确定要归档吗?", + "calendarDetailArchiveConfirm": "确认归档", + "calendarDetailArchiveFailed": "归档失败", + "calendarDetailDateTimeShort": "{month}月{day}日 {weekday} {time}", + "@calendarDetailDateTimeShort": { + "placeholders": { + "month": {"type": "int"}, + "day": {"type": "int"}, + "weekday": {}, + "time": {} + } + }, + "calendarDetailRangeWithStartEnd": "开始: {start}\n结束: {end}", + "@calendarDetailRangeWithStartEnd": { + "placeholders": { + "start": {}, + "end": {} + } + }, + "calendarDetailStatusExpired": "已过期", + "calendarCreateEditTitle": "编辑日程", + "calendarCreateNewTitle": "新建日程", + "calendarCreateTabBasic": "基础", + "calendarCreateTabAdvanced": "进阶", + "calendarCreateFieldTitle": "标题", + "calendarCreateFieldTitleHint": "请输入日程标题", + "calendarCreateFieldStart": "开始", + "calendarCreateFieldEnd": "结束", + "calendarCreateFieldDescription": "描述", + "calendarCreateFieldDescriptionHint": "请输入描述", + "calendarCreateFieldLocation": "地点", + "calendarCreateFieldLocationHint": "请输入地点", + "calendarCreateFieldNotesHint": "请输入备注", + "calendarCreateOptionalField": "{label}(可选)", + "@calendarCreateOptionalField": {"placeholders": {"label": {}}}, + "calendarCreateDateTimeLabel": "{year}年{month}月{day}日 {hour}:{minute}", + "@calendarCreateDateTimeLabel": { + "placeholders": { + "year": {"type": "int"}, + "month": {"type": "int"}, + "day": {"type": "int"}, + "hour": {}, + "minute": {} + } + }, + "calendarCreateReminderNone": "无提醒", + "calendarCreateReminderTime": "提醒时间", + "calendarCreatePickReminderTime": "选择提醒时间", + "calendarCreateReminderPermissionFailed": "提醒创建失败,请检查通知权限", + "settingsEditProfileLoadFailed": "加载用户信息失败", + "settingsEditProfileAvatarUploadSuccess": "头像上传成功", + "settingsEditProfileAvatarUploadFailed": "头像上传失败,请重试", + "settingsEditProfileUsernameRequired": "用户名不能为空", + "settingsEditProfileUsernameLengthInvalid": "用户名需要3-30个字符", + "settingsEditProfileSaveSuccess": "保存成功", + "settingsEditProfileSaveFailed": "保存失败,请重试", + "settingsEditProfileTitle": "编辑资料", + "settingsEditProfileSaveChanges": "保存修改", + "settingsEditProfileBasicInfo": "基础信息", + "settingsEditProfileUsername": "用户名", + "settingsEditProfileUsernameHint": "请输入用户名", + "settingsEditProfileBio": "个人简介", + "settingsEditProfileBioContent": "简介内容", + "settingsEditProfileBioHint": "介绍一下自己吧", + "calendarSharePhoneRequired": "请输入手机号", + "calendarShareInviteSent": "邀请已发送", + "calendarShareInviteFailed": "发送邀请失败", + "calendarShareTitle": "分享日历", + "calendarSharePhoneLabel": "手机号", + "calendarSharePhoneHint": "输入对方的 +86 手机号", + "calendarSharePermissionTitle": "权限设置", + "calendarSharePermissionView": "查看", + "calendarSharePermissionViewDesc": "可以查看此日历事件(必选)", + "calendarSharePermissionEdit": "编辑", + "calendarSharePermissionEditDesc": "可以编辑此日历事件", + "calendarSharePermissionInvite": "邀请", + "calendarSharePermissionInviteDesc": "可以邀请其他人", + "calendarShareSendInvite": "发送邀请", + "calendarMonthHeader": "{month}月", + "@calendarMonthHeader": { + "placeholders": { + "month": {"type": "int"} + } + }, + "calendarMonthToday": "今天", + "calendarMonthWeekdaySunShort": "日", + "calendarMonthWeekdayMonShort": "一", + "calendarMonthWeekdayTueShort": "二", + "calendarMonthWeekdayWedShort": "三", + "calendarMonthWeekdayThuShort": "四", + "calendarMonthWeekdayFriShort": "五", + "calendarMonthWeekdaySatShort": "六", + "calendarMonthYearLabel": "{year}年", + "@calendarMonthYearLabel": { + "placeholders": { + "year": {"type": "int"} + } + }, + "calendarDateTimePickerDateLabel": "日期", + "calendarDateTimePickerYearUnit": "年", + "calendarDateTimePickerMonthUnit": "月", + "calendarDateTimePickerDayUnit": "日", + "calendarDateTimePickerTimeLabel": "时间", + "calendarDateTimePickerTitle": "选择时间", + "messagesCalendarCardInviteTitle": "日历邀请", + "messagesCalendarCardInviteWithTitle": "邀请你访问 \"{title}\"", + "@messagesCalendarCardInviteWithTitle": { + "placeholders": { + "title": {} + } + }, + "messagesCalendarCardInviteWithoutTitle": "邀请你访问日历", + "messagesCalendarCardUpdatedWithTitle": "{title} 已更新", + "@messagesCalendarCardUpdatedWithTitle": { + "placeholders": { + "title": {} + } + }, + "messagesCalendarCardUpdatedWithoutTitle": "日历事件已更新", + "messagesCalendarCardTimeMinutesAgo": "{minutes}分钟前", + "@messagesCalendarCardTimeMinutesAgo": { + "placeholders": { + "minutes": {"type": "int"} + } + }, + "messagesCalendarCardTimeHoursAgo": "{hours}小时前", + "@messagesCalendarCardTimeHoursAgo": { + "placeholders": { + "hours": {"type": "int"} + } + }, + "messagesCalendarCardTimeDaysAgo": "{days}天前", + "@messagesCalendarCardTimeDaysAgo": { + "placeholders": { + "days": {"type": "int"} + } + }, + "messagesCalendarCardTimeDate": "{month}月{day}日", + "@messagesCalendarCardTimeDate": { + "placeholders": { + "month": {"type": "int"}, + "day": {"type": "int"} + } + }, + "messagesCalendarCardDeletedWithTitle": "{title} 已删除", + "@messagesCalendarCardDeletedWithTitle": { + "placeholders": { + "title": {} + } + }, + "messagesCalendarCardDeletedWithoutTitle": "日历事件已删除" +} diff --git a/apps/lib/main.dart b/apps/lib/main.dart index f4bfe18..c94041e 100644 --- a/apps/lib/main.dart +++ b/apps/lib/main.dart @@ -1,241 +1,17 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - import 'core/constants/app_constants.dart'; -import 'core/cache/cache_refresh_coordinator.dart'; -import 'core/di/injection.dart'; -import 'core/notifications/local_notification_service.dart'; -import 'core/notifications/reminder_notification_callbacks.dart'; -import 'core/notifications/ios_notification_payload_bridge.dart'; -import 'core/router/app_router.dart'; -import 'core/startup/auth_session_bootstrapper.dart'; -import 'core/theme/app_theme.dart'; +import 'app/di/injection.dart'; import 'features/auth/presentation/bloc/auth_bloc.dart'; import 'features/auth/presentation/bloc/auth_event.dart'; -import 'features/auth/presentation/bloc/auth_state.dart'; -import 'features/calendar/data/services/calendar_service.dart'; -import 'features/calendar/data/services/calendar_repository.dart'; -import 'features/calendar/reminders/reminder_queue_manager.dart'; -import 'features/calendar/reminders/ui/reminder_overlay.dart'; -import 'features/calendar/ui/calendar_state_manager.dart'; -import 'features/chat/presentation/bloc/chat_bloc.dart'; -import 'features/settings/data/services/settings_user_cache.dart'; -import 'features/todo/data/todo_repository.dart'; +import 'app/app.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await configureDependencies(); await AppConstants.init(); - final rootNavigatorKey = GlobalKey(); - await sl().initialize(); - final authBloc = sl(); authBloc.add(AuthStarted()); - final cacheRefreshCoordinator = CacheRefreshCoordinator( - minInterval: const Duration(minutes: 5), - onRefresh: () { - final selected = sl().selectedDate; - unawaited( - sl().getDayEvents(selected, forceRefresh: true), - ); - unawaited( - sl().getMonthEvents( - DateTime(selected.year, selected.month, 1), - forceRefresh: true, - ), - ); - unawaited(sl().getPendingTodos(forceRefresh: true)); - unawaited(sl().getProfile(forceRefresh: true)); - }, - ); - WidgetsBinding.instance.addObserver(cacheRefreshCoordinator); - - final prefs = await SharedPreferences.getInstance(); - final payloadBridge = IOSNotificationPayloadBridge(prefs); - final pendingPayload = await payloadBridge.getPendingPayload(); - final queueManager = ReminderQueueManager(); - if (pendingPayload != null) { - queueManager.enqueueFromClick(pendingPayload); - await payloadBridge.clearPendingPayload(); - } - - final linksyAppKey = GlobalKey(); - - ReminderNotificationCallbacks.onNotificationPayloadReceived = - (payload) async { - await payloadBridge.setPendingPayload(payload); - queueManager.enqueueFromClick(payload); - WidgetsBinding.instance.addPostFrameCallback((_) { - final state = linksyAppKey.currentState; - if (state != null && state is _LinksyAppState) { - state.showReminderOverlayFromNotification(); - } - }); - }; - - runApp( - LinksyApp( - key: linksyAppKey, - authBloc: authBloc, - rootNavigatorKey: rootNavigatorKey, - sessionBootstrapper: AuthSessionBootstrapper( - calendarService: sl(), - notificationService: sl(), - ), - queueManager: queueManager, - payloadBridge: payloadBridge, - ), - ); - - WidgetsBinding.instance.addPostFrameCallback((_) { - unawaited( - ReminderNotificationCallbacks.bindResponseHandler( - sl().handleNotificationResponse, - ), - ); - }); -} - -class LinksyApp extends StatefulWidget { - final AuthBloc authBloc; - final GlobalKey rootNavigatorKey; - final AuthSessionBootstrapper sessionBootstrapper; - final ReminderQueueManager queueManager; - final IOSNotificationPayloadBridge payloadBridge; - - const LinksyApp({ - super.key, - required this.authBloc, - required this.rootNavigatorKey, - required this.sessionBootstrapper, - required this.queueManager, - required this.payloadBridge, - }); - - @override - State createState() => _LinksyAppState(); -} - -class _LinksyAppState extends State with WidgetsBindingObserver { - OverlayEntry? _reminderOverlay; - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addObserver(this); - _checkAndShowReminderOverlay(); - } - - @override - void dispose() { - WidgetsBinding.instance.removeObserver(this); - super.dispose(); - } - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - if (state == AppLifecycleState.resumed) { - _checkAndShowReminderOverlay(); - } - } - - Future _checkAndShowReminderOverlay() async { - if (widget.queueManager.currentPayload != null) { - _showReminderOverlay(); - return; - } - final pendingPayload = await widget.payloadBridge.getPendingPayload(); - if (pendingPayload != null) { - widget.queueManager.enqueueFromClick(pendingPayload); - await widget.payloadBridge.clearPendingPayload(); - _showReminderOverlay(); - } - } - - void _showReminderOverlay() { - if (_reminderOverlay != null) return; - - _reminderOverlay = OverlayEntry( - builder: (context) => Positioned.fill( - child: Material( - color: Colors.black54, - child: ReminderOverlay( - queueManager: widget.queueManager, - onComplete: _onReminderComplete, - onSnooze: _onSnooze, - onArchive: _onArchive, - ), - ), - ), - ); - Overlay.of(context).insert(_reminderOverlay!); - } - - void showReminderOverlayFromNotification() { - if (widget.queueManager.currentPayload != null) { - _showReminderOverlay(); - } - } - - void _onReminderComplete() { - _reminderOverlay?.remove(); - _reminderOverlay = null; - - if (!widget.queueManager.isEmpty) { - _showReminderOverlay(); - } - } - - Future _onSnooze(int minutes) async { - final payload = widget.queueManager.currentPayload; - if (payload == null) return; - - await sl().cancelEventReminder(payload.eventId); - final event = await sl().getEventById(payload.eventId); - if (event != null) { - final snoozeTime = DateTime.now().add(Duration(minutes: minutes)); - await sl().scheduleReminderAt( - event, - snoozeTime, - ); - } - } - - Future _onArchive() async { - final payload = widget.queueManager.currentPayload; - if (payload == null) return; - - try { - await sl().archiveEvent(payload.eventId); - } catch (_) { - // archive failed, continue anyway - } - } - - @override - Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider.value(value: widget.authBloc), - BlocProvider(create: (_) => ChatBloc(apiClient: sl())), - ], - child: BlocListener( - listenWhen: (previous, current) => previous != current, - listener: (context, state) { - unawaited(widget.sessionBootstrapper.syncForAuthState(state)); - }, - child: MaterialApp.router( - title: 'Linksy', - debugShowCheckedModeBanner: false, - theme: AppTheme.light, - routerConfig: createAppRouter(widget.authBloc), - ), - ), - ); - } + runApp(LinksyApp(authBloc: authBloc)); } diff --git a/apps/lib/core/form_inputs/form_inputs.dart b/apps/lib/shared/forms/inputs.dart similarity index 61% rename from apps/lib/core/form_inputs/form_inputs.dart rename to apps/lib/shared/forms/inputs.dart index 25ff9b8..ac2f812 100644 --- a/apps/lib/core/form_inputs/form_inputs.dart +++ b/apps/lib/shared/forms/inputs.dart @@ -1,4 +1,5 @@ import 'package:formz/formz.dart'; +import '../../core/l10n/l10n.dart'; class Username extends FormzInput { const Username.pure() : super.pure(''); @@ -6,9 +7,9 @@ class Username extends FormzInput { @override String? validator(String value) { - if (value.isEmpty) return '请输入用户名'; - if (value.length < 3) return '用户名至少 3 个字符'; - if (value.length > 30) return '用户名最多 30 个字符'; + if (value.isEmpty) return L10n.current.inputUsernameRequired; + if (value.length < 3) return L10n.current.inputUsernameMin; + if (value.length > 30) return L10n.current.inputUsernameMax; return null; } } @@ -22,8 +23,8 @@ class Phone extends FormzInput { @override String? validator(String value) { final normalized = value.replaceAll(RegExp(r'\s+'), ''); - if (normalized.isEmpty) return '请输入手机号'; - if (!_regex.hasMatch(normalized)) return '手机号格式不正确'; + if (normalized.isEmpty) return L10n.current.inputPhoneRequired; + if (!_regex.hasMatch(normalized)) return L10n.current.inputPhoneInvalid; return null; } } @@ -34,8 +35,8 @@ class Password extends FormzInput { @override String? validator(String value) { - if (value.isEmpty) return '请输入密码'; - if (value.length < 6) return '密码至少 6 个字符'; + if (value.isEmpty) return L10n.current.inputPasswordRequired; + if (value.length < 6) return L10n.current.inputPasswordMin; return null; } } @@ -46,8 +47,10 @@ class VerificationCode extends FormzInput { @override String? validator(String value) { - if (value.isEmpty) return '请输入验证码'; - if (!RegExp(r'^\d{6}$').hasMatch(value)) return '验证码必须是 6 位数字'; + if (value.isEmpty) return L10n.current.inputCodeRequired; + if (!RegExp(r'^\d{6}$').hasMatch(value)) { + return L10n.current.inputCodeInvalid; + } return null; } } diff --git a/apps/lib/shared/widgets/app_pull_refresh_feedback.dart b/apps/lib/shared/widgets/app_pull_refresh_feedback.dart index 688d9c9..46bd4b6 100644 --- a/apps/lib/shared/widgets/app_pull_refresh_feedback.dart +++ b/apps/lib/shared/widgets/app_pull_refresh_feedback.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import '../../core/l10n/l10n.dart'; import '../../core/theme/design_tokens.dart'; import 'app_loading_indicator.dart'; @@ -7,16 +8,17 @@ class AppPullRefreshFeedback extends StatelessWidget { const AppPullRefreshFeedback({ super.key, required this.visible, - this.label = '正在刷新', + this.label, this.margin = const EdgeInsets.only(top: AppSpacing.sm), }); final bool visible; - final String label; + final String? label; final EdgeInsetsGeometry margin; @override Widget build(BuildContext context) { + final resolvedLabel = label ?? context.l10n.commonRefreshing; return IgnorePointer( child: AnimatedOpacity( opacity: visible ? 1 : 0, @@ -44,7 +46,7 @@ class AppPullRefreshFeedback extends StatelessWidget { ), const SizedBox(width: AppSpacing.sm), Text( - label, + resolvedLabel, style: Theme.of(context).textTheme.labelSmall?.copyWith( color: AppColors.slate600, fontWeight: FontWeight.w500, diff --git a/apps/lib/shared/widgets/app_selection_sheet.dart b/apps/lib/shared/widgets/app_selection_sheet.dart index 429005c..28583b0 100644 --- a/apps/lib/shared/widgets/app_selection_sheet.dart +++ b/apps/lib/shared/widgets/app_selection_sheet.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import '../../core/l10n/l10n.dart'; import '../../core/theme/design_tokens.dart'; import 'app_button.dart'; @@ -69,7 +70,7 @@ Future showAppSelectionSheet( child: SizedBox( height: 48, child: AppButton( - text: '取消', + text: context.l10n.commonCancel, isOutlined: true, onPressed: () => Navigator.of(sheetContext).pop(), ), diff --git a/apps/lib/shared/widgets/banner/app_banner.dart b/apps/lib/shared/widgets/banner/app_banner.dart index f1eca0c..fb778e9 100644 --- a/apps/lib/shared/widgets/banner/app_banner.dart +++ b/apps/lib/shared/widgets/banner/app_banner.dart @@ -21,7 +21,7 @@ class AppBanner extends StatelessWidget { Widget build(BuildContext context) { if (!visible) return const SizedBox.shrink(); - final config = ToastTypeConfig.fromType(type); + final config = ToastTypeConfig.fromType(context, type); return Container( width: double.infinity, diff --git a/apps/lib/shared/widgets/chat_bubble.dart b/apps/lib/shared/widgets/chat_bubble.dart index 0ede5c0..5390fbe 100644 --- a/apps/lib/shared/widgets/chat_bubble.dart +++ b/apps/lib/shared/widgets/chat_bubble.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../../../core/l10n/l10n.dart'; import '../../../core/theme/design_tokens.dart'; enum MessageSender { user, ai } @@ -77,11 +78,11 @@ class ChatBubble extends StatelessWidget { String dateStr; if (msgDate == today) { - dateStr = '今天'; + dateStr = L10n.current.chatTimestampToday; } else if (msgDate == today.subtract(const Duration(days: 1))) { - dateStr = '昨天'; + dateStr = L10n.current.chatTimestampYesterday; } else { - dateStr = '${time.month}月${time.day}日'; + dateStr = L10n.current.chatTimestampMonthDay(time.month, time.day); } final timeStr = diff --git a/apps/lib/shared/widgets/confirm_sheet.dart b/apps/lib/shared/widgets/confirm_sheet.dart index bae0c6a..89bbf9f 100644 --- a/apps/lib/shared/widgets/confirm_sheet.dart +++ b/apps/lib/shared/widgets/confirm_sheet.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import '../../core/l10n/l10n.dart'; import '../../core/theme/design_tokens.dart'; import 'app_button.dart'; @@ -7,10 +8,13 @@ Future showConfirmSheet( BuildContext context, { required String title, required String message, - String confirmText = '确认', - String cancelText = '取消', + String? confirmText, + String? cancelText, bool isDestructive = false, }) async { + final l10n = context.l10n; + final resolvedConfirmText = confirmText ?? l10n.commonConfirm; + final resolvedCancelText = cancelText ?? l10n.commonCancel; final result = await showModalBottomSheet( context: context, isScrollControlled: true, @@ -64,7 +68,7 @@ Future showConfirmSheet( borderRadius: BorderRadius.circular(AppRadius.full), ), child: Text( - confirmText, + resolvedConfirmText, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w700, @@ -78,7 +82,7 @@ Future showConfirmSheet( SizedBox( height: 52, child: AppButton( - text: cancelText, + text: resolvedCancelText, isOutlined: true, onPressed: () => Navigator.of(sheetContext).pop(false), ), diff --git a/apps/lib/shared/widgets/destructive_action_sheet.dart b/apps/lib/shared/widgets/destructive_action_sheet.dart index cafdae3..a45da61 100644 --- a/apps/lib/shared/widgets/destructive_action_sheet.dart +++ b/apps/lib/shared/widgets/destructive_action_sheet.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import '../../core/l10n/l10n.dart'; import '../../core/theme/design_tokens.dart'; import 'app_button.dart'; @@ -74,7 +75,7 @@ Future showDestructiveActionSheet( SizedBox( height: 52, child: AppButton( - text: '取消', + text: context.l10n.commonCancel, isOutlined: true, onPressed: () => Navigator.of(sheetContext).pop(false), ), diff --git a/apps/lib/shared/widgets/error_retry_surface.dart b/apps/lib/shared/widgets/error_retry_surface.dart index afee67c..c0dde6e 100644 --- a/apps/lib/shared/widgets/error_retry_surface.dart +++ b/apps/lib/shared/widgets/error_retry_surface.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import '../../core/l10n/l10n.dart'; import '../../core/theme/design_tokens.dart'; import 'app_button.dart'; @@ -30,7 +31,7 @@ class ErrorRetrySurface extends StatelessWidget { style: const TextStyle(color: AppColors.red500), ), const SizedBox(height: AppSpacing.md), - AppButton(text: '重试', onPressed: onRetry), + AppButton(text: context.l10n.commonRetry, onPressed: onRetry), ], ), ), diff --git a/apps/lib/shared/widgets/message_composer.dart b/apps/lib/shared/widgets/message_composer.dart index f477c47..5610030 100644 --- a/apps/lib/shared/widgets/message_composer.dart +++ b/apps/lib/shared/widgets/message_composer.dart @@ -2,6 +2,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:lucide_icons/lucide_icons.dart'; +import '../../core/l10n/l10n.dart'; import '../../core/theme/design_tokens.dart'; import 'app_loading_indicator.dart'; @@ -37,10 +38,10 @@ class MessageComposer extends StatelessWidget { required this.onHoldToSpeakCancel, required this.textInputChild, required this.recordingAnimation, - this.holdToSpeakText = '按住说话', - this.recordingText = '松开发送', - this.transcribingText = '语音识别中...', - this.recordingHintText = '松开发送,上滑取消', + this.holdToSpeakText, + this.recordingText, + this.transcribingText, + this.recordingHintText, this.showRecordingInlineFeedback = true, }); @@ -58,10 +59,10 @@ class MessageComposer extends StatelessWidget { final VoidCallback onHoldToSpeakCancel; final Widget textInputChild; final Widget recordingAnimation; - final String holdToSpeakText; - final String recordingText; - final String transcribingText; - final String recordingHintText; + final String? holdToSpeakText; + final String? recordingText; + final String? transcribingText; + final String? recordingHintText; final bool showRecordingInlineFeedback; bool get _isHoldMode => mode == MessageComposerMode.holdToSpeak; @@ -193,12 +194,20 @@ class MessageComposer extends StatelessWidget { } Widget _buildHoldToSpeakContent() { + final l10n = L10n.current; + final resolvedRecordingText = + recordingText ?? l10n.homeRecordingReleaseSend; + final resolvedRecordingHintText = + recordingHintText ?? l10n.homeRecordingHintReleaseSend; + final resolvedTranscribingText = transcribingText ?? l10n.homeTranscribing; + final resolvedHoldToSpeakText = holdToSpeakText ?? l10n.homeHoldToSpeakText; + if (_isRecording) { if (!showRecordingInlineFeedback) { return Align( alignment: Alignment.center, child: Text( - recordingText, + resolvedRecordingText, style: const TextStyle(color: AppColors.slate700), ), ); @@ -210,12 +219,12 @@ class MessageComposer extends StatelessWidget { recordingAnimation, const SizedBox(height: AppSpacing.xs), Text( - recordingText, + resolvedRecordingText, style: const TextStyle(color: AppColors.slate700), ), const SizedBox(height: AppSpacing.xs), Text( - recordingHintText, + resolvedRecordingHintText, key: messageComposerRecordingHintKey, style: const TextStyle(color: AppColors.slate500), ), @@ -227,7 +236,7 @@ class MessageComposer extends StatelessWidget { return Align( alignment: Alignment.center, child: Text( - transcribingText, + resolvedTranscribingText, style: const TextStyle(color: AppColors.slate500), ), ); @@ -236,7 +245,7 @@ class MessageComposer extends StatelessWidget { return Align( alignment: Alignment.center, child: Text( - holdToSpeakText, + resolvedHoldToSpeakText, style: const TextStyle(color: AppColors.slate500), ), ); diff --git a/apps/lib/shared/widgets/toast/toast.dart b/apps/lib/shared/widgets/toast/toast.dart index 5abe905..0e7f1b9 100644 --- a/apps/lib/shared/widgets/toast/toast.dart +++ b/apps/lib/shared/widgets/toast/toast.dart @@ -91,7 +91,7 @@ class _ToastWidgetState extends State<_ToastWidget> @override Widget build(BuildContext context) { - final config = ToastTypeConfig.fromType(widget.type); + final config = ToastTypeConfig.fromType(context, widget.type); return Positioned( top: MediaQuery.of(context).padding.top + 12, diff --git a/apps/lib/shared/widgets/toast/toast_type_config.dart b/apps/lib/shared/widgets/toast/toast_type_config.dart index cb531bd..0e8c974 100644 --- a/apps/lib/shared/widgets/toast/toast_type_config.dart +++ b/apps/lib/shared/widgets/toast/toast_type_config.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../../../core/l10n/l10n.dart'; import 'toast_type.dart'; import '../../../core/theme/design_tokens.dart'; @@ -19,38 +20,41 @@ class ToastTypeConfig { required this.icon, }); - static ToastTypeConfig fromType(ToastType type) => switch (type) { - ToastType.success => const ToastTypeConfig( - surfaceColor: AppColors.feedbackSuccessSurface, - borderColor: AppColors.feedbackSuccessBorder, - iconColor: AppColors.feedbackSuccessIcon, - textColor: AppColors.feedbackSuccessText, - label: '成功', - icon: Icons.check_circle_outline, - ), - ToastType.warning => const ToastTypeConfig( - surfaceColor: AppColors.feedbackWarningSurface, - borderColor: AppColors.feedbackWarningBorder, - iconColor: AppColors.feedbackWarningIcon, - textColor: AppColors.feedbackWarningText, - label: '提醒', - icon: Icons.warning_amber_rounded, - ), - ToastType.error => const ToastTypeConfig( - surfaceColor: AppColors.feedbackErrorSurface, - borderColor: AppColors.feedbackErrorBorder, - iconColor: AppColors.feedbackErrorIcon, - textColor: AppColors.feedbackErrorText, - label: '错误', - icon: Icons.error_outline, - ), - ToastType.info => const ToastTypeConfig( - surfaceColor: AppColors.feedbackInfoSurface, - borderColor: AppColors.feedbackInfoBorder, - iconColor: AppColors.feedbackInfoIcon, - textColor: AppColors.feedbackInfoText, - label: '提示', - icon: Icons.info_outline, - ), - }; + static ToastTypeConfig fromType(BuildContext context, ToastType type) { + final l10n = context.l10n; + return switch (type) { + ToastType.success => ToastTypeConfig( + surfaceColor: AppColors.feedbackSuccessSurface, + borderColor: AppColors.feedbackSuccessBorder, + iconColor: AppColors.feedbackSuccessIcon, + textColor: AppColors.feedbackSuccessText, + label: l10n.toastLabelSuccess, + icon: Icons.check_circle_outline, + ), + ToastType.warning => ToastTypeConfig( + surfaceColor: AppColors.feedbackWarningSurface, + borderColor: AppColors.feedbackWarningBorder, + iconColor: AppColors.feedbackWarningIcon, + textColor: AppColors.feedbackWarningText, + label: l10n.toastLabelWarning, + icon: Icons.warning_amber_rounded, + ), + ToastType.error => ToastTypeConfig( + surfaceColor: AppColors.feedbackErrorSurface, + borderColor: AppColors.feedbackErrorBorder, + iconColor: AppColors.feedbackErrorIcon, + textColor: AppColors.feedbackErrorText, + label: l10n.toastLabelError, + icon: Icons.error_outline, + ), + ToastType.info => ToastTypeConfig( + surfaceColor: AppColors.feedbackInfoSurface, + borderColor: AppColors.feedbackInfoBorder, + iconColor: AppColors.feedbackInfoIcon, + textColor: AppColors.feedbackInfoText, + label: l10n.toastLabelInfo, + icon: Icons.info_outline, + ), + }; + } } diff --git a/apps/pubspec.yaml b/apps/pubspec.yaml index b965924..8f1daea 100644 --- a/apps/pubspec.yaml +++ b/apps/pubspec.yaml @@ -8,6 +8,8 @@ environment: dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter cupertino_icons: ^1.0.8 equatable: ^2.0.8 flutter_bloc: ^8.1.6 @@ -17,7 +19,7 @@ dependencies: formz: ^0.7.0 get_it: ^7.7.0 lucide_icons: ^0.257.0 - intl: ^0.19.0 + intl: ^0.20.2 shared_preferences: ^2.2.2 json_annotation: ^4.8.1 record: ^6.1.1 @@ -38,6 +40,7 @@ dev_dependencies: flutter_launcher_icons: ^0.14.0 flutter: + generate: true uses-material-design: true assets: - assets/images/ diff --git a/apps/test/core/api/api_exception_test.dart b/apps/test/core/api/api_exception_test.dart deleted file mode 100644 index e086cf0..0000000 --- a/apps/test/core/api/api_exception_test.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:dio/dio.dart'; -import 'package:social_app/core/api/api_exception.dart'; - -void main() { - group('ApiException', () { - test('creates from DioException with 400 status', () { - final dioException = Exception('Bad request'); - final apiException = ApiException.fromDioError(dioException); - - expect(apiException, isA()); - expect(apiException.message, contains('网络错误')); - }); - - test('UnauthorizedException has default message', () { - const exception = UnauthorizedException(); - expect(exception.message, '请重新登录'); - }); - - test('429 returns backend detail message', () { - final dioException = DioException( - requestOptions: RequestOptions(path: '/api/v1/agent/runs'), - response: Response( - requestOptions: RequestOptions(path: '/api/v1/agent/runs'), - statusCode: 429, - data: {'detail': 'Too many SSE connections'}, - ), - ); - - final apiException = ApiException.fromDioError(dioException); - - expect(apiException.statusCode, 429); - expect(apiException.message, 'Too many SSE connections'); - }); - - test('429 parses detail from string json body', () { - final dioException = DioException( - requestOptions: RequestOptions(path: '/api/v1/agent/runs'), - response: Response( - requestOptions: RequestOptions(path: '/api/v1/agent/runs'), - statusCode: 429, - data: '{"detail":"Too many SSE connections"}', - ), - ); - - final apiException = ApiException.fromDioError(dioException); - - expect(apiException.statusCode, 429); - expect(apiException.message, 'Too many SSE connections'); - }); - }); -} diff --git a/apps/test/core/api/api_interceptor_test.dart b/apps/test/core/api/api_interceptor_test.dart deleted file mode 100644 index 81ddf87..0000000 --- a/apps/test/core/api/api_interceptor_test.dart +++ /dev/null @@ -1,143 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:social_app/core/api/api_interceptor.dart'; -import 'package:social_app/core/storage/token_storage.dart'; - -class MockTokenStorage extends Mock implements TokenStorage {} - -class MockErrorInterceptorHandler extends Mock - implements ErrorInterceptorHandler {} - -void main() { - late MockTokenStorage tokenStorage; - late MockErrorInterceptorHandler handler; - late ApiInterceptor interceptor; - - setUpAll(() { - registerFallbackValue( - DioException(requestOptions: RequestOptions(path: '/fallback')), - ); - registerFallbackValue( - Response(requestOptions: RequestOptions(path: '/fallback')), - ); - }); - - setUp(() { - tokenStorage = MockTokenStorage(); - handler = MockErrorInterceptorHandler(); - interceptor = ApiInterceptor( - tokenStorage: tokenStorage, - dio: Dio(), - refreshFailureCooldown: const Duration(milliseconds: 80), - ); - when(() => handler.next(any())).thenReturn(null); - when(() => handler.resolve(any())).thenReturn(null); - when(() => tokenStorage.getAccessToken()).thenAnswer((_) async => null); - }); - - DioException _unauthorized(String path, {bool withAuthHeader = false}) { - final requestOptions = RequestOptions( - path: path, - headers: withAuthHeader - ? {'Authorization': 'Bearer expired'} - : null, - ); - return DioException( - requestOptions: requestOptions, - response: Response( - requestOptions: requestOptions, - statusCode: 401, - ), - type: DioExceptionType.badResponse, - ); - } - - test('401并发请求仅触发一次refresh', () async { - var refreshCalls = 0; - interceptor.onTokenRefresh = () async { - refreshCalls += 1; - await Future.delayed(const Duration(milliseconds: 20)); - return false; - }; - - interceptor.onError(_unauthorized('/api/v1/agent/history'), handler); - interceptor.onError(_unauthorized('/api/v1/agent/history'), handler); - await Future.delayed(const Duration(milliseconds: 60)); - - expect(refreshCalls, 1); - }); - - test('refresh接口401不应再次触发refresh', () async { - var refreshCalls = 0; - interceptor.onTokenRefresh = () async { - refreshCalls += 1; - return false; - }; - - interceptor.onError( - _unauthorized('/api/v1/auth/sessions/refresh'), - handler, - ); - await Future.delayed(const Duration(milliseconds: 20)); - - expect(refreshCalls, 0); - }); - - test('refresh接口带query时401也不应再次触发refresh', () async { - var refreshCalls = 0; - interceptor.onTokenRefresh = () async { - refreshCalls += 1; - return false; - }; - - interceptor.onError( - _unauthorized('/api/v1/auth/sessions/refresh?source=boot'), - handler, - ); - await Future.delayed(const Duration(milliseconds: 20)); - - expect(refreshCalls, 0); - }); - - test('refresh失败冷却期内不应重复触发refresh', () async { - var refreshCalls = 0; - interceptor.onTokenRefresh = () async { - refreshCalls += 1; - return false; - }; - - interceptor.onError(_unauthorized('/api/v1/agent/history'), handler); - await Future.delayed(const Duration(milliseconds: 20)); - interceptor.onError(_unauthorized('/api/v1/agent/history'), handler); - await Future.delayed(const Duration(milliseconds: 20)); - - expect(refreshCalls, 1); - }); - - test('并发401刷新失败仅触发一次auth failure回调', () async { - var refreshCalls = 0; - var authFailureCalls = 0; - interceptor.onTokenRefresh = () async { - refreshCalls += 1; - await Future.delayed(const Duration(milliseconds: 20)); - return false; - }; - interceptor.onAuthFailure = () async { - authFailureCalls += 1; - }; - - interceptor.onError( - _unauthorized('/api/v1/agent/history', withAuthHeader: true), - handler, - ); - interceptor.onError( - _unauthorized('/api/v1/agent/history', withAuthHeader: true), - handler, - ); - await Future.delayed(const Duration(milliseconds: 80)); - - expect(refreshCalls, 1); - expect(authFailureCalls, 1); - }); -} diff --git a/apps/test/core/cache/cache_invalidator_test.dart b/apps/test/core/cache/cache_invalidator_test.dart deleted file mode 100644 index a41635c..0000000 --- a/apps/test/core/cache/cache_invalidator_test.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:social_app/core/cache/cache_invalidator.dart'; - -void main() { - test('invalidate calendar day should also invalidate month key', () { - final inv = CacheInvalidator(); - inv.invalidateCalendarDay(DateTime(2026, 3, 20)); - expect(inv.wasInvalidated('calendar:day:2026-03-20'), true); - expect(inv.wasInvalidated('calendar:month:2026-03'), true); - }); -} diff --git a/apps/test/core/cache/cache_policy_test.dart b/apps/test/core/cache/cache_policy_test.dart deleted file mode 100644 index 8f3db23..0000000 --- a/apps/test/core/cache/cache_policy_test.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:social_app/core/cache/cache_policy.dart'; - -void main() { - test('soft expired should allow stale read with background refresh', () { - final now = DateTime(2026, 3, 20, 12); - final policy = CachePolicy( - softTtl: const Duration(minutes: 2), - hardTtl: const Duration(minutes: 30), - minRefreshInterval: const Duration(minutes: 1), - ); - - final fetchedAt = now.subtract(const Duration(minutes: 3)); - final decision = policy.evaluate(now: now, fetchedAt: fetchedAt); - expect(decision.canUseCached, true); - expect(decision.shouldRefreshInBackground, true); - expect(decision.mustBlockForNetwork, false); - }); -} diff --git a/apps/test/core/cache/cache_refresh_coordinator_test.dart b/apps/test/core/cache/cache_refresh_coordinator_test.dart deleted file mode 100644 index fcd361c..0000000 --- a/apps/test/core/cache/cache_refresh_coordinator_test.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:social_app/core/cache/cache_refresh_coordinator.dart'; - -void main() { - test('resume should trigger refresh only when min interval elapsed', () { - var calls = 0; - var now = DateTime(2026, 3, 20, 10, 0); - - final coordinator = CacheRefreshCoordinator( - minInterval: const Duration(minutes: 5), - onRefresh: () => calls += 1, - now: () => now, - ); - - coordinator.didChangeAppLifecycleState(AppLifecycleState.resumed); - expect(calls, 1); - - now = DateTime(2026, 3, 20, 10, 3); - coordinator.didChangeAppLifecycleState(AppLifecycleState.resumed); - expect(calls, 1); - - now = DateTime(2026, 3, 20, 10, 6); - coordinator.didChangeAppLifecycleState(AppLifecycleState.resumed); - expect(calls, 2); - }); -} diff --git a/apps/test/core/cache/hybrid_cache_store_test.dart b/apps/test/core/cache/hybrid_cache_store_test.dart deleted file mode 100644 index 770a6e5..0000000 --- a/apps/test/core/cache/hybrid_cache_store_test.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:social_app/core/cache/hybrid_cache_store.dart'; -import 'package:social_app/core/cache/memory_cache_store.dart'; -import 'package:social_app/core/cache/persistent_cache_store.dart'; - -void main() { - test('same key concurrent load should execute loader once', () async { - var calls = 0; - final store = HybridCacheStore( - memory: MemoryCacheStore(), - persistent: PersistentCacheStore(), - ); - - Future loader() async { - calls += 1; - await Future.delayed(const Duration(milliseconds: 20)); - return 'ok'; - } - - await Future.wait([ - store.getOrLoad('k', loader: loader), - store.getOrLoad('k', loader: loader), - ]); - - expect(calls, 1); - }); -} diff --git a/apps/test/core/notifications/ios_notification_payload_bridge_test.dart b/apps/test/core/notifications/ios_notification_payload_bridge_test.dart deleted file mode 100644 index 6965bdd..0000000 --- a/apps/test/core/notifications/ios_notification_payload_bridge_test.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:social_app/core/notifications/ios_notification_payload_bridge.dart'; -import 'dart:convert'; - -void main() { - group('IOSNotificationPayloadBridge', () { - test('启动时读取待处理的 notification payload', () async { - SharedPreferences.setMockInitialValues({ - 'pending_notification_payload': jsonEncode({ - 'eventId': 'evt_123', - 'title': 'Test Event', - 'startAt': '2026-03-20T10:00:00Z', - 'mode': 'single', - }), - }); - - final prefs = await SharedPreferences.getInstance(); - final bridge = IOSNotificationPayloadBridge(prefs); - final payload = await bridge.getPendingPayload(); - - expect(payload?.eventId, 'evt_123'); - expect(payload?.title, 'Test Event'); - }); - - test('处理完成后清理 UserDefaults', () async { - SharedPreferences.setMockInitialValues({ - 'pending_notification_payload': jsonEncode({ - 'eventId': 'evt_123', - 'title': 'Test Event', - 'startAt': '2026-03-20T10:00:00Z', - 'mode': 'single', - }), - }); - - final prefs = await SharedPreferences.getInstance(); - final bridge = IOSNotificationPayloadBridge(prefs); - await bridge.clearPendingPayload(); - - final remaining = prefs.getString('pending_notification_payload'); - expect(remaining, isNull); - }); - }); -} diff --git a/apps/test/core/router/app_routes_test.dart b/apps/test/core/router/app_routes_test.dart deleted file mode 100644 index 2f8b790..0000000 --- a/apps/test/core/router/app_routes_test.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:social_app/core/router/app_routes.dart'; - -void main() { - test('calendar and todo route builders generate concrete paths', () { - expect( - AppRoutes.calendarEventEdit('evt_123'), - '/calendar/events/evt_123/edit', - ); - expect( - AppRoutes.calendarEventShare('evt_123'), - '/calendar/events/evt_123/share', - ); - expect(AppRoutes.todoEdit('todo_123'), '/todo/todo_123/edit'); - }); -} diff --git a/apps/test/core/schemas/ui_schema_test.dart b/apps/test/core/schemas/ui_schema_test.dart deleted file mode 100644 index add0bc5..0000000 --- a/apps/test/core/schemas/ui_schema_test.dart +++ /dev/null @@ -1,105 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:social_app/core/schemas/ui_schema.dart'; - -void main() { - group('ui_schema protocol stability', () { - test('UiSchemaDocument.fromJson keeps enum fallback defaults', () { - final doc = UiSchemaDocument.fromJson({ - 'version': '1.0', - 'schemaType': 'unknown_type', - 'status': 'unknown_status', - 'nodes': const [], - }); - - expect(doc.schemaType, SchemaType.toolResult); - expect(doc.status, UiStatus.info); - }); - - test('actionSpecFromJson covers known and unknown branches', () { - final navigation = actionSpecFromJson({ - 'type': 'navigation', - 'path': '/calendar/dayweek', - 'params': {'from': 'home'}, - }); - expect(navigation, isA()); - - final unknown = actionSpecFromJson({'type': 'not_supported'}); - expect(unknown, isA()); - expect((unknown as EventAction).event, 'unknown'); - }); - - test('UiNode.fromJson returns text fallback for unknown node type', () { - final node = UiNode.fromJson({'type': 'mystery'}); - - expect(node, isA()); - expect((node as UiTextNode).content, 'Unknown node type: mystery'); - }); - - test( - 'buildSuccessDocument and buildErrorDocument keep status semantics', - () { - final success = buildSuccessDocument(const [ - UiTextNode(content: 'ok'), - ], schemaType: SchemaType.agentResponse); - final error = buildErrorDocument(const [UiTextNode(content: 'bad')]); - - expect(success.status, UiStatus.success); - expect(success.schemaType, SchemaType.agentResponse); - expect(success.locale, 'zh-CN'); - - expect(error.status, UiStatus.error); - expect(error.schemaType, SchemaType.toolResult); - expect(error.version, '1.0'); - }, - ); - - test('UiSchemaDocument round-trip keeps critical fields stable', () { - final original = UiSchemaDocument( - version: '1.0', - schemaType: SchemaType.toolResult, - docId: 'doc_1', - timestamp: '2026-03-19T10:00:00Z', - locale: 'zh-CN', - status: UiStatus.success, - renderer: const RendererConfig(theme: RendererTheme.light), - meta: const DocumentMeta(requestId: 'req_1', toolId: 'tool_1'), - nodes: const [ - UiContainerNode( - direction: ContainerDirection.vertical, - children: [ - UiTextNode(content: 'hello', format: TextFormat.markdown), - ], - ), - ], - ); - - final encoded = original.toJson(); - final decoded = UiSchemaDocument.fromJson(encoded); - - expect(decoded.version, '1.0'); - expect(decoded.schemaType, SchemaType.toolResult); - expect(decoded.docId, 'doc_1'); - expect(decoded.status, UiStatus.success); - expect(decoded.renderer?.theme, RendererTheme.light); - expect(decoded.nodes, hasLength(1)); - expect(decoded.nodes.first, isA()); - }); - - test('toJson omits nullable fields as before', () { - const action = UiAction( - id: 'a1', - label: 'open', - action: NavigateAction(path: '/settings'), - ); - - final json = action.toJson(); - - expect(json['id'], 'a1'); - expect(json['label'], 'open'); - expect(json.containsKey('icon'), false); - expect(json.containsKey('style'), false); - expect(json.containsKey('confirm'), false); - expect((json['action'] as Map)['path'], '/settings'); - }); - }); -} diff --git a/apps/test/core/storage/token_storage_test.dart b/apps/test/core/storage/token_storage_test.dart deleted file mode 100644 index ea79f96..0000000 --- a/apps/test/core/storage/token_storage_test.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:social_app/core/storage/token_storage.dart'; - -class MockTokenStorage extends Mock implements TokenStorage {} - -void main() { - late TokenStorage storage; - - setUp(() { - storage = MockTokenStorage(); - }); - - group('TokenStorage', () { - test('saves and retrieves access token', () async { - when( - () => storage.getAccessToken(), - ).thenAnswer((_) async => 'test_access'); - when( - () => - storage.saveTokens(access: 'test_access', refresh: 'test_refresh'), - ).thenAnswer((_) async {}); - - await storage.saveTokens(access: 'test_access', refresh: 'test_refresh'); - final token = await storage.getAccessToken(); - - expect(token, 'test_access'); - verify( - () => - storage.saveTokens(access: 'test_access', refresh: 'test_refresh'), - ).called(1); - }); - - test('clear removes all tokens', () async { - when(() => storage.clear()).thenAnswer((_) async {}); - when(() => storage.getAccessToken()).thenAnswer((_) async => null); - - await storage.clear(); - final token = await storage.getAccessToken(); - - expect(token, isNull); - }); - }); -} diff --git a/apps/test/features/auth/data/auth_repository_test.dart b/apps/test/features/auth/data/auth_repository_test.dart deleted file mode 100644 index 542c264..0000000 --- a/apps/test/features/auth/data/auth_repository_test.dart +++ /dev/null @@ -1,125 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:social_app/features/auth/data/auth_api.dart'; -import 'package:social_app/features/auth/data/auth_repository_impl.dart'; -import 'package:social_app/features/auth/data/models/signup_request.dart'; -import 'package:social_app/features/auth/data/models/login_request.dart'; -import 'package:social_app/features/auth/data/models/auth_response.dart'; -import 'package:social_app/core/storage/token_storage.dart'; - -class MockAuthApi extends Mock implements AuthApi {} - -class MockTokenStorage extends Mock implements TokenStorage {} - -void main() { - late AuthRepositoryImpl repository; - late MockAuthApi mockApi; - late MockTokenStorage mockStorage; - - setUp(() { - mockApi = MockAuthApi(); - mockStorage = MockTokenStorage(); - repository = AuthRepositoryImpl(api: mockApi, tokenStorage: mockStorage); - registerFallbackValue(const OtpSendRequest(phone: '')); - registerFallbackValue(const LoginRequest(phone: '', token: '')); - registerFallbackValue(const LogoutRequest(refreshToken: '')); - registerFallbackValue(const RefreshRequest(refreshToken: '')); - }); - - group('AuthRepositoryImpl', () { - test('sendOtp calls api', () async { - when(() => mockApi.sendOtp(any())).thenAnswer((_) async {}); - - await repository.sendOtp('+8613812345678'); - - verify(() => mockApi.sendOtp(any())).called(1); - }); - - test('createPhoneSession calls api and saves tokens', () async { - when(() => mockApi.createPhoneSession(any())).thenAnswer( - (_) async => AuthResponse( - accessToken: 'access_token', - refreshToken: 'refresh_token', - expiresIn: 3600, - tokenType: 'bearer', - user: const AuthUser(id: '123', phone: '+8613812345678'), - ), - ); - when( - () => mockStorage.saveTokens( - access: any(named: 'access'), - refresh: any(named: 'refresh'), - ), - ).thenAnswer((_) async {}); - - final result = await repository.createPhoneSession( - phone: '+8613812345678', - token: '123456', - ); - - expect(result.accessToken, 'access_token'); - verify( - () => mockStorage.saveTokens( - access: 'access_token', - refresh: 'refresh_token', - ), - ).called(1); - }); - - test( - 'deleteSession calls api with refresh token and clears storage', - () async { - when( - () => mockStorage.getRefreshToken(), - ).thenAnswer((_) async => 'refresh_token'); - when(() => mockApi.deleteSession(any())).thenAnswer((_) async {}); - when(() => mockStorage.clear()).thenAnswer((_) async {}); - - await repository.deleteSession(); - - verify(() => mockApi.deleteSession(any())).called(1); - verify(() => mockStorage.clear()).called(1); - }, - ); - - test( - 'clearSessionLocalOnly clears local tokens without api revoke', - () async { - when(() => mockStorage.clear()).thenAnswer((_) async {}); - - await repository.clearSessionLocalOnly(); - - verify(() => mockStorage.clear()).called(1); - verifyNever(() => mockApi.deleteSession(any())); - }, - ); - - test('refreshSession saves new tokens', () async { - when(() => mockApi.refreshSession(any())).thenAnswer( - (_) async => AuthResponse( - accessToken: 'new_access', - refreshToken: 'new_refresh', - expiresIn: 3600, - tokenType: 'bearer', - user: const AuthUser(id: '123', phone: '+8613812345678'), - ), - ); - when( - () => mockStorage.saveTokens( - access: any(named: 'access'), - refresh: any(named: 'refresh'), - ), - ).thenAnswer((_) async {}); - - final result = await repository.refreshSession('old_refresh'); - - expect(result.accessToken, 'new_access'); - verify( - () => mockStorage.saveTokens( - access: 'new_access', - refresh: 'new_refresh', - ), - ).called(1); - }); - }); -} diff --git a/apps/test/features/auth/data/models/auth_models_test.dart b/apps/test/features/auth/data/models/auth_models_test.dart deleted file mode 100644 index f77848e..0000000 --- a/apps/test/features/auth/data/models/auth_models_test.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:social_app/features/auth/data/models/signup_request.dart'; -import 'package:social_app/features/auth/data/models/login_request.dart'; -import 'package:social_app/features/auth/data/models/auth_response.dart'; - -void main() { - group('OtpSendRequest', () { - test('serializes e164 phone to JSON', () { - final request = OtpSendRequest(phone: '+14155552671'); - - final json = request.toJson(); - - expect(json['phone'], '+14155552671'); - }); - - test('normalizes 00 prefix to plus', () { - final request = OtpSendRequest(phone: '0014155552671'); - - final json = request.toJson(); - - expect(json['phone'], '+14155552671'); - }); - }); - - group('LoginRequest', () { - test('serializes e164 to JSON', () { - final request = LoginRequest(phone: '+14155552671', token: '123456'); - - final json = request.toJson(); - - expect(json['phone'], '+14155552671'); - expect(json['token'], '123456'); - }); - }); - - group('AuthResponse', () { - test('parses from JSON', () { - final json = { - 'access_token': 'test_access', - 'refresh_token': 'test_refresh', - 'expires_in': 3600, - 'token_type': 'bearer', - 'user': {'id': '123', 'phone': '+8613812345678'}, - }; - - final response = AuthResponse.fromJson(json); - - expect(response.accessToken, 'test_access'); - expect(response.refreshToken, 'test_refresh'); - expect(response.expiresIn, 3600); - expect(response.user.id, '123'); - expect(response.user.phone, '+8613812345678'); - }); - }); -} diff --git a/apps/test/features/auth/presentation/bloc/auth_bloc_test.dart b/apps/test/features/auth/presentation/bloc/auth_bloc_test.dart deleted file mode 100644 index 1795f05..0000000 --- a/apps/test/features/auth/presentation/bloc/auth_bloc_test.dart +++ /dev/null @@ -1,152 +0,0 @@ -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:social_app/features/auth/data/auth_repository.dart'; -import 'package:social_app/features/auth/data/models/auth_response.dart'; -import 'package:social_app/features/auth/presentation/bloc/auth_bloc.dart'; -import 'package:social_app/features/auth/presentation/bloc/auth_event.dart'; -import 'package:social_app/features/auth/presentation/bloc/auth_state.dart'; - -class MockAuthRepository extends Mock implements AuthRepository {} - -void main() { - late AuthBloc authBloc; - late MockAuthRepository mockRepository; - - setUp(() { - mockRepository = MockAuthRepository(); - authBloc = AuthBloc(mockRepository); - }); - - tearDown(() { - authBloc.close(); - }); - - group('AuthBloc', () { - blocTest( - 'emits [AuthLoading, AuthUnauthenticated] when AuthStarted and no refresh token', - build: () { - when( - () => mockRepository.getRefreshToken(), - ).thenAnswer((_) async => null); - return authBloc; - }, - act: (bloc) => bloc.add(AuthStarted()), - expect: () => [ - AuthLoading(), - const AuthUnauthenticated(reason: AuthUnauthenticatedReason.signedOut), - ], - ); - - blocTest( - 'emits [AuthLoading, AuthAuthenticated] when AuthStarted with valid refresh token', - build: () { - when( - () => mockRepository.getRefreshToken(), - ).thenAnswer((_) async => 'valid_refresh'); - when(() => mockRepository.refreshSession('valid_refresh')).thenAnswer( - (_) async => AuthResponse( - accessToken: 'new_access', - refreshToken: 'new_refresh', - expiresIn: 3600, - tokenType: 'bearer', - user: const AuthUser(id: '123', phone: '+8613812345678'), - ), - ); - return authBloc; - }, - act: (bloc) => bloc.add(AuthStarted()), - expect: () => [AuthLoading(), isA()], - ); - - blocTest( - 'emits [AuthLoading, AuthUnauthenticated] when refresh token expired', - build: () { - when( - () => mockRepository.getRefreshToken(), - ).thenAnswer((_) async => 'expired_refresh'); - when( - () => mockRepository.refreshSession('expired_refresh'), - ).thenThrow(Exception('Invalid refresh token')); - when( - () => mockRepository.clearSessionLocalOnly(), - ).thenAnswer((_) async {}); - return authBloc; - }, - act: (bloc) => bloc.add(AuthStarted()), - expect: () => [ - AuthLoading(), - const AuthUnauthenticated( - reason: AuthUnauthenticatedReason.startupRecoveryFailed, - ), - ], - ); - - blocTest( - 'emits startupRecoveryFailed when storage read throws', - build: () { - when( - () => mockRepository.getRefreshToken(), - ).thenThrow(Exception('storage failed')); - when( - () => mockRepository.clearSessionLocalOnly(), - ).thenAnswer((_) async {}); - return authBloc; - }, - act: (bloc) => bloc.add(AuthStarted()), - expect: () => [ - AuthLoading(), - const AuthUnauthenticated( - reason: AuthUnauthenticatedReason.startupRecoveryFailed, - ), - ], - ); - - blocTest( - 'emits [AuthAuthenticated] when AuthLoggedIn', - build: () => authBloc, - act: (bloc) => bloc.add( - AuthLoggedIn( - user: const AuthUser(id: '1', phone: '+8613812345678'), - ), - ), - expect: () => [isA()], - ); - - blocTest( - 'emits [AuthUnauthenticated] when AuthLoggedOut', - build: () { - when(() => mockRepository.deleteSession()).thenAnswer((_) async {}); - return authBloc; - }, - seed: () => AuthAuthenticated( - user: const AuthUser(id: '1', phone: '+8613812345678'), - ), - act: (bloc) => bloc.add(AuthLoggedOut()), - expect: () => [ - const AuthUnauthenticated(reason: AuthUnauthenticatedReason.signedOut), - ], - ); - - blocTest( - 'emits expired unauthenticated when session invalidated', - build: () { - when( - () => mockRepository.clearSessionLocalOnly(), - ).thenAnswer((_) async {}); - return authBloc; - }, - seed: () => AuthAuthenticated( - user: const AuthUser(id: '1', phone: '+8613812345678'), - ), - act: (bloc) => bloc.add( - const AuthSessionInvalidated( - source: AuthInvalidationSource.unauthorized401, - ), - ), - expect: () => [ - const AuthUnauthenticated(reason: AuthUnauthenticatedReason.expired), - ], - ); - }); -} diff --git a/apps/test/features/auth/presentation/cubits/login_cubit_test.dart b/apps/test/features/auth/presentation/cubits/login_cubit_test.dart deleted file mode 100644 index fab811e..0000000 --- a/apps/test/features/auth/presentation/cubits/login_cubit_test.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'package:bloc_test/bloc_test.dart'; -import 'package:fake_async/fake_async.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:formz/formz.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:social_app/features/auth/data/auth_repository.dart'; -import 'package:social_app/features/auth/presentation/cubits/login_cubit.dart'; - -class MockAuthRepository extends Mock implements AuthRepository {} - -void main() { - late LoginCubit cubit; - late MockAuthRepository mockRepository; - - setUp(() { - mockRepository = MockAuthRepository(); - cubit = LoginCubit(mockRepository); - }); - - tearDown(() { - cubit.close(); - }); - - group('LoginCubit', () { - test('initial state has pure status', () { - expect(cubit.state.status, FormzSubmissionStatus.initial); - }); - - blocTest( - 'phoneChanged updates phone', - build: () => cubit, - act: (c) => c.phoneChanged('+8613812345678'), - expect: () => [isA()], - ); - - blocTest( - 'codeChanged updates code', - build: () => cubit, - act: (c) => c.codeChanged('123456'), - expect: () => [isA()], - ); - - test('sendCode success starts 60s cooldown', () { - when(() => mockRepository.sendOtp(any())).thenAnswer((_) async {}); - - fakeAsync((async) { - cubit.phoneChanged('13812345678'); - - cubit.sendCode(); - async.flushMicrotasks(); - - expect(cubit.state.resendCooldownSeconds, 60); - - async.elapse(const Duration(seconds: 1)); - expect(cubit.state.resendCooldownSeconds, 59); - - async.elapse(const Duration(seconds: 59)); - expect(cubit.state.resendCooldownSeconds, 0); - }); - }); - - test('sendCode is blocked during cooldown', () async { - when(() => mockRepository.sendOtp(any())).thenAnswer((_) async {}); - cubit.phoneChanged('13812345678'); - - final first = await cubit.sendCode(); - final second = await cubit.sendCode(); - - expect(first, isTrue); - expect(second, isFalse); - verify(() => mockRepository.sendOtp(any())).called(1); - }); - - test('phone change resets cooldown and code state', () { - when(() => mockRepository.sendOtp(any())).thenAnswer((_) async {}); - - fakeAsync((async) { - cubit.phoneChanged('13812345678'); - cubit.codeChanged('123456'); - cubit.sendCode(); - async.flushMicrotasks(); - - expect(cubit.state.resendCooldownSeconds, 60); - expect(cubit.state.codeSent, isTrue); - - cubit.phoneChanged('14155552671'); - - expect(cubit.state.resendCooldownSeconds, 0); - expect(cubit.state.codeSent, isFalse); - expect(cubit.state.code.value, ''); - }); - }); - }); -} diff --git a/apps/test/features/calendar/data/services/calendar_repository_test.dart b/apps/test/features/calendar/data/services/calendar_repository_test.dart deleted file mode 100644 index cf969ae..0000000 --- a/apps/test/features/calendar/data/services/calendar_repository_test.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:social_app/core/cache/cache_entry.dart'; -import 'package:social_app/core/cache/cache_policy.dart'; -import 'package:social_app/core/cache/hybrid_cache_store.dart'; -import 'package:social_app/core/cache/memory_cache_store.dart'; -import 'package:social_app/core/cache/persistent_cache_store.dart'; -import 'package:social_app/features/calendar/data/models/schedule_item_model.dart'; -import 'package:social_app/features/calendar/data/services/calendar_repository.dart'; - -void main() { - test( - 'getDayEvents returns cache immediately and refreshes in background', - () async { - final store = HybridCacheStore( - memory: MemoryCacheStore(), - persistent: PersistentCacheStore(), - ); - final date = DateTime(2026, 3, 20); - final key = CalendarRepository.dayKey(date); - await store.persistent.write>>( - key, - CacheEntry( - value: [ - ScheduleItemModel( - id: 'evt_cached', - ownerId: 'owner_1', - title: 'cached', - startAt: DateTime(2026, 3, 20, 10), - endAt: DateTime(2026, 3, 20, 11), - status: ScheduleStatus.active, - ), - ], - fetchedAt: DateTime(2026, 3, 20, 11, 0), - ), - ); - - var remoteCalls = 0; - final repository = CalendarRepository( - store: store, - now: () => DateTime(2026, 3, 20, 11, 5), - policy: const CachePolicy( - softTtl: Duration(minutes: 2), - hardTtl: Duration(minutes: 30), - minRefreshInterval: Duration(minutes: 1), - ), - loadDayFromRemote: (_) async { - remoteCalls += 1; - return const []; - }, - loadMonthFromRemote: (start, end) async => const [], - ); - - final result = await repository.getDayEvents(date); - await Future.delayed(const Duration(milliseconds: 10)); - - expect(result.first.id, 'evt_cached'); - expect(remoteCalls, 1); - }, - ); -} diff --git a/apps/test/features/calendar/reminders/models/reminder_payload_test.dart b/apps/test/features/calendar/reminders/models/reminder_payload_test.dart deleted file mode 100644 index 2478296..0000000 --- a/apps/test/features/calendar/reminders/models/reminder_payload_test.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:social_app/features/calendar/reminders/models/reminder_payload.dart'; - -void main() { - group('ReminderPayload', () { - test('round-trips single payload', () { - final payload = ReminderPayload( - eventId: 'evt_1', - title: 'Daily Sync', - startAt: DateTime.parse('2026-03-18T16:00:00+08:00'), - endAt: DateTime.parse('2026-03-18T17:00:00+08:00'), - timezone: 'Asia/Shanghai', - location: 'A101', - notes: 'Bring docs', - color: '#3B82F6', - mode: ReminderPayloadMode.single, - aggregateIds: const [], - version: 1, - ); - - final decoded = ReminderPayload.fromJson(payload.toJson()); - expect(decoded, payload); - }); - - test('round-trips aggregate payload', () { - final payload = ReminderPayload( - eventId: 'evt_group', - title: 'Overlap Reminder', - startAt: DateTime.parse('2026-03-18T16:00:00+08:00'), - timezone: 'Asia/Shanghai', - mode: ReminderPayloadMode.aggregate, - aggregateIds: const ['evt_1', 'evt_2'], - version: 1, - ); - - final decoded = ReminderPayload.fromJson(payload.toJson()); - expect(decoded.mode, ReminderPayloadMode.aggregate); - expect(decoded.aggregateIds, const ['evt_1', 'evt_2']); - expect(decoded, payload); - }); - }); -} diff --git a/apps/test/features/calendar/reminders/reminder_action_executor_test.dart b/apps/test/features/calendar/reminders/reminder_action_executor_test.dart deleted file mode 100644 index 9b7ac7a..0000000 --- a/apps/test/features/calendar/reminders/reminder_action_executor_test.dart +++ /dev/null @@ -1,120 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:social_app/core/notifications/local_notification_service.dart'; -import 'package:social_app/features/calendar/data/models/schedule_item_model.dart'; -import 'package:social_app/features/calendar/data/services/calendar_service.dart'; -import 'package:social_app/features/calendar/reminders/models/reminder_action.dart'; -import 'package:social_app/features/calendar/reminders/models/reminder_payload.dart'; -import 'package:social_app/features/calendar/reminders/reminder_action_executor.dart'; - -class MockCalendarService extends Mock implements CalendarService {} - -class MockLocalNotificationService extends Mock - implements LocalNotificationService {} - -void main() { - late MockCalendarService calendarService; - late MockLocalNotificationService notificationService; - late ReminderActionExecutor executor; - - setUp(() { - calendarService = MockCalendarService(); - notificationService = MockLocalNotificationService(); - executor = ReminderActionExecutor( - calendarService: calendarService, - notificationService: notificationService, - ); - }); - - test('archive archives remotely and cancels local reminder', () async { - when( - () => notificationService.cancelEventReminder('evt_1'), - ).thenAnswer((_) async {}); - when( - () => calendarService.archiveEvent('evt_1'), - ).thenAnswer((_) async => null); - - await executor.handleAction( - action: ReminderAction.archive, - payload: ReminderPayload( - eventId: 'evt_1', - title: 'sync', - startAt: DateTime.parse('2026-03-18T16:00:00+08:00'), - timezone: 'Asia/Shanghai', - ), - ); - - verify(() => notificationService.cancelEventReminder('evt_1')).called(1); - verify(() => calendarService.archiveEvent('evt_1')).called(1); - }); - - test('snooze reschedules +10m when event not expired', () async { - final now = DateTime.now(); - final event = ScheduleItemModel( - id: 'evt_1', - ownerId: 'u1', - title: 'sync', - startAt: now.add(const Duration(minutes: 1)), - endAt: now.add(const Duration(hours: 1)), - metadata: ScheduleMetadata(reminderMinutes: 15), - ); - when( - () => calendarService.getEventById('evt_1'), - ).thenAnswer((_) async => event); - when( - () => notificationService.scheduleReminderAt(event, any()), - ).thenAnswer((_) async {}); - - await executor.handleAction( - action: ReminderAction.snooze10m, - payload: ReminderPayload( - eventId: 'evt_1', - title: 'sync', - startAt: event.startAt, - endAt: event.endAt, - timezone: 'Asia/Shanghai', - ), - ); - - verify( - () => notificationService.scheduleReminderAt(event, any()), - ).called(1); - verifyNever(() => calendarService.archiveEvent(any())); - }); - - test('fromValue throws on unknown action', () { - expect( - () => ReminderAction.fromValue('unknown_action'), - throwsA(isA()), - ); - }); - - test( - 'aggregate action falls back to eventId when aggregateIds is empty', - () async { - when( - () => notificationService.cancelEventReminder('evt_fallback'), - ).thenAnswer((_) async {}); - when( - () => calendarService.archiveEvent('evt_fallback'), - ).thenAnswer((_) async => null); - - await executor.handleAction( - action: ReminderAction.archive, - payload: ReminderPayload( - eventId: 'evt_fallback', - title: 'sync', - startAt: DateTime.parse('2026-03-18T16:00:00+08:00'), - timezone: 'Asia/Shanghai', - mode: ReminderPayloadMode.aggregate, - aggregateIds: const [], - ), - ); - - verify( - () => notificationService.cancelEventReminder('evt_fallback'), - ).called(1); - verify(() => calendarService.archiveEvent('evt_fallback')).called(1); - }, - ); -} diff --git a/apps/test/features/calendar/reminders/reminder_notification_callbacks_test.dart b/apps/test/features/calendar/reminders/reminder_notification_callbacks_test.dart deleted file mode 100644 index 9ef90d8..0000000 --- a/apps/test/features/calendar/reminders/reminder_notification_callbacks_test.dart +++ /dev/null @@ -1,142 +0,0 @@ -import 'dart:io'; - -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:social_app/core/notifications/reminder_notification_callbacks.dart'; - -void main() { - setUp(() async { - SharedPreferences.setMockInitialValues({}); - await ReminderNotificationCallbacks.resetForTest(); - }); - - test('contains top-level vm entry-point background callback', () async { - final source = await File( - 'lib/core/notifications/reminder_notification_callbacks.dart', - ).readAsString(); - - expect(source, contains("@pragma('vm:entry-point')")); - expect(source, contains('Future reminderNotificationTapBackground(')); - }); - - test( - 'dispatches foreground and background responses to bound handler', - () async { - final handledIds = []; - await ReminderNotificationCallbacks.bindResponseHandler((response) async { - handledIds.add(response.id); - }); - - await ReminderNotificationCallbacks.onForegroundResponse( - NotificationResponse( - notificationResponseType: - NotificationResponseType.selectedNotificationAction, - id: 10, - ), - ); - await reminderNotificationTapBackground( - NotificationResponse( - notificationResponseType: - NotificationResponseType.selectedNotificationAction, - id: 20, - ), - ); - - expect(handledIds, [10, 20]); - }, - ); - - test( - 'queues background response when handler is unbound and drains later', - () async { - await reminderNotificationTapBackground( - NotificationResponse( - notificationResponseType: - NotificationResponseType.selectedNotificationAction, - id: 99, - ), - ); - - final handledIds = []; - await ReminderNotificationCallbacks.bindResponseHandler((response) async { - handledIds.add(response.id); - }); - - expect(handledIds, [99]); - }, - ); - - test( - 'queues foreground response when handler is unbound and drains later', - () async { - await ReminderNotificationCallbacks.onForegroundResponse( - NotificationResponse( - notificationResponseType: - NotificationResponseType.selectedNotificationAction, - id: 55, - ), - ); - - final handledIds = []; - await ReminderNotificationCallbacks.bindResponseHandler((response) async { - handledIds.add(response.id); - }); - - expect(handledIds, [55]); - }, - ); - - test('failed pending item stays queued for next bind retry', () async { - await reminderNotificationTapBackground( - NotificationResponse( - notificationResponseType: - NotificationResponseType.selectedNotificationAction, - id: 77, - ), - ); - - var firstAttempt = true; - await ReminderNotificationCallbacks.bindResponseHandler((response) async { - if (firstAttempt) { - firstAttempt = false; - throw Exception('temporary failure'); - } - }); - - final handledIds = []; - await ReminderNotificationCallbacks.bindResponseHandler((response) async { - handledIds.add(response.id); - }); - - expect(handledIds, [77]); - }); - - test( - 'background handler failure while bound is enqueued for retry', - () async { - var firstAttempt = true; - await ReminderNotificationCallbacks.bindResponseHandler((response) async { - if (firstAttempt) { - firstAttempt = false; - throw Exception('temporary failure'); - } - }); - - await reminderNotificationTapBackground( - NotificationResponse( - notificationResponseType: - NotificationResponseType.selectedNotificationAction, - id: 123, - ), - ); - - final handledIds = []; - await ReminderNotificationCallbacks.bindResponseHandler((response) async { - handledIds.add(response.id); - }); - - expect(handledIds, [123]); - }, - ); -} diff --git a/apps/test/features/calendar/reminders/reminder_overlay_test.dart b/apps/test/features/calendar/reminders/reminder_overlay_test.dart deleted file mode 100644 index 0763108..0000000 --- a/apps/test/features/calendar/reminders/reminder_overlay_test.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:social_app/features/calendar/reminders/ui/reminder_overlay.dart'; -import 'package:social_app/features/calendar/reminders/reminder_queue_manager.dart'; -import 'package:social_app/features/calendar/reminders/models/reminder_payload.dart'; - -void main() { - group('ReminderOverlay', () { - late ReminderQueueManager queueManager; - - setUp(() { - SharedPreferences.setMockInitialValues({}); - queueManager = ReminderQueueManager(); - }); - - testWidgets('显示日程标题和当前时间', (tester) async { - final payload = ReminderPayload( - eventId: '1', - title: 'Test Meeting', - startAt: DateTime(2026, 3, 20, 10, 0), - endAt: DateTime(2026, 3, 20, 11, 0), - timezone: 'Asia/Shanghai', - mode: ReminderPayloadMode.single, - ); - queueManager.enqueueFromClick(payload); - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: ReminderOverlay( - queueManager: queueManager, - onComplete: () {}, - onSnooze: (minutes) {}, - onArchive: () {}, - ), - ), - ), - ); - - expect(find.text('Test Meeting'), findsOneWidget); - }); - - testWidgets('点击完成按钮触发归档', (tester) async { - bool archiveCalled = false; - final payload = ReminderPayload( - eventId: '1', - title: 'Test Meeting', - startAt: DateTime(2026, 3, 20, 10, 0), - endAt: DateTime(2026, 3, 20, 11, 0), - timezone: 'Asia/Shanghai', - mode: ReminderPayloadMode.single, - ); - queueManager.enqueueFromClick(payload); - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: ReminderOverlay( - queueManager: queueManager, - onComplete: () {}, - onSnooze: (minutes) {}, - onArchive: () => archiveCalled = true, - ), - ), - ), - ); - - await tester.tap(find.text('完成')); - await tester.pump(); - - expect(archiveCalled, true); - }); - - testWidgets('点击稍后提醒显示下拉选项', (tester) async { - final payload = ReminderPayload( - eventId: '1', - title: 'Test Meeting', - startAt: DateTime(2026, 3, 20, 10, 0), - endAt: DateTime(2026, 3, 20, 11, 0), - timezone: 'Asia/Shanghai', - mode: ReminderPayloadMode.single, - ); - queueManager.enqueueFromClick(payload); - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: ReminderOverlay( - queueManager: queueManager, - onComplete: () {}, - onSnooze: (minutes) {}, - onArchive: () {}, - ), - ), - ), - ); - - await tester.tap(find.text('稍后提醒')); - await tester.pumpAndSettle(); - - expect(find.text('5 分钟'), findsOneWidget); - expect(find.text('15 分钟'), findsOneWidget); - }); - }); -} diff --git a/apps/test/features/calendar/reminders/reminder_queue_manager_test.dart b/apps/test/features/calendar/reminders/reminder_queue_manager_test.dart deleted file mode 100644 index ab12ccc..0000000 --- a/apps/test/features/calendar/reminders/reminder_queue_manager_test.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:social_app/features/calendar/reminders/reminder_queue_manager.dart'; -import 'package:social_app/features/calendar/reminders/models/reminder_payload.dart'; - -void main() { - group('ReminderQueueManager', () { - test('按点击顺序处理,第一条处理完后处理剩余的按时间排序', () { - final manager = ReminderQueueManager(); - - final event1 = ReminderPayload( - eventId: '1', - title: 'Event 1', - startAt: DateTime(2026, 3, 20, 10, 1), - timezone: 'UTC', - mode: ReminderPayloadMode.single, - ); - final event2 = ReminderPayload( - eventId: '2', - title: 'Event 2', - startAt: DateTime(2026, 3, 20, 10, 2), - timezone: 'UTC', - mode: ReminderPayloadMode.single, - ); - final event3 = ReminderPayload( - eventId: '3', - title: 'Event 3', - startAt: DateTime(2026, 3, 20, 10, 3), - timezone: 'UTC', - mode: ReminderPayloadMode.single, - ); - - manager.enqueueFromClick(event2); - manager.enqueuePending([event1, event3]); - - expect(manager.currentPayload?.eventId, '2'); - manager.dequeueCurrent(); - expect(manager.currentPayload?.eventId, '1'); - manager.dequeueCurrent(); - expect(manager.currentPayload?.eventId, '3'); - manager.dequeueCurrent(); - expect(manager.isEmpty, true); - }); - - test('单条通知处理完直接清空', () { - final manager = ReminderQueueManager(); - final event = ReminderPayload( - eventId: '1', - title: 'Event 1', - startAt: DateTime.now(), - timezone: 'UTC', - mode: ReminderPayloadMode.single, - ); - - manager.enqueueFromClick(event); - expect(manager.isEmpty, false); - manager.dequeueCurrent(); - expect(manager.isEmpty, true); - }); - }); -} diff --git a/apps/test/features/chat/ag_ui_event_test.dart b/apps/test/features/chat/ag_ui_event_test.dart deleted file mode 100644 index fd1b16a..0000000 --- a/apps/test/features/chat/ag_ui_event_test.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:social_app/features/chat/data/models/ag_ui_event.dart'; - -void main() { - group('AgUiEvent parsing', () { - test('parses TEXT_MESSAGE_END with ui_schema payload', () { - final event = AgUiEvent.fromJson({ - 'type': 'TEXT_MESSAGE_END', - 'messageId': 'msg_1', - 'answer': '你好', - 'role': 'assistant', - 'status': 'success', - 'ui_schema': { - 'version': '2.0', - 'root': { - 'type': 'stack', - 'direction': 'vertical', - 'appearance': 'card', - 'children': [ - {'type': 'text', 'role': 'title', 'content': '创建成功'}, - ], - }, - }, - }); - - expect(event, isA()); - final textEnd = event as TextMessageEndEvent; - expect(textEnd.messageId, 'msg_1'); - expect(textEnd.answer, '你好'); - expect(textEnd.uiSchema?['version'], '2.0'); - }); - - test('parses TOOL_CALL_RESULT snake_case fields', () { - final event = AgUiEvent.fromJson({ - 'type': 'TOOL_CALL_RESULT', - 'messageId': 'tool_1', - 'tool_call_id': 'call_1', - 'tool_name': 'calendar_read', - 'status': 'success', - 'result': '找到 2 条结果', - }); - - expect(event, isA()); - final result = event as ToolCallResultEvent; - expect(result.toolCallId, 'call_1'); - expect(result.toolName, 'calendar_read'); - expect(result.resultSummary, '找到 2 条结果'); - expect(result.status, 'success'); - }); - - test('parses history snapshot with ui_schema', () { - final snapshot = HistorySnapshot.fromJson({ - 'scope': 'history_day', - 'threadId': 'thread_1', - 'day': '2026-03-16', - 'hasMore': false, - 'messages': [ - { - 'id': 'm1', - 'seq': 1, - 'role': 'assistant', - 'content': '已处理', - 'ui_schema': { - 'version': '2.0', - 'root': { - 'type': 'stack', - 'direction': 'vertical', - 'appearance': 'card', - 'children': [], - }, - }, - 'timestamp': '2026-03-16T10:00:00Z', - }, - ], - }); - - expect(snapshot.scope, 'history_day'); - expect(snapshot.messages, hasLength(1)); - expect(snapshot.messages.first.uiSchema, isNotNull); - }); - - test('parses history user attachments list', () { - final snapshot = HistorySnapshot.fromJson({ - 'scope': 'history_day', - 'threadId': 'thread_1', - 'day': '2026-03-16', - 'hasMore': false, - 'messages': [ - { - 'id': 'm1', - 'seq': 1, - 'role': 'user', - 'content': '请看图', - 'attachments': [ - {'url': 'https://signed.example/a.png', 'mimeType': 'image/png'}, - {'url': 'https://signed.example/b.jpg', 'mimeType': 'image/jpeg'}, - ], - 'timestamp': '2026-03-16T10:00:00Z', - }, - ], - }); - - final userMessage = snapshot.messages.first; - expect(userMessage.attachments, hasLength(2)); - expect(userMessage.attachments.first.url, 'https://signed.example/a.png'); - expect(userMessage.attachments.last.mimeType, 'image/jpeg'); - }); - }); -} diff --git a/apps/test/features/chat/data/services/ag_ui_service_test.dart b/apps/test/features/chat/data/services/ag_ui_service_test.dart deleted file mode 100644 index b745b96..0000000 --- a/apps/test/features/chat/data/services/ag_ui_service_test.dart +++ /dev/null @@ -1,346 +0,0 @@ -import 'dart:async'; - -import 'package:dio/dio.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:social_app/core/api/i_api_client.dart'; -import 'package:social_app/features/chat/data/models/ag_ui_event.dart'; -import 'package:social_app/features/chat/data/services/ag_ui_service.dart'; - -class _FakeApiClient implements IApiClient { - _FakeApiClient({ - required this.sseLines, - this.sseLineStreamFactory, - this.runIdFactory, - }); - - final List sseLines; - final Stream Function()? sseLineStreamFactory; - final String Function()? runIdFactory; - final List postPaths = []; - - @override - Future> delete(String path, {data, Options? options}) { - throw UnimplementedError(); - } - - @override - Future> get(String path, {Options? options}) { - throw UnimplementedError(); - } - - @override - Future> getSseLines( - String path, { - Map? headers, - }) async { - final streamFactory = sseLineStreamFactory; - if (streamFactory != null) { - return streamFactory(); - } - return Stream.fromIterable(sseLines); - } - - @override - Future> patch(String path, {data, Options? options}) { - throw UnimplementedError(); - } - - @override - Future> put(String path, {data, Options? options}) { - throw UnimplementedError(); - } - - @override - Future> post(String path, {data, Options? options}) async { - postPaths.add(path); - if (path.contains('/cancel?runId=')) { - final payload = { - 'threadId': 'thread-1', - 'runId': 'run-new', - 'accepted': true, - }; - return Response( - requestOptions: RequestOptions(path: path), - data: payload as T, - statusCode: 202, - ); - } - final runIdFactory = this.runIdFactory; - final payload = { - 'taskId': 'task-1', - 'threadId': 'thread-1', - 'runId': runIdFactory != null ? runIdFactory() : 'run-new', - 'created': true, - }; - return Response( - requestOptions: RequestOptions(path: path), - data: payload as T, - statusCode: 202, - ); - } -} - -List _buildSseEvent({ - required String id, - required String type, - required String payload, -}) { - return ['id: $id', 'event: $type', 'data: $payload', '']; -} - -void main() { - test( - 'sendMessage ignores stale run events and waits for expected run', - () async { - final oldRunLines = _buildSseEvent( - id: '1', - type: AgUiEventTypeWire.runStarted, - payload: - '{"type":"RUN_STARTED","threadId":"thread-1","runId":"run-old"}', - ); - final oldFinishedLines = _buildSseEvent( - id: '2', - type: AgUiEventTypeWire.runFinished, - payload: - '{"type":"RUN_FINISHED","threadId":"thread-1","runId":"run-old"}', - ); - final newRunLines = _buildSseEvent( - id: '3', - type: AgUiEventTypeWire.runStarted, - payload: - '{"type":"RUN_STARTED","threadId":"thread-1","runId":"run-new"}', - ); - final newFinishedLines = _buildSseEvent( - id: '4', - type: AgUiEventTypeWire.runFinished, - payload: - '{"type":"RUN_FINISHED","threadId":"thread-1","runId":"run-new"}', - ); - - final service = AgUiService( - apiClient: _FakeApiClient( - sseLines: [ - ...oldRunLines, - ...oldFinishedLines, - ...newRunLines, - ...newFinishedLines, - ], - ), - ); - final events = []; - service.onEvent = events.add; - - await service.sendMessage('hello'); - - expect(events, hasLength(2)); - expect(events.first, isA()); - expect((events.first as RunStartedEvent).runId, 'run-new'); - expect(events.last, isA()); - expect((events.last as RunFinishedEvent).runId, 'run-new'); - }, - ); - - test( - 'sendMessage accepts in-run terminal event without runId after binding', - () async { - final newRunLines = _buildSseEvent( - id: '11', - type: AgUiEventTypeWire.runStarted, - payload: - '{"type":"RUN_STARTED","threadId":"thread-1","runId":"run-new"}', - ); - final noRunIdTextLines = _buildSseEvent( - id: '12', - type: AgUiEventTypeWire.textMessageEnd, - payload: - '{"type":"TEXT_MESSAGE_END","threadId":"thread-1","messageId":"m1","answer":"ok","role":"assistant","status":"success"}', - ); - final noRunIdFinishedLines = _buildSseEvent( - id: '13', - type: AgUiEventTypeWire.runFinished, - payload: '{"type":"RUN_FINISHED","threadId":"thread-1"}', - ); - - final service = AgUiService( - apiClient: _FakeApiClient( - sseLines: [ - ...newRunLines, - ...noRunIdTextLines, - ...noRunIdFinishedLines, - ], - ), - ); - final events = []; - service.onEvent = events.add; - - await service.sendMessage('hello'); - - expect(events, hasLength(3)); - expect(events[0], isA()); - expect(events[1], isA()); - expect(events[2], isA()); - }, - ); - - test('cancelCurrentRun actively closes current SSE subscription', () async { - var streamCancelled = false; - final streamController = StreamController( - onCancel: () { - streamCancelled = true; - }, - ); - - final service = AgUiService( - apiClient: _FakeApiClient( - sseLines: const [], - sseLineStreamFactory: () => streamController.stream, - ), - ); - - final sendFuture = service.sendMessage('hello'); - await Future.delayed(Duration.zero); - await service.cancelCurrentRun(); - - await sendFuture; - expect(streamCancelled, isTrue); - await streamController.close(); - }); - - test( - 'cancelCurrentRun calls backend cancel endpoint for active run', - () async { - final streamController = StreamController(); - final fakeApi = _FakeApiClient( - sseLines: const [], - sseLineStreamFactory: () => streamController.stream, - ); - final service = AgUiService(apiClient: fakeApi); - - final sendFuture = service.sendMessage('hello'); - await Future.delayed(Duration.zero); - for (final line in _buildSseEvent( - id: '51', - type: AgUiEventTypeWire.runStarted, - payload: - '{"type":"RUN_STARTED","threadId":"thread-1","runId":"run-new"}', - )) { - streamController.add(line); - } - await Future.delayed(Duration.zero); - - await service.cancelCurrentRun(); - await sendFuture; - - expect( - fakeApi.postPaths, - contains('/api/v1/agent/runs/thread-1/cancel?runId=run-new'), - ); - await streamController.close(); - }, - ); - - test( - 'new sendMessage cancels previous SSE subscription explicitly', - () async { - var firstStreamCancelled = false; - final firstController = StreamController( - onCancel: () { - firstStreamCancelled = true; - }, - ); - final secondController = StreamController(); - final streamQueue = >[ - firstController, - secondController, - ]; - var streamIndex = 0; - var runIndex = 0; - - final service = AgUiService( - apiClient: _FakeApiClient( - sseLines: const [], - sseLineStreamFactory: () => streamQueue[streamIndex++].stream, - runIdFactory: () { - runIndex += 1; - return 'run-$runIndex'; - }, - ), - ); - - final firstSendFuture = service.sendMessage('first'); - await Future.delayed(Duration.zero); - final secondSendFuture = service.sendMessage('second'); - - await Future.delayed(Duration.zero); - for (final line in _buildSseEvent( - id: '21', - type: AgUiEventTypeWire.runStarted, - payload: '{"type":"RUN_STARTED","threadId":"thread-1","runId":"run-2"}', - )) { - secondController.add(line); - } - for (final line in _buildSseEvent( - id: '22', - type: AgUiEventTypeWire.runFinished, - payload: - '{"type":"RUN_FINISHED","threadId":"thread-1","runId":"run-2"}', - )) { - secondController.add(line); - } - await secondController.close(); - - await firstSendFuture; - await secondSendFuture; - - expect(firstStreamCancelled, isTrue); - await firstController.close(); - }, - ); - - test('sendMessage surfaces event callback exceptions', () async { - final service = AgUiService( - apiClient: _FakeApiClient( - sseLines: [ - ..._buildSseEvent( - id: '31', - type: AgUiEventTypeWire.runStarted, - payload: - '{"type":"RUN_STARTED","threadId":"thread-1","runId":"run-new"}', - ), - ..._buildSseEvent( - id: '32', - type: AgUiEventTypeWire.runFinished, - payload: - '{"type":"RUN_FINISHED","threadId":"thread-1","runId":"run-new"}', - ), - ], - ), - ); - service.onEvent = (_) => throw StateError('event callback failed'); - - await expectLater(service.sendMessage('hello'), throwsA(isA())); - }); - - test('sendMessage fails when SSE closes before terminal event', () async { - final startedLines = _buildSseEvent( - id: '41', - type: AgUiEventTypeWire.runStarted, - payload: '{"type":"RUN_STARTED","threadId":"thread-1","runId":"run-new"}', - ); - - final service = AgUiService( - apiClient: _FakeApiClient(sseLines: [...startedLines]), - ); - - await expectLater( - service.sendMessage('hello'), - throwsA( - isA().having( - (e) => e.message, - 'message', - contains('SSE closed before terminal event'), - ), - ), - ); - }); -} diff --git a/apps/test/features/chat/presentation/agent_stage_mapping_test.dart b/apps/test/features/chat/presentation/agent_stage_mapping_test.dart deleted file mode 100644 index 66742bf..0000000 --- a/apps/test/features/chat/presentation/agent_stage_mapping_test.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:social_app/features/chat/presentation/bloc/agent_stage.dart'; - -void main() { - group('agent stage mapping', () { - test('maps protocol step router to routing stage label', () { - final stage = stageFromStepName('router'); - - expect(stage, AgentStage.routing); - expect(stageLabel(stage), '意图识别中'); - }); - - test('maps protocol step worker to execution stage label', () { - final stage = stageFromStepName('worker'); - - expect(stage, AgentStage.execution); - expect(stageLabel(stage), '任务执行中'); - }); - - test('maps protocol step memory to memory stage label', () { - final stage = stageFromStepName('memory'); - - expect(stage, AgentStage.memory); - expect(stageLabel(stage), '记忆提取中'); - }); - - test('uses processing label when step is unknown', () { - final stage = stageFromStepName('unexpected'); - - expect(stage, isNull); - expect(stageLabel(stage), '任务处理中'); - }); - }); -} diff --git a/apps/test/features/chat/presentation/chat_bloc_attachment_sync_test.dart b/apps/test/features/chat/presentation/chat_bloc_attachment_sync_test.dart deleted file mode 100644 index 70c7036..0000000 --- a/apps/test/features/chat/presentation/chat_bloc_attachment_sync_test.dart +++ /dev/null @@ -1,358 +0,0 @@ -import 'dart:async'; - -import 'package:dio/dio.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:social_app/core/api/i_api_client.dart'; -import 'package:social_app/features/chat/data/models/ag_ui_event.dart'; -import 'package:social_app/features/chat/data/models/chat_list_item.dart'; -import 'package:social_app/features/chat/data/services/ag_ui_service.dart'; -import 'package:social_app/features/chat/presentation/bloc/chat_bloc.dart'; - -class _NoopApiClient implements IApiClient { - @override - Future> delete(String path, {data, Options? options}) { - throw UnimplementedError(); - } - - @override - Future> get(String path, {Options? options}) { - throw UnimplementedError(); - } - - @override - Future> getSseLines( - String path, { - Map? headers, - }) { - throw UnimplementedError(); - } - - @override - Future> patch(String path, {data, Options? options}) { - throw UnimplementedError(); - } - - @override - Future> put(String path, {data, Options? options}) { - throw UnimplementedError(); - } - - @override - Future> post(String path, {data, Options? options}) { - throw UnimplementedError(); - } -} - -class _FakeAgUiService extends AgUiService { - _FakeAgUiService() : super(apiClient: _NoopApiClient()); - - Completer? pendingResult; - Completer? pendingHistory; - Object? nextError; - - @override - Future sendMessage( - String content, { - List? images, - }) async { - final error = nextError; - if (error != null) { - nextError = null; - throw error; - } - final pending = pendingResult; - if (pending != null) { - return pending.future; - } - return const SendMessageResult(uploadedAttachments: []); - } - - @override - Future loadHistory({DateTime? beforeDate}) async { - final pending = pendingHistory; - if (pending != null) { - return pending.future; - } - return const HistorySnapshot( - scope: 'history_day', - threadId: null, - day: null, - hasMore: false, - messages: [], - ); - } - - void emitEvent(AgUiEvent event) { - onEvent(event); - } -} - -void main() { - group('ChatBloc attachment sync', () { - late _FakeAgUiService service; - late ChatBloc bloc; - - setUp(() { - service = _FakeAgUiService(); - bloc = ChatBloc(service: service, apiClient: _NoopApiClient()); - }); - - tearDown(() async { - await bloc.close(); - }); - - test('optimistic local image is replaced with uploaded url', () async { - final completer = Completer(); - service.pendingResult = completer; - - final sendFuture = bloc.sendMessage( - 'hello', - images: [ - XFile('/tmp/local.jpg', name: 'local.jpg', mimeType: 'image/jpeg'), - ], - ); - - await Future.delayed(Duration.zero); - final optimistic = bloc.state.items.last as TextMessageItem; - expect(optimistic.attachments, hasLength(1)); - expect(optimistic.attachments.first['path'], '/tmp/local.jpg'); - expect(optimistic.attachments.first['uploading'], isTrue); - - completer.complete( - const SendMessageResult( - uploadedAttachments: [ - UploadedAttachment( - localPath: '/tmp/local.jpg', - url: 'https://cdn.example.com/a.jpg', - mimeType: 'image/jpeg', - ), - ], - ), - ); - await sendFuture; - - final synced = bloc.state.items.last as TextMessageItem; - expect(synced.attachments.first['url'], 'https://cdn.example.com/a.jpg'); - expect(synced.attachments.first['uploading'], isFalse); - }); - - test( - 'upload failure clears uploading state to avoid endless spinner', - () async { - service.nextError = StateError('upload failed'); - - await bloc.sendMessage( - 'hello', - images: [ - XFile('/tmp/local.jpg', name: 'local.jpg', mimeType: 'image/jpeg'), - ], - ); - - final failed = bloc.state.items.last as TextMessageItem; - expect(failed.attachments.first['uploading'], isFalse); - expect(bloc.state.error, contains('upload failed')); - }, - ); - - test('tool call stays visible until assistant final output', () { - service.emitEvent( - ToolCallStartEvent(toolCallId: 'tool-1', toolCallName: 'ocr_image'), - ); - var toolItem = bloc.state.items.last as ToolCallItem; - expect(toolItem.status, ToolCallStatus.pending); - - service.emitEvent(ToolCallEndEvent(toolCallId: 'tool-1')); - toolItem = bloc.state.items.last as ToolCallItem; - expect(toolItem.status, ToolCallStatus.executing); - - service.emitEvent( - ToolCallResultEvent( - messageId: 'tool-msg-1', - toolCallId: 'tool-1', - toolName: 'ocr_image', - resultSummary: 'done', - status: 'success', - ), - ); - toolItem = bloc.state.items.last as ToolCallItem; - expect(toolItem.status, ToolCallStatus.completed); - - service.emitEvent( - TextMessageEndEvent( - messageId: 'assistant-1', - answer: '识别完成', - role: 'assistant', - status: 'success', - uiSchema: null, - ), - ); - - expect(bloc.state.items.whereType(), isEmpty); - expect(bloc.state.items.whereType().length, 1); - }); - - test('run error keeps tool card and marks it failed', () { - service.emitEvent( - ToolCallStartEvent(toolCallId: 'tool-err', toolCallName: 'ocr_image'), - ); - service.emitEvent(ToolCallEndEvent(toolCallId: 'tool-err')); - - service.emitEvent(RunErrorEvent(message: 'runtime execution failed')); - - final toolItem = bloc.state.items.whereType().single; - expect(toolItem.status, ToolCallStatus.error); - expect(toolItem.errorMessage, '本次运行已失败'); - expect(bloc.state.error, 'runtime execution failed'); - }); - - test('run canceled error clears error and marks tool as canceled', () { - service.emitEvent( - ToolCallStartEvent( - toolCallId: 'tool-cancel', - toolCallName: 'ocr_image', - ), - ); - service.emitEvent(ToolCallEndEvent(toolCallId: 'tool-cancel')); - - service.emitEvent( - RunErrorEvent(message: 'run canceled by user', code: 'RUN_CANCELED'), - ); - - final toolItem = bloc.state.items.whereType().single; - expect(toolItem.status, ToolCallStatus.error); - expect(toolItem.errorMessage, '本次运行已取消'); - expect(bloc.state.error, isNull); - expect(bloc.state.isWaitingFirstToken, isFalse); - expect(bloc.state.isStreaming, isFalse); - expect(bloc.state.isCancelling, isFalse); - }); - - test('text event with ui schema is rendered into chat items', () { - service.emitEvent(RunStartedEvent(threadId: 'thread-1', runId: 'run-1')); - - service.emitEvent( - TextMessageEndEvent( - messageId: 'assistant-1', - answer: '这是测试回复', - role: 'assistant', - status: 'success', - uiSchema: { - 'version': '2.0', - 'root': { - 'type': 'stack', - 'direction': 'vertical', - 'children': [ - {'type': 'text', 'role': 'body', 'content': '测试 UI 卡片'}, - ], - }, - }, - ), - ); - - service.emitEvent(RunFinishedEvent(threadId: 'thread-1', runId: 'run-1')); - - final messages = bloc.state.items.whereType().toList(); - final uiCards = bloc.state.items.whereType().toList(); - - expect(messages, hasLength(1)); - expect(messages.single.content, '这是测试回复'); - expect(uiCards, hasLength(1)); - expect(uiCards.single.uiSchema['root'], isA>()); - expect(bloc.state.isWaitingFirstToken, isFalse); - expect(bloc.state.isStreaming, isFalse); - expect(bloc.state.currentStage, isNull); - }); - - test( - 'history loading does not overwrite real-time text and ui events', - () async { - final historyCompleter = Completer(); - service.pendingHistory = historyCompleter; - - final loadFuture = bloc.loadHistory(); - await Future.delayed(Duration.zero); - - service.emitEvent( - RunStartedEvent(threadId: 'thread-1', runId: 'run-1'), - ); - service.emitEvent( - TextMessageEndEvent( - messageId: 'assistant-live', - answer: '实时回复', - role: 'assistant', - status: 'success', - uiSchema: { - 'version': '2.0', - 'root': { - 'type': 'stack', - 'direction': 'vertical', - 'children': [ - {'type': 'text', 'role': 'body', 'content': '实时 UI 卡片'}, - ], - }, - }, - ), - ); - - historyCompleter.complete( - const HistorySnapshot( - scope: 'history_day', - threadId: 'thread-1', - day: '2026-03-24', - hasMore: false, - messages: [], - ), - ); - - await loadFuture; - - final texts = bloc.state.items.whereType().toList(); - final uiCards = bloc.state.items.whereType().toList(); - expect(texts.map((item) => item.id), contains('assistant-live')); - expect(uiCards.map((item) => item.id), contains('assistant-live-ui')); - }, - ); - - test( - 'abnormal SSE close recovers from history without raw bad-state error', - () async { - service.nextError = StateError( - 'SSE closed before terminal event for run', - ); - service.pendingHistory = Completer() - ..complete( - HistorySnapshot( - scope: 'history_day', - threadId: 'thread-1', - day: '2026-03-24', - hasMore: false, - messages: [ - HistoryMessage( - id: 'assistant-history-1', - seq: 2, - role: 'assistant', - content: '历史补偿回复', - timestamp: DateTime(2026, 3, 24, 17, 0, 0), - ), - ], - ), - ); - - await bloc.sendMessage('你是谁?'); - - expect(bloc.state.error, isNull); - expect(bloc.state.isWaitingFirstToken, isFalse); - expect(bloc.state.isStreaming, isFalse); - expect(bloc.state.currentStage, isNull); - expect( - bloc.state.items - .whereType() - .map((item) => item.content) - .toList(), - contains('历史补偿回复'), - ); - }, - ); - }); -} diff --git a/apps/test/features/chat/ui_schema_navigation_test.dart b/apps/test/features/chat/ui_schema_navigation_test.dart deleted file mode 100644 index d913ddc..0000000 --- a/apps/test/features/chat/ui_schema_navigation_test.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:social_app/features/chat/ui/navigation/ui_schema_navigation.dart'; - -void main() { - test('buildUiSchemaNavigationTarget merges scalar params only', () { - final target = buildUiSchemaNavigationTarget( - path: '/calendar/dayweek', - params: { - 'date': '2026-03-18', - 'from': 'home', - 'count': 2, - 'enabled': true, - 'ignored': {'nested': true}, - }, - ); - - expect( - target, - '/calendar/dayweek?date=2026-03-18&from=home&count=2&enabled=true', - ); - }); - - test('isValidInternalNavigationPath follows protocol constraints', () { - expect(isValidInternalNavigationPath('/todo/123/edit'), true); - expect(isValidInternalNavigationPath('https://evil.com'), false); - expect(isValidInternalNavigationPath('/todo/123?x=1'), false); - expect(isValidInternalNavigationPath('/todo/:id'), false); - }); -} diff --git a/apps/test/features/chat/ui_schema_renderer_test.dart b/apps/test/features/chat/ui_schema_renderer_test.dart deleted file mode 100644 index 557cbbb..0000000 --- a/apps/test/features/chat/ui_schema_renderer_test.dart +++ /dev/null @@ -1,277 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:go_router/go_router.dart'; -import 'package:social_app/features/chat/ui/widgets/ui_schema_renderer.dart'; - -void main() { - group('UiSchemaRenderer', () { - testWidgets('renders stack title and badge', (tester) async { - final schema = { - 'version': '2.0', - 'locale': 'zh-CN', - 'status': 'success', - 'theme': 'default', - 'root': { - 'type': 'stack', - 'direction': 'vertical', - 'appearance': 'card', - 'children': [ - {'type': 'text', 'role': 'title', 'content': '日程已创建'}, - {'type': 'badge', 'label': 'SUCCESS', 'status': 'success'}, - ], - }, - }; - - await tester.pumpWidget( - MaterialApp( - home: Scaffold(body: UiSchemaRenderer.renderSchema(schema)), - ), - ); - - expect(find.text('日程已创建'), findsOneWidget); - expect(find.text('SUCCESS'), findsOneWidget); - }); - - testWidgets('renders kv node values', (tester) async { - final schema = { - 'version': '2.0', - 'root': { - 'type': 'stack', - 'direction': 'vertical', - 'appearance': 'card', - 'children': [ - { - 'type': 'kv', - 'items': [ - {'key': 'title', 'label': '标题', 'value': '评审会'}, - ], - }, - ], - }, - }; - - await tester.pumpWidget( - MaterialApp( - home: Scaffold(body: UiSchemaRenderer.renderSchema(schema)), - ), - ); - - expect(find.text('标题'), findsOneWidget); - expect(find.text('评审会'), findsOneWidget); - }); - - testWidgets('renders batch result list items in one card', (tester) async { - final schema = { - 'version': '2.0', - 'root': { - 'type': 'stack', - 'direction': 'vertical', - 'appearance': 'card', - 'status': 'warning', - 'children': [ - {'type': 'text', 'role': 'title', 'content': '日历操作完成'}, - { - 'type': 'stack', - 'direction': 'vertical', - 'gap': 8, - 'children': [ - { - 'type': 'stack', - 'direction': 'vertical', - 'appearance': 'card', - 'children': [ - {'type': 'text', 'role': 'body', 'content': '#1 create'}, - {'type': 'text', 'role': 'caption', 'content': '成功'}, - {'type': 'text', 'role': 'caption', 'content': '日程「晨会」已创建'}, - ], - }, - { - 'type': 'stack', - 'direction': 'vertical', - 'appearance': 'card', - 'children': [ - {'type': 'text', 'role': 'body', 'content': '#2 delete'}, - {'type': 'text', 'role': 'caption', 'content': '失败'}, - { - 'type': 'text', - 'role': 'caption', - 'content': 'Schedule item not found', - }, - ], - }, - ], - }, - ], - }, - }; - - await tester.pumpWidget( - MaterialApp( - home: Scaffold(body: UiSchemaRenderer.renderSchema(schema)), - ), - ); - - expect(find.text('日历操作完成'), findsOneWidget); - expect(find.text('#1 create'), findsOneWidget); - expect(find.text('#2 delete'), findsOneWidget); - expect(find.text('成功'), findsOneWidget); - expect(find.text('失败'), findsOneWidget); - }); - - testWidgets('renders fallback for invalid schema', (tester) async { - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: UiSchemaRenderer.renderSchema({'version': '2.0'}), - ), - ), - ); - - expect(find.textContaining('无效 UI Schema'), findsOneWidget); - }); - - testWidgets('handles navigation action by pushing target page', ( - tester, - ) async { - final schema = { - 'version': '2.0', - 'root': { - 'type': 'stack', - 'direction': 'vertical', - 'appearance': 'plain', - 'children': [ - { - 'type': 'button', - 'label': '查看待办', - 'style': 'primary', - 'action': { - 'type': 'navigation', - 'path': '/todo/123', - 'params': {'from': 'assistant'}, - }, - }, - ], - }, - }; - - final router = GoRouter( - initialLocation: '/', - routes: [ - GoRoute( - path: '/', - builder: (context, state) => - Scaffold(body: UiSchemaRenderer.renderSchema(schema)), - ), - GoRoute( - path: '/todo/:id', - builder: (context, state) => Text( - 'todo detail ${state.pathParameters['id']} from ${state.uri.queryParameters['from']}', - ), - ), - ], - ); - - await tester.pumpWidget(MaterialApp.router(routerConfig: router)); - await tester.tap(find.text('查看待办')); - await tester.pumpAndSettle(); - - expect(find.text('todo detail 123 from assistant'), findsOneWidget); - expect(router.canPop(), isTrue); - - router.pop(); - await tester.pumpAndSettle(); - - expect(find.text('查看待办'), findsOneWidget); - }); - - testWidgets('uses replace navigation when replace is true', (tester) async { - final schema = { - 'version': '2.0', - 'root': { - 'type': 'stack', - 'direction': 'vertical', - 'appearance': 'plain', - 'children': [ - { - 'type': 'button', - 'label': '替换跳转', - 'style': 'primary', - 'action': { - 'type': 'navigation', - 'path': '/todo/456', - 'replace': true, - }, - }, - ], - }, - }; - - final router = GoRouter( - initialLocation: '/', - routes: [ - GoRoute( - path: '/', - builder: (context, state) => - Scaffold(body: UiSchemaRenderer.renderSchema(schema)), - ), - GoRoute( - path: '/todo/:id', - builder: (context, state) => - Text('todo detail ${state.pathParameters['id']}'), - ), - ], - ); - - await tester.pumpWidget(MaterialApp.router(routerConfig: router)); - await tester.tap(find.text('替换跳转')); - await tester.pumpAndSettle(); - - expect(find.text('todo detail 456'), findsOneWidget); - expect(router.canPop(), isFalse); - - expect(find.text('todo detail 456'), findsOneWidget); - }); - - testWidgets('does not navigate for placeholder path', (tester) async { - final schema = { - 'version': '2.0', - 'root': { - 'type': 'stack', - 'direction': 'vertical', - 'appearance': 'plain', - 'children': [ - { - 'type': 'button', - 'label': '坏路径', - 'style': 'primary', - 'action': {'type': 'navigation', 'path': '/todo/:id'}, - }, - ], - }, - }; - - final router = GoRouter( - initialLocation: '/', - routes: [ - GoRoute( - path: '/', - builder: (context, state) => - Scaffold(body: UiSchemaRenderer.renderSchema(schema)), - ), - GoRoute( - path: '/todo/:id', - builder: (context, state) => const Text('detail'), - ), - ], - ); - - await tester.pumpWidget(MaterialApp.router(routerConfig: router)); - await tester.tap(find.text('坏路径')); - await tester.pumpAndSettle(); - await tester.pump(const Duration(seconds: 3)); - - expect(find.text('坏路径'), findsOneWidget); - expect(find.text('detail'), findsNothing); - }); - }); -} diff --git a/apps/test/features/home/ui/controllers/home_keyboard_inset_calculator_test.dart b/apps/test/features/home/ui/controllers/home_keyboard_inset_calculator_test.dart deleted file mode 100644 index c48418d..0000000 --- a/apps/test/features/home/ui/controllers/home_keyboard_inset_calculator_test.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:social_app/features/home/ui/controllers/home_keyboard_inset_calculator.dart'; - -void main() { - test('subtracts bottom safe area from keyboard inset', () { - final inset = HomeKeyboardInsetCalculator.compute( - rawViewInsetBottom: 336, - bottomViewPadding: 34, - ); - - expect(inset, 302); - }); - - test('returns zero when keyboard is effectively hidden', () { - final inset = HomeKeyboardInsetCalculator.compute( - rawViewInsetBottom: 6, - bottomViewPadding: 34, - ); - - expect(inset, 0); - }); - - test('follows keyboard fallback immediately when inset decreases', () { - final openedInset = HomeKeyboardInsetCalculator.compute( - rawViewInsetBottom: 336, - bottomViewPadding: 34, - ); - final collapsedInset = HomeKeyboardInsetCalculator.compute( - rawViewInsetBottom: 120, - bottomViewPadding: 34, - ); - - expect(openedInset, 302); - expect(collapsedInset, 86); - }); -} diff --git a/apps/test/features/home/ui/controllers/home_message_viewport_controller_test.dart b/apps/test/features/home/ui/controllers/home_message_viewport_controller_test.dart deleted file mode 100644 index 5f57187..0000000 --- a/apps/test/features/home/ui/controllers/home_message_viewport_controller_test.dart +++ /dev/null @@ -1,197 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:social_app/features/home/ui/controllers/home_message_viewport_controller.dart'; - -void main() { - ViewportEvent buildEvent({ - required ViewportEventType type, - required String conversationId, - required int eventSeq, - required double distanceToBottomPx, - bool isFirstEnter = false, - bool hasSavedViewport = false, - bool hasAnchor = false, - int deltaCount = 0, - ViewportTriggerSource source = ViewportTriggerSource.system, - }) { - return ViewportEvent( - type: type, - conversationId: conversationId, - eventSeq: eventSeq, - triggerSource: source, - deltaCount: deltaCount, - anchor: const ViewportAnchor(messageId: null, offsetPx: null), - timestamp: 1000 + eventSeq, - viewportContext: ViewportContext( - distanceToBottomPx: distanceToBottomPx, - isFirstEnter: isFirstEnter, - hasSavedViewport: hasSavedViewport, - hasAnchor: hasAnchor, - ), - ); - } - - test('distance<=96 and new message => animateBottom', () { - final controller = HomeMessageViewportController(); - final decision = controller.apply( - buildEvent( - type: ViewportEventType.newMessageAppended, - conversationId: 'c1', - eventSeq: 1, - distanceToBottomPx: 80, - deltaCount: 1, - ), - ); - - expect(decision.action, ViewportAction.animateBottom); - }); - - test('distance>96 and new message => showUnreadBadge', () { - final controller = HomeMessageViewportController(); - controller.apply( - buildEvent( - type: ViewportEventType.userScrollStateChanged, - conversationId: 'c1', - eventSeq: 1, - distanceToBottomPx: 200, - source: ViewportTriggerSource.user, - ), - ); - - final decision = controller.apply( - buildEvent( - type: ViewportEventType.newMessageAppended, - conversationId: 'c1', - eventSeq: 2, - distanceToBottomPx: 200, - deltaCount: 1, - ), - ); - - expect(decision.action, ViewportAction.showUnreadBadge); - expect(controller.unreadCount, 1); - }); - - test('stale event is dropped by sequence', () { - final controller = HomeMessageViewportController(); - controller.apply( - buildEvent( - type: ViewportEventType.historyInitialLoaded, - conversationId: 'c1', - eventSeq: 10, - distanceToBottomPx: 0, - isFirstEnter: true, - ), - ); - - final decision = controller.apply( - buildEvent( - type: ViewportEventType.newMessageAppended, - conversationId: 'c1', - eventSeq: 9, - distanceToBottomPx: 0, - ), - ); - - expect(decision.action, ViewportAction.none); - expect(decision.reason, 'stale-event'); - }); - - test('different conversations keep independent sequence', () { - final controller = HomeMessageViewportController(); - controller.apply( - buildEvent( - type: ViewportEventType.historyInitialLoaded, - conversationId: 'A', - eventSeq: 10, - distanceToBottomPx: 0, - isFirstEnter: true, - ), - ); - - final decision = controller.apply( - buildEvent( - type: ViewportEventType.historyInitialLoaded, - conversationId: 'B', - eventSeq: 1, - distanceToBottomPx: 0, - isFirstEnter: true, - ), - ); - - expect(decision.action, ViewportAction.jumpBottom); - }); - - test('refresh keeps reading history position when far from bottom', () { - final controller = HomeMessageViewportController(); - controller.apply( - buildEvent( - type: ViewportEventType.userScrollStateChanged, - conversationId: 'c1', - eventSeq: 1, - distanceToBottomPx: 180, - source: ViewportTriggerSource.user, - ), - ); - - final decision = controller.apply( - buildEvent( - type: ViewportEventType.sessionRefreshCompleted, - conversationId: 'c1', - eventSeq: 2, - distanceToBottomPx: 180, - ), - ); - - expect(decision.action, ViewportAction.none); - }); - - test('resume with saved viewport restores anchor', () { - final controller = HomeMessageViewportController(); - final decision = controller.apply( - buildEvent( - type: ViewportEventType.screenResumedFromSubRoute, - conversationId: 'c1', - eventSeq: 1, - distanceToBottomPx: 180, - hasSavedViewport: true, - hasAnchor: true, - ), - ); - - expect(decision.action, ViewportAction.restoreAnchor); - expect(decision.reason, 'resume-restore-saved-viewport'); - }); - - test('prepend finish without anchor exits restoring state', () { - final controller = HomeMessageViewportController(); - controller.apply( - buildEvent( - type: ViewportEventType.historyPagePrependStarted, - conversationId: 'c1', - eventSeq: 1, - distanceToBottomPx: 200, - hasAnchor: true, - ), - ); - controller.apply( - buildEvent( - type: ViewportEventType.historyPagePrependFinished, - conversationId: 'c1', - eventSeq: 2, - distanceToBottomPx: 200, - hasAnchor: false, - ), - ); - - final decision = controller.apply( - buildEvent( - type: ViewportEventType.newMessageAppended, - conversationId: 'c1', - eventSeq: 3, - distanceToBottomPx: 200, - deltaCount: 1, - ), - ); - expect(decision.action, ViewportAction.showUnreadBadge); - }); -} diff --git a/apps/test/features/home/ui/navigation/home_return_policy_test.dart b/apps/test/features/home/ui/navigation/home_return_policy_test.dart deleted file mode 100644 index 28e8229..0000000 --- a/apps/test/features/home/ui/navigation/home_return_policy_test.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:social_app/features/home/ui/navigation/home_return_policy.dart'; - -void main() { - group('resolveHomeReturnAction', () { - test('dock home action should always resolve to goHome', () { - final action = resolveHomeReturnAction(canPop: true, isAuthEntry: false); - expect(action, HomeReturnAction.goHomeForDock); - }); - - test('second-level pages should return to home instead of exiting app', () { - final action = resolveHomeReturnAction( - canPop: false, - isAuthEntry: false, - forceGoHome: true, - ); - expect(action, HomeReturnAction.goHome); - }); - - test('business route with back stack resolves to dock home action', () { - final action = resolveHomeReturnAction(canPop: true, isAuthEntry: false); - expect(action, HomeReturnAction.goHomeForDock); - }); - - test('business route without back stack falls back to go home', () { - final action = resolveHomeReturnAction(canPop: false, isAuthEntry: false); - expect(action, HomeReturnAction.goHome); - }); - - test('auth entry always goes home directly', () { - final action = resolveHomeReturnAction(canPop: true, isAuthEntry: true); - expect(action, HomeReturnAction.goHome); - }); - }); -} diff --git a/apps/test/features/home/ui/widgets/home_chat_item_renderer_test.dart b/apps/test/features/home/ui/widgets/home_chat_item_renderer_test.dart deleted file mode 100644 index df47cb1..0000000 --- a/apps/test/features/home/ui/widgets/home_chat_item_renderer_test.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:social_app/features/chat/data/models/chat_list_item.dart'; -import 'package:social_app/features/home/ui/widgets/home_chat_item_renderer.dart'; - -void main() { - ToolCallItem _toolCallItem(String toolName) { - return ToolCallItem( - id: 'tc-1', - callId: 'tc-1', - toolName: toolName, - args: const {}, - status: ToolCallStatus.pending, - timestamp: DateTime(2026, 1, 1), - sender: MessageSender.ai, - ); - } - - Future _pumpToolCallItem(WidgetTester tester, String toolName) async { - final widget = MaterialApp( - home: Scaffold(body: HomeChatItemRenderer.build(_toolCallItem(toolName))), - ); - await tester.pumpWidget(widget); - } - - group('HomeChatItemRenderer tool name localization', () { - testWidgets('renders dot style name in Chinese', (tester) async { - await _pumpToolCallItem(tester, 'memory.write'); - expect(find.text('写入记忆'), findsOneWidget); - }); - - testWidgets('renders snake style alias in Chinese', (tester) async { - await _pumpToolCallItem(tester, 'memory_write'); - expect(find.text('写入记忆'), findsOneWidget); - }); - - testWidgets('falls back to raw name for unknown tool', (tester) async { - await _pumpToolCallItem(tester, 'unknown.tool'); - expect(find.text('unknown.tool'), findsOneWidget); - }); - }); -} diff --git a/apps/test/features/home/ui/widgets/home_screen_layout_test.dart b/apps/test/features/home/ui/widgets/home_screen_layout_test.dart deleted file mode 100644 index 9b9c6df..0000000 --- a/apps/test/features/home/ui/widgets/home_screen_layout_test.dart +++ /dev/null @@ -1,452 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:social_app/core/api/i_api_client.dart'; -import 'package:social_app/core/di/injection.dart'; -import 'package:social_app/features/chat/presentation/bloc/chat_bloc.dart'; -import 'package:social_app/features/chat/data/models/chat_list_item.dart'; -import 'package:social_app/features/home/data/voice_recorder.dart'; -import 'package:social_app/features/home/ui/screens/home_screen.dart'; -import 'package:social_app/features/home/ui/widgets/home_attachment_strip.dart'; -import 'package:social_app/features/home/ui/widgets/home_floating_header.dart'; -import 'package:social_app/features/messages/data/inbox_api.dart'; -import 'package:social_app/shared/widgets/message_composer.dart'; - -class _PermissionDeniedRecorder implements VoiceRecorder { - _PermissionDeniedRecorder(); - - int stopCalls = 0; - - @override - Future dispose() async {} - - @override - Future start() async { - await Future.delayed(const Duration(milliseconds: 400)); - throw StateError('录音权限未授权'); - } - - @override - Future stop() async { - stopCalls += 1; - return null; - } -} - -class _DelayedSuccessRecorder implements VoiceRecorder { - _DelayedSuccessRecorder(); - - int stopCalls = 0; - - @override - Future dispose() async {} - - @override - Future start() async { - await Future.delayed(const Duration(milliseconds: 400)); - } - - @override - Future stop() async { - stopCalls += 1; - return '/tmp/mock-recording.wav'; - } -} - -class _TestApiClient implements IApiClient { - @override - Future> delete(String path, {data, Options? options}) async { - return Response(requestOptions: RequestOptions(path: path)); - } - - @override - Future> get(String path, {Options? options}) async { - return Response( - requestOptions: RequestOptions(path: path), - data: [] as T, - ); - } - - @override - Future> getSseLines( - String path, { - Map? headers, - }) async { - return const Stream.empty(); - } - - @override - Future> patch(String path, {data, Options? options}) async { - return Response(requestOptions: RequestOptions(path: path)); - } - - @override - Future> put(String path, {data, Options? options}) async { - return Response(requestOptions: RequestOptions(path: path)); - } - - @override - Future> post(String path, {data, Options? options}) async { - return Response(requestOptions: RequestOptions(path: path)); - } -} - -void main() { - late ChatBloc chatBloc; - - setUp(() { - final apiClient = _TestApiClient(); - if (sl.isRegistered()) { - sl.unregister(); - } - sl.registerSingleton(InboxApi(apiClient)); - chatBloc = ChatBloc(apiClient: apiClient); - }); - - tearDown(() async { - await chatBloc.close(); - if (sl.isRegistered()) { - await sl.unregister(); - } - }); - - Future pumpHomeScreen( - WidgetTester tester, { - List initialSelectedImages = const [], - VoiceRecorder? voiceRecorder, - Future Function(String filePath)? onTranscribeAudio, - }) async { - await tester.pumpWidget( - MaterialApp( - home: HomeScreen( - chatBloc: chatBloc, - autoLoadHistory: false, - initialSelectedImages: initialSelectedImages, - voiceRecorder: voiceRecorder, - onTranscribeAudio: onTranscribeAudio, - ), - ), - ); - await tester.pump(); - } - - List buildMessages(int count) { - final base = DateTime(2026, 1, 1, 9, 0); - return List.generate(count, (index) { - return TextMessageItem( - id: 'msg_$index', - content: 'message $index', - timestamp: base.add(Duration(minutes: index)), - sender: index.isEven ? MessageSender.user : MessageSender.ai, - ); - }); - } - - testWidgets( - 'home screen shows floating header, conversation stage, and bottom input stack', - (tester) async { - await pumpHomeScreen(tester); - - expect(find.byKey(homeFloatingHeaderKey), findsOneWidget); - expect(find.byKey(homeFloatingHeaderTitleKey), findsOneWidget); - expect(find.text('Linksy'), findsOneWidget); - expect(find.byKey(homeConversationStageKey), findsOneWidget); - expect(find.byKey(homeBottomInputStackKey), findsOneWidget); - }, - ); - - testWidgets('empty state keeps clean stage without helper copy', ( - tester, - ) async { - await pumpHomeScreen(tester); - - expect(find.byKey(homeConversationStageKey), findsOneWidget); - expect(find.byKey(homeEmptyStateAmbientKey), findsOneWidget); - expect(find.text('开始对话吧'), findsNothing); - }); - - testWidgets('selected images render in attachment strip above composer', ( - tester, - ) async { - await pumpHomeScreen( - tester, - initialSelectedImages: [XFile('/tmp/mock-image-a.png')], - ); - - expect(find.byKey(homeAttachmentStripKey), findsOneWidget); - }); - - testWidgets( - 'long press release does not stop recorder before start succeeds', - (tester) async { - final recorder = _PermissionDeniedRecorder(); - await pumpHomeScreen(tester, voiceRecorder: recorder); - - final holdArea = find.byKey(messageComposerHoldAreaKey); - expect(holdArea, findsOneWidget); - - final center = tester.getCenter(holdArea); - final gesture = await tester.startGesture(center); - await tester.pump(const Duration(milliseconds: 130)); - await gesture.up(); - await tester.pump(const Duration(milliseconds: 500)); - - expect(recorder.stopCalls, 0); - expect(tester.takeException(), isNull); - await tester.pump(const Duration(seconds: 3)); - }, - ); - - testWidgets('switching to text mode auto focuses input', (tester) async { - await pumpHomeScreen(tester); - - await tester.tap(find.byKey(messageComposerRightButtonKey)); - await tester.pump(); - await tester.pump(); - - final editable = tester.widget(find.byType(EditableText)); - expect(editable.focusNode.hasFocus, isTrue); - }); - - testWidgets('single tap on input keeps text field focused', (tester) async { - await pumpHomeScreen(tester); - - await tester.tap(find.byKey(messageComposerRightButtonKey)); - await tester.pump(); - await tester.pump(); - - await tester.tap(find.byType(EditableText)); - await tester.pump(); - - final editable = tester.widget(find.byType(EditableText)); - expect(editable.focusNode.hasFocus, isTrue); - }); - - testWidgets('switching to text mode triggers keyboard show fallback', ( - tester, - ) async { - var showCalls = 0; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(SystemChannels.textInput, (call) async { - if (call.method == 'TextInput.show') { - showCalls += 1; - } - return null; - }); - addTearDown(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(SystemChannels.textInput, null); - }); - - await pumpHomeScreen(tester); - await tester.tap(find.byKey(messageComposerRightButtonKey)); - await tester.pump(); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 130)); - - expect(showCalls, greaterThanOrEqualTo(1)); - }); - - testWidgets('tap center of input lane focuses text field', (tester) async { - await pumpHomeScreen(tester); - - await tester.tap(find.byKey(messageComposerRightButtonKey)); - await tester.pump(); - await tester.pump(); - - final composerRect = tester.getRect(find.byKey(messageComposerInnerKey)); - final centerLaneTap = Offset( - composerRect.left + composerRect.width * 0.5, - composerRect.center.dy, - ); - - await tester.tapAt(centerLaneTap); - await tester.pump(); - - final editable = tester.widget(find.byType(EditableText)); - expect(editable.focusNode.hasFocus, isTrue); - }); - - testWidgets('tap focused input triggers at most one keyboard show', ( - tester, - ) async { - var showCalls = 0; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(SystemChannels.textInput, (call) async { - if (call.method == 'TextInput.show') { - showCalls += 1; - } - return null; - }); - addTearDown(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(SystemChannels.textInput, null); - }); - - await pumpHomeScreen(tester); - await tester.tap(find.byKey(messageComposerRightButtonKey)); - await tester.pump(); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 130)); - - showCalls = 0; - await tester.tap(find.byType(EditableText)); - await tester.pump(); - - expect(showCalls, lessThanOrEqualTo(1)); - }); - - testWidgets('double toggle returns to hold-to-speak mode', (tester) async { - await pumpHomeScreen(tester); - - await tester.tap(find.byKey(messageComposerRightButtonKey)); - await tester.pump(); - await tester.pump(); - expect(find.byType(EditableText), findsOneWidget); - - await tester.tap(find.byKey(messageComposerRightButtonKey)); - await tester.pump(); - await tester.pump(); - - expect(find.byType(EditableText), findsNothing); - expect(tester.takeException(), isNull); - }); - - testWidgets('rapid triple toggle ends in text mode with focused input', ( - tester, - ) async { - await pumpHomeScreen(tester); - - await tester.tap(find.byKey(messageComposerRightButtonKey)); - await tester.pump(); - await tester.tap(find.byKey(messageComposerRightButtonKey)); - await tester.pump(); - await tester.tap(find.byKey(messageComposerRightButtonKey)); - await tester.pump(); - await tester.pump(); - - final editable = tester.widget(find.byType(EditableText)); - expect(editable.focusNode.hasFocus, isTrue); - expect(tester.takeException(), isNull); - }); - - testWidgets('release during delayed start continues to transcribe path', ( - tester, - ) async { - final recorder = _DelayedSuccessRecorder(); - var transcribeCalls = 0; - await pumpHomeScreen( - tester, - voiceRecorder: recorder, - onTranscribeAudio: (_) async { - transcribeCalls += 1; - return ''; - }, - ); - - final holdArea = find.byKey(messageComposerHoldAreaKey); - final center = tester.getCenter(holdArea); - final gesture = await tester.startGesture(center); - await tester.pump(const Duration(milliseconds: 130)); - await gesture.up(); - await tester.pump(const Duration(milliseconds: 500)); - - expect(recorder.stopCalls, 1); - expect(transcribeCalls, 1); - await tester.pump(const Duration(seconds: 3)); - }); - - testWidgets('cancel during delayed start skips transcribe path', ( - tester, - ) async { - final recorder = _DelayedSuccessRecorder(); - var transcribeCalls = 0; - await pumpHomeScreen( - tester, - voiceRecorder: recorder, - onTranscribeAudio: (_) async { - transcribeCalls += 1; - return 'ignored'; - }, - ); - - final holdArea = find.byKey(messageComposerHoldAreaKey); - final center = tester.getCenter(holdArea); - final gesture = await tester.startGesture(center); - await tester.pump(const Duration(milliseconds: 130)); - await gesture.moveBy(const Offset(0, -90)); - await tester.pump(); - await gesture.up(); - await tester.pump(const Duration(milliseconds: 500)); - - expect(recorder.stopCalls, 1); - expect(transcribeCalls, 0); - }); - - testWidgets( - 'shows unread badge when new message arrives during history reading', - (tester) async { - await pumpHomeScreen(tester); - - final initialItems = buildMessages(30); - chatBloc.emit(const ChatState().copyWith(items: initialItems)); - await tester.pump(const Duration(milliseconds: 700)); - - final position = tester - .state(find.byType(Scrollable)) - .position; - position.jumpTo(0); - await tester.pump(const Duration(milliseconds: 220)); - - final nextItems = [ - ...initialItems, - ...buildMessages(1).map( - (e) => (e as TextMessageItem).copyWith( - id: 'new_1', - content: 'new message', - ), - ), - ]; - chatBloc.emit(const ChatState().copyWith(items: nextItems)); - await tester.pump(const Duration(milliseconds: 700)); - - expect(find.textContaining('有1条新消息'), findsOneWidget); - }, - ); - - testWidgets('tap unread badge scrolls bottom and clears badge', ( - tester, - ) async { - await pumpHomeScreen(tester); - - final initialItems = buildMessages(30); - chatBloc.emit(const ChatState().copyWith(items: initialItems)); - await tester.pump(const Duration(milliseconds: 700)); - - final position = tester - .state(find.byType(Scrollable)) - .position; - position.jumpTo(0); - await tester.pump(const Duration(milliseconds: 220)); - - final nextItems = [ - ...initialItems, - ...buildMessages(1).map( - (e) => (e as TextMessageItem).copyWith( - id: 'new_2', - content: 'new message 2', - ), - ), - ]; - chatBloc.emit(const ChatState().copyWith(items: nextItems)); - await tester.pump(const Duration(milliseconds: 700)); - - await tester.tap(find.textContaining('有1条新消息')); - await tester.pump(const Duration(milliseconds: 700)); - - expect(find.textContaining('有1条新消息'), findsNothing); - }); -} diff --git a/apps/test/features/settings/data/models/automation_job_model_test.dart b/apps/test/features/settings/data/models/automation_job_model_test.dart deleted file mode 100644 index 80ac137..0000000 --- a/apps/test/features/settings/data/models/automation_job_model_test.dart +++ /dev/null @@ -1,130 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:social_app/features/settings/data/models/automation_job_model.dart'; - -void main() { - group('AutomationJobConfigModel', () { - test('fromJson parses schedule correctly', () { - final json = { - 'input_template': 'Hello {{name}}', - 'enabled_tools': ['tool1', 'tool2'], - 'context': { - 'source': 'latest_chat', - 'window_mode': 'day', - 'window_count': 5, - }, - 'schedule': { - 'type': 'weekly', - 'run_at': {'hour': 9, 'minute': 30}, - 'weekdays': [1, 3, 5], - }, - }; - - final model = AutomationJobConfigModel.fromJson(json); - - expect(model.schedule.type, 'weekly'); - expect(model.schedule.runAt.hour, 9); - expect(model.schedule.runAt.minute, 30); - expect(model.schedule.weekdays, [1, 3, 5]); - }); - }); - - group('AutomationJobModel', () { - test('fromJson parses all fields correctly', () { - final json = { - 'id': 'job-123', - 'owner_id': 'user-456', - 'bootstrap_key': 'key-789', - 'title': 'Daily Report', - 'timezone': 'America/New_York', - 'status': 'ACTIVE', - 'is_system': false, - 'config': { - 'input_template': 'Hello', - 'enabled_tools': ['tool1'], - 'context': { - 'source': 'latest_chat', - 'window_mode': 'day', - 'window_count': 2, - }, - 'schedule': { - 'type': 'daily', - 'run_at': {'hour': 9, 'minute': 0}, - }, - }, - 'next_run_at': '2024-01-15T09:00:00Z', - 'last_run_at': '2024-01-14T09:00:00Z', - 'created_at': '2024-01-01T00:00:00Z', - 'updated_at': '2024-01-14T12:00:00Z', - }; - - final model = AutomationJobModel.fromJson(json); - - expect(model.id, 'job-123'); - expect(model.ownerId, 'user-456'); - expect(model.title, 'Daily Report'); - expect(model.config.schedule.type, 'daily'); - expect(model.config.schedule.runAt.hour, 9); - expect(model.timezone, 'America/New_York'); - expect(model.isDaily, isTrue); - expect(model.isWeekly, isFalse); - }); - }); - - group('AutomationJobCreateRequest', () { - test('toJson serializes schedule under config', () { - final request = AutomationJobCreateRequest( - title: 'New Job', - timezone: 'UTC', - status: 'ACTIVE', - config: AutomationJobConfigModel( - inputTemplate: 'Hello', - enabledTools: ['tool1'], - context: MessageContextConfigModel( - source: 'latest_chat', - windowMode: 'day', - windowCount: 2, - ), - schedule: ScheduleConfigModel( - type: 'daily', - runAt: ScheduleRunAtModel(hour: 10, minute: 0), - ), - ), - ); - - final json = request.toJson(); - - expect(json['title'], 'New Job'); - expect(json['timezone'], 'UTC'); - expect(json['status'], 'ACTIVE'); - expect((json['config'] as Map)['schedule'], { - 'type': 'daily', - 'run_at': {'hour': 10, 'minute': 0}, - }); - expect(json.containsKey('run_at'), isFalse); - expect(json.containsKey('schedule_type'), isFalse); - }); - }); - - group('AutomationJobUpdateRequest', () { - test('toJson includes schedule patch in config', () { - final request = AutomationJobUpdateRequest( - config: AutomationJobConfigPatchModel( - schedule: ScheduleConfigModel( - type: 'weekly', - runAt: ScheduleRunAtModel(hour: 8, minute: 0), - weekdays: [2, 4], - ), - ), - ); - - final json = request.toJson(); - final configJson = json['config'] as Map; - - expect(configJson['schedule'], { - 'type': 'weekly', - 'run_at': {'hour': 8, 'minute': 0}, - 'weekdays': [2, 4], - }); - }); - }); -} diff --git a/apps/test/features/settings/data/services/settings_user_cache_test.dart b/apps/test/features/settings/data/services/settings_user_cache_test.dart deleted file mode 100644 index c9467af..0000000 --- a/apps/test/features/settings/data/services/settings_user_cache_test.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:social_app/core/cache/cache_policy.dart'; -import 'package:social_app/core/cache/hybrid_cache_store.dart'; -import 'package:social_app/core/cache/memory_cache_store.dart'; -import 'package:social_app/core/cache/persistent_cache_store.dart'; -import 'package:social_app/features/settings/data/services/settings_user_cache.dart'; -import 'package:social_app/features/settings/data/services/user_profile_cache_repository.dart'; -import 'package:social_app/features/users/data/models/user_response.dart'; - -void main() { - test('getProfile caches latest user in memory field', () async { - var loadCalls = 0; - final repository = UserProfileCacheRepository( - store: HybridCacheStore( - memory: MemoryCacheStore(), - persistent: PersistentCacheStore(), - ), - policy: const CachePolicy( - softTtl: Duration(minutes: 2), - hardTtl: Duration(minutes: 30), - minRefreshInterval: Duration(minutes: 1), - ), - remoteLoader: () async { - loadCalls += 1; - return const UserResponse(id: 'u1', username: 'first'); - }, - ); - final cache = SettingsUserCache(repository); - - final first = await cache.getProfile(); - final second = await cache.getProfile(); - - expect(first.username, 'first'); - expect(second.username, 'first'); - expect(cache.cachedUser?.id, 'u1'); - expect(loadCalls, 1); - }); - - test('invalidate clears memory cache', () { - final repository = UserProfileCacheRepository( - store: HybridCacheStore( - memory: MemoryCacheStore(), - persistent: PersistentCacheStore(), - ), - remoteLoader: () async => const UserResponse(id: 'u1', username: 'first'), - ); - final cache = SettingsUserCache(repository); - - cache.set(const UserResponse(id: 'u1', username: 'first')); - cache.invalidate(); - - expect(cache.cachedUser, isNull); - }); - - test('set should update cached user immediately', () { - final repository = UserProfileCacheRepository( - store: HybridCacheStore( - memory: MemoryCacheStore(), - persistent: PersistentCacheStore(), - ), - remoteLoader: () async => const UserResponse(id: 'u1', username: 'first'), - ); - final cache = SettingsUserCache(repository); - - cache.set(const UserResponse(id: 'u2', username: 'next')); - - expect(cache.cachedUser?.id, 'u2'); - }); -} diff --git a/apps/test/features/settings/data/services/user_profile_cache_repository_test.dart b/apps/test/features/settings/data/services/user_profile_cache_repository_test.dart deleted file mode 100644 index 97a082e..0000000 --- a/apps/test/features/settings/data/services/user_profile_cache_repository_test.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:social_app/core/cache/cache_entry.dart'; -import 'package:social_app/core/cache/cache_policy.dart'; -import 'package:social_app/core/cache/hybrid_cache_store.dart'; -import 'package:social_app/core/cache/memory_cache_store.dart'; -import 'package:social_app/core/cache/persistent_cache_store.dart'; -import 'package:social_app/features/settings/data/services/user_profile_cache_repository.dart'; -import 'package:social_app/features/users/data/models/user_response.dart'; - -void main() { - test( - 'repository should return persistent cache first then refresh in background', - () async { - final store = HybridCacheStore( - memory: MemoryCacheStore(), - persistent: PersistentCacheStore(), - ); - const key = UserProfileCacheRepository.cacheKey; - final stale = CacheEntry( - value: const UserResponse(id: 'u1', username: 'cached'), - fetchedAt: DateTime(2026, 3, 20, 11, 0), - ); - await store.persistent.write>(key, stale); - - var refreshCalls = 0; - final repository = UserProfileCacheRepository( - store: store, - now: () => DateTime(2026, 3, 20, 11, 5), - policy: const CachePolicy( - softTtl: Duration(minutes: 2), - hardTtl: Duration(minutes: 30), - minRefreshInterval: Duration(minutes: 1), - ), - remoteLoader: () async { - refreshCalls += 1; - return const UserResponse(id: 'u1', username: 'remote'); - }, - ); - - final result = await repository.getProfile(); - await Future.delayed(const Duration(milliseconds: 10)); - - expect(result.username, 'cached'); - expect(refreshCalls, 1); - }, - ); -} diff --git a/apps/test/features/settings/presentation/cubits/automation_jobs_cubit_test.dart b/apps/test/features/settings/presentation/cubits/automation_jobs_cubit_test.dart deleted file mode 100644 index eb5c107..0000000 --- a/apps/test/features/settings/presentation/cubits/automation_jobs_cubit_test.dart +++ /dev/null @@ -1,167 +0,0 @@ -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:social_app/features/settings/data/models/automation_job_model.dart'; -import 'package:social_app/features/settings/data/services/automation_jobs_api.dart'; -import 'package:social_app/features/settings/presentation/cubits/automation_jobs_cubit.dart'; - -class MockAutomationJobsApi extends Mock implements AutomationJobsApi {} - -class FakeAutomationJobUpdateRequest extends Fake - implements AutomationJobUpdateRequest {} - -void main() { - late AutomationJobsCubit cubit; - late MockAutomationJobsApi mockApi; - - final testJob = AutomationJobModel( - id: '1', - ownerId: 'owner1', - title: 'Test Job', - timezone: 'UTC', - status: 'ACTIVE', - isSystem: false, - config: AutomationJobConfigModel( - inputTemplate: '', - enabledTools: const [], - context: MessageContextConfigModel( - source: 'latest_chat', - windowMode: 'day', - windowCount: 2, - ), - schedule: ScheduleConfigModel( - type: 'daily', - runAt: ScheduleRunAtModel(hour: 8, minute: 0), - ), - ), - nextRunAt: DateTime(2024, 1, 1), - createdAt: DateTime(2024, 1, 1), - updatedAt: DateTime(2024, 1, 1), - ); - - setUpAll(() { - registerFallbackValue(FakeAutomationJobUpdateRequest()); - }); - - setUp(() { - mockApi = MockAutomationJobsApi(); - cubit = AutomationJobsCubit(mockApi); - }); - - tearDown(() { - cubit.close(); - }); - - group('AutomationJobsCubit', () { - test('initial state is correct', () { - expect(cubit.state.jobs, isEmpty); - expect(cubit.state.isLoading, isFalse); - expect(cubit.state.error, isNull); - }); - - blocTest( - 'loadJobs success emits loading then jobs', - build: () { - when(() => mockApi.list()).thenAnswer((_) async => [testJob]); - return cubit; - }, - act: (c) => c.loadJobs(), - expect: () => [ - isA().having( - (s) => s.isLoading, - 'isLoading', - true, - ), - isA() - .having((s) => s.isLoading, 'isLoading', false) - .having((s) => s.jobs, 'jobs', [testJob]), - ], - ); - - blocTest( - 'loadJobs failure emits loading then error', - build: () { - when(() => mockApi.list()).thenThrow(Exception('Network error')); - return cubit; - }, - act: (c) => c.loadJobs(), - expect: () => [ - isA().having( - (s) => s.isLoading, - 'isLoading', - true, - ), - isA() - .having((s) => s.isLoading, 'isLoading', false) - .having((s) => s.error, 'error', isNotNull), - ], - ); - - blocTest( - 'deleteJob success calls loadJobs to refresh', - build: () { - when(() => mockApi.delete(any())).thenAnswer((_) async {}); - when(() => mockApi.list()).thenAnswer((_) async => []); - return cubit; - }, - act: (c) => c.deleteJob('1'), - verify: (_) { - verify(() => mockApi.delete('1')).called(1); - verify(() => mockApi.list()).called(1); - }, - ); - - blocTest( - 'deleteJob failure emits error without refreshing', - build: () { - when(() => mockApi.delete(any())).thenThrow(Exception('Delete failed')); - return cubit; - }, - act: (c) => c.deleteJob('1'), - expect: () => [ - isA().having((s) => s.error, 'error', isNotNull), - ], - verify: (_) { - verify(() => mockApi.delete('1')).called(1); - verifyNever(() => mockApi.list()); - }, - ); - - blocTest( - 'updateJobStatus success replaces target job', - build: () { - when( - () => mockApi.update(any(), any()), - ).thenAnswer((_) async => testJob.copyWith(status: 'disabled')); - return cubit; - }, - seed: () => AutomationJobsState(jobs: [testJob]), - act: (c) => c.updateJobStatus(id: '1', enabled: false), - expect: () => [ - isA().having( - (s) => s.jobs.first.status, - 'updated status', - 'disabled', - ), - ], - verify: (_) { - verify(() => mockApi.update('1', any())).called(1); - }, - ); - - blocTest( - 'updateJobStatus failure emits error', - build: () { - when( - () => mockApi.update(any(), any()), - ).thenThrow(Exception('Update failed')); - return cubit; - }, - seed: () => AutomationJobsState(jobs: [testJob]), - act: (c) => c.updateJobStatus(id: '1', enabled: false), - expect: () => [ - isA().having((s) => s.error, 'error', isNotNull), - ], - ); - }); -} diff --git a/apps/test/features/settings/presentation/cubits/job_detail_cubit_test.dart b/apps/test/features/settings/presentation/cubits/job_detail_cubit_test.dart deleted file mode 100644 index df068b2..0000000 --- a/apps/test/features/settings/presentation/cubits/job_detail_cubit_test.dart +++ /dev/null @@ -1,244 +0,0 @@ -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:social_app/features/settings/data/models/automation_job_model.dart'; -import 'package:social_app/features/settings/data/services/automation_jobs_api.dart'; -import 'package:social_app/features/settings/presentation/cubits/job_detail_cubit.dart'; - -class MockAutomationJobsApi extends Mock implements AutomationJobsApi {} - -class FakeAutomationJobUpdateRequest extends Fake - implements AutomationJobUpdateRequest {} - -class FakeAutomationJobCreateRequest extends Fake - implements AutomationJobCreateRequest {} - -void main() { - late JobDetailCubit cubit; - late MockAutomationJobsApi mockApi; - - final testJob = AutomationJobModel( - id: '1', - ownerId: 'owner1', - title: 'Test Job', - timezone: 'UTC', - status: 'ACTIVE', - isSystem: false, - config: AutomationJobConfigModel( - inputTemplate: '', - enabledTools: const [], - context: MessageContextConfigModel( - source: 'latest_chat', - windowMode: 'day', - windowCount: 2, - ), - schedule: ScheduleConfigModel( - type: 'daily', - runAt: ScheduleRunAtModel(hour: 8, minute: 0), - ), - ), - nextRunAt: DateTime(2024, 1, 1), - createdAt: DateTime(2024, 1, 1), - updatedAt: DateTime(2024, 1, 1), - ); - - setUpAll(() { - registerFallbackValue(FakeAutomationJobUpdateRequest()); - registerFallbackValue(FakeAutomationJobCreateRequest()); - }); - - setUp(() { - mockApi = MockAutomationJobsApi(); - cubit = JobDetailCubit(mockApi); - }); - - tearDown(() { - cubit.close(); - }); - - group('JobDetailCubit', () { - test('initial state is correct', () { - expect(cubit.state.job, isNull); - expect(cubit.state.isLoading, isFalse); - expect(cubit.state.isSaving, isFalse); - expect(cubit.state.error, isNull); - }); - - blocTest( - 'loadJob success emits loading then job', - build: () { - when(() => mockApi.get(any())).thenAnswer((_) async => testJob); - return cubit; - }, - act: (c) => c.loadJob('1'), - expect: () => [ - isA().having((s) => s.isLoading, 'isLoading', true), - isA() - .having((s) => s.isLoading, 'isLoading', false) - .having((s) => s.job, 'job', testJob), - ], - ); - - blocTest( - 'loadJob failure emits loading then error', - build: () { - when(() => mockApi.get(any())).thenThrow(Exception('Network error')); - return cubit; - }, - act: (c) => c.loadJob('1'), - expect: () => [ - isA().having((s) => s.isLoading, 'isLoading', true), - isA() - .having((s) => s.isLoading, 'isLoading', false) - .having((s) => s.error, 'error', isNotNull), - ], - ); - - blocTest( - 'updateJob success emits saving then job with saving false', - build: () { - when( - () => mockApi.update(any(), any()), - ).thenAnswer((_) async => testJob); - return cubit; - }, - act: (c) => c.updateJob('1', AutomationJobUpdateRequest()), - expect: () => [ - isA().having((s) => s.isSaving, 'isSaving', true), - isA() - .having((s) => s.isSaving, 'isSaving', false) - .having((s) => s.job, 'job', testJob), - ], - ); - - blocTest( - 'updateJob failure emits saving then error', - build: () { - when( - () => mockApi.update(any(), any()), - ).thenThrow(Exception('Update failed')); - return cubit; - }, - act: (c) => c.updateJob('1', AutomationJobUpdateRequest()), - expect: () => [ - isA().having((s) => s.isSaving, 'isSaving', true), - isA() - .having((s) => s.isSaving, 'isSaving', false) - .having((s) => s.error, 'error', isNotNull), - ], - ); - - blocTest( - 'deleteJob success emits saving then saving false', - build: () { - when(() => mockApi.delete(any())).thenAnswer((_) async {}); - return cubit; - }, - act: (c) => c.deleteJob('1'), - expect: () => [ - isA() - .having((s) => s.isSaving, 'isSaving', true) - .having((s) => s.error, 'error', isNull), - isA().having((s) => s.isSaving, 'isSaving', false), - ], - verify: (_) { - verify(() => mockApi.delete('1')).called(1); - }, - ); - - blocTest( - 'deleteJob failure emits saving then error', - build: () { - when(() => mockApi.delete(any())).thenThrow(Exception('Delete failed')); - return cubit; - }, - act: (c) => c.deleteJob('1'), - expect: () => [ - isA() - .having((s) => s.isSaving, 'isSaving', true) - .having((s) => s.error, 'error', isNull), - isA() - .having((s) => s.isSaving, 'isSaving', false) - .having((s) => s.error, 'error', isNotNull), - ], - verify: (_) { - verify(() => mockApi.delete('1')).called(1); - }, - ); - - blocTest( - 'createJob success emits saving then created job', - build: () { - when(() => mockApi.create(any())).thenAnswer((_) async => testJob); - return cubit; - }, - act: (c) => c.createJob( - AutomationJobCreateRequest( - title: 'New Job', - timezone: 'Asia/Shanghai', - status: 'active', - config: AutomationJobConfigModel( - inputTemplate: 'hello', - enabledTools: const ['memory.write'], - context: MessageContextConfigModel( - source: 'latest_chat', - windowMode: 'day', - windowCount: 2, - ), - schedule: ScheduleConfigModel( - type: 'daily', - runAt: ScheduleRunAtModel(hour: 8, minute: 0), - ), - ), - ), - ), - expect: () => [ - isA() - .having((s) => s.isSaving, 'isSaving', true) - .having((s) => s.error, 'error', isNull), - isA() - .having((s) => s.isSaving, 'isSaving', false) - .having((s) => s.job, 'job', testJob), - ], - verify: (_) { - verify(() => mockApi.create(any())).called(1); - }, - ); - - blocTest( - 'createJob failure emits saving then error', - build: () { - when(() => mockApi.create(any())).thenThrow(Exception('Create failed')); - return cubit; - }, - act: (c) => c.createJob( - AutomationJobCreateRequest( - title: 'New Job', - timezone: 'Asia/Shanghai', - status: 'active', - config: AutomationJobConfigModel( - inputTemplate: 'hello', - enabledTools: const ['memory.write'], - context: MessageContextConfigModel( - source: 'latest_chat', - windowMode: 'day', - windowCount: 2, - ), - schedule: ScheduleConfigModel( - type: 'daily', - runAt: ScheduleRunAtModel(hour: 8, minute: 0), - ), - ), - ), - ), - expect: () => [ - isA() - .having((s) => s.isSaving, 'isSaving', true) - .having((s) => s.error, 'error', isNull), - isA() - .having((s) => s.isSaving, 'isSaving', false) - .having((s) => s.error, 'error', isNotNull), - ], - ); - }); -} diff --git a/apps/test/features/settings/ui/screens/settings_screen_test.dart b/apps/test/features/settings/ui/screens/settings_screen_test.dart deleted file mode 100644 index abd1f57..0000000 --- a/apps/test/features/settings/ui/screens/settings_screen_test.dart +++ /dev/null @@ -1,153 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:social_app/core/cache/hybrid_cache_store.dart'; -import 'package:social_app/core/cache/memory_cache_store.dart'; -import 'package:social_app/core/cache/persistent_cache_store.dart'; -import 'package:social_app/core/api/i_api_client.dart'; -import 'package:social_app/core/di/injection.dart'; -import 'package:social_app/features/friends/data/friends_api.dart'; -import 'package:social_app/features/settings/data/services/settings_user_cache.dart'; -import 'package:social_app/features/settings/data/services/user_profile_cache_repository.dart'; -import 'package:social_app/features/settings/ui/screens/settings_screen.dart'; -import 'package:social_app/features/users/data/models/user_response.dart'; -import 'package:social_app/features/users/data/users_api.dart'; - -class _TestApiClient implements IApiClient { - @override - Future> delete(String path, {data, Options? options}) async { - return Response(requestOptions: RequestOptions(path: path)); - } - - @override - Future> get(String path, {Options? options}) async { - return Response(requestOptions: RequestOptions(path: path)); - } - - @override - Future> getSseLines( - String path, { - Map? headers, - }) async { - return const Stream.empty(); - } - - @override - Future> patch(String path, {data, Options? options}) async { - return Response(requestOptions: RequestOptions(path: path)); - } - - @override - Future> post(String path, {data, Options? options}) async { - return Response(requestOptions: RequestOptions(path: path)); - } - - @override - Future> put(String path, {data, Options? options}) async { - return Response(requestOptions: RequestOptions(path: path)); - } -} - -class _FakeUsersApi extends UsersApi { - _FakeUsersApi(super.client); - - int getMeCalls = 0; - - @override - Future getMe() async { - getMeCalls += 1; - return const UserResponse( - id: 'u1', - username: 'Linksy', - phone: '13800000000', - ); - } -} - -class _FakeFriendsApi extends FriendsApi { - _FakeFriendsApi(super.client); - - @override - Future> getFriends() async { - return const []; - } -} - -void main() { - late _FakeUsersApi usersApi; - - setUp(() { - final apiClient = _TestApiClient(); - if (sl.isRegistered()) { - sl.unregister(); - } - if (sl.isRegistered()) { - sl.unregister(); - } - if (sl.isRegistered()) { - sl.unregister(); - } - if (sl.isRegistered()) { - sl.unregister(); - } - usersApi = _FakeUsersApi(apiClient); - final repository = UserProfileCacheRepository( - store: HybridCacheStore( - memory: MemoryCacheStore(), - persistent: PersistentCacheStore(), - ), - remoteLoader: usersApi.getMe, - ); - sl.registerSingleton(usersApi); - sl.registerSingleton(_FakeFriendsApi(apiClient)); - sl.registerSingleton(repository); - sl.registerSingleton(SettingsUserCache(repository)); - }); - - tearDown(() async { - if (sl.isRegistered()) { - await sl.unregister(); - } - if (sl.isRegistered()) { - await sl.unregister(); - } - if (sl.isRegistered()) { - await sl.unregister(); - } - if (sl.isRegistered()) { - await sl.unregister(); - } - }); - - testWidgets('settings screen removes account row and shows logout button', ( - tester, - ) async { - await tester.pumpWidget(const MaterialApp(home: SettingsScreen())); - await tester.pump(); - - expect(find.text('我的账户'), findsNothing); - expect(find.text('退出登录'), findsOneWidget); - }); - - testWidgets('settings profile hero shows edit icon entry', (tester) async { - await tester.pumpWidget(const MaterialApp(home: SettingsScreen())); - await tester.pump(); - - expect(find.byKey(settingsProfileEditButtonKey), findsOneWidget); - }); - - testWidgets('settings screen re-entry uses cached user', (tester) async { - await tester.pumpWidget(const MaterialApp(home: SettingsScreen())); - await tester.pump(); - await tester.pump(); - - await tester.pumpWidget(const SizedBox.shrink()); - await tester.pump(); - - await tester.pumpWidget(const MaterialApp(home: SettingsScreen())); - await tester.pump(); - await tester.pump(); - - expect(usersApi.getMeCalls, 1); - }); -} diff --git a/apps/test/features/todo/quadrant_drag_test.dart b/apps/test/features/todo/quadrant_drag_test.dart deleted file mode 100644 index 20c1acd..0000000 --- a/apps/test/features/todo/quadrant_drag_test.dart +++ /dev/null @@ -1,176 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:social_app/features/todo/data/todo_api.dart'; -import 'package:social_app/core/api/i_api_client.dart'; - -class MockApiClient extends Mock implements IApiClient {} - -class FakeRequestOptions extends Fake implements RequestOptions {} - -void main() { - late TodoApi todoApi; - late MockApiClient mockClient; - - setUpAll(() { - registerFallbackValue(FakeRequestOptions()); - }); - - setUp(() { - mockClient = MockApiClient(); - todoApi = TodoApi(mockClient); - }); - - group('TodoApi.updateTodo - cross-quadrant drag', () { - test( - 'calls PATCH with priority when moving to different quadrant', - () async { - const todoId = 'todo-123'; - const targetPriority = 2; - const targetOrder = 0; - - when( - () => mockClient.patch(any(), data: any(named: 'data')), - ).thenAnswer( - (_) async => Response( - requestOptions: RequestOptions(path: '/api/v1/todos/$todoId'), - data: { - 'id': todoId, - 'owner_id': 'user-1', - 'title': 'Test Todo', - 'priority': targetPriority, - 'order': targetOrder, - 'status': 'pending', - 'created_at': '2024-01-01T00:00:00Z', - 'updated_at': '2024-01-01T00:00:00Z', - }, - ), - ); - - final result = await todoApi.updateTodo( - todoId, - priority: targetPriority, - order: targetOrder, - ); - - expect(result.priority, targetPriority); - expect(result.order, targetOrder); - verify( - () => mockClient.patch( - '/api/v1/todos/$todoId', - data: {'priority': targetPriority, 'order': targetOrder}, - ), - ).called(1); - }, - ); - - test('throws when API fails - triggers rollback', () async { - const todoId = 'todo-123'; - - when( - () => mockClient.patch(any(), data: any(named: 'data')), - ).thenThrow(Exception('Network error')); - - expect( - () => todoApi.updateTodo(todoId, priority: 2, order: 0), - throwsException, - ); - }); - }); - - group('Quadrant priority mapping', () { - test('priority 1 = important urgent (Q1)', () async { - when(() => mockClient.patch(any(), data: any(named: 'data'))).thenAnswer( - (_) async => Response( - requestOptions: RequestOptions(path: '/api/v1/todos/todo-1'), - data: { - 'id': 'todo-1', - 'owner_id': 'user-1', - 'title': 'Q1 Todo', - 'priority': 1, - 'order': 1, - 'status': 'pending', - 'created_at': '2024-01-01T00:00:00Z', - 'updated_at': '2024-01-01T00:00:00Z', - }, - ), - ); - - final result = await todoApi.updateTodo('todo-1', priority: 1, order: 1); - expect(result.priority, 1); - expect(result.order, 1); - }); - - test('priority 2 = important not urgent (Q3)', () async { - when(() => mockClient.patch(any(), data: any(named: 'data'))).thenAnswer( - (_) async => Response( - requestOptions: RequestOptions(path: '/api/v1/todos/todo-2'), - data: { - 'id': 'todo-2', - 'owner_id': 'user-1', - 'title': 'Q3 Todo', - 'priority': 2, - 'order': 2, - 'status': 'pending', - 'created_at': '2024-01-01T00:00:00Z', - 'updated_at': '2024-01-01T00:00:00Z', - }, - ), - ); - - final result = await todoApi.updateTodo('todo-2', priority: 2, order: 2); - expect(result.priority, 2); - expect(result.order, 2); - }); - - test('priority 3 = urgent not important (Q2)', () async { - when(() => mockClient.patch(any(), data: any(named: 'data'))).thenAnswer( - (_) async => Response( - requestOptions: RequestOptions(path: '/api/v1/todos/todo-3'), - data: { - 'id': 'todo-3', - 'owner_id': 'user-1', - 'title': 'Q2 Todo', - 'priority': 3, - 'order': 0, - 'status': 'pending', - 'created_at': '2024-01-01T00:00:00Z', - 'updated_at': '2024-01-01T00:00:00Z', - }, - ), - ); - - final result = await todoApi.updateTodo('todo-3', priority: 3, order: 0); - expect(result.priority, 3); - expect(result.order, 0); - }); - }); - - group('TodoApi.reorderTodos', () { - test('calls batch reorder endpoint once', () async { - when(() => mockClient.patch(any(), data: any(named: 'data'))).thenAnswer( - (_) async => Response( - requestOptions: RequestOptions(path: '/api/v1/todos/reorder'), - data: {}, - ), - ); - - await todoApi.reorderTodos(const [ - TodoReorderItemPayload(id: 'todo-1', priority: 1, order: 0), - TodoReorderItemPayload(id: 'todo-2', priority: 1, order: 1), - ]); - - verify( - () => mockClient.patch( - '/api/v1/todos/reorder', - data: { - 'items': [ - {'id': 'todo-1', 'priority': 1, 'order': 0}, - {'id': 'todo-2', 'priority': 1, 'order': 1}, - ], - }, - ), - ).called(1); - }); - }); -} diff --git a/apps/test/features/todo/todo_repository_test.dart b/apps/test/features/todo/todo_repository_test.dart deleted file mode 100644 index ffa633d..0000000 --- a/apps/test/features/todo/todo_repository_test.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:social_app/core/cache/cache_entry.dart'; -import 'package:social_app/core/cache/cache_invalidator.dart'; -import 'package:social_app/core/cache/hybrid_cache_store.dart'; -import 'package:social_app/core/cache/memory_cache_store.dart'; -import 'package:social_app/core/cache/persistent_cache_store.dart'; -import 'package:social_app/features/todo/data/todo_api.dart'; -import 'package:social_app/features/todo/data/todo_repository.dart'; - -class _MockTodoApi extends Mock implements TodoApi {} - -void main() { - test( - 'complete todo should optimistically remove item and invalidate pending list key', - () async { - final api = _MockTodoApi(); - final store = HybridCacheStore( - memory: MemoryCacheStore(), - persistent: PersistentCacheStore(), - ); - final invalidator = CacheInvalidator(store: store); - final repository = TodoRepository( - api: api, - store: store, - invalidator: invalidator, - ); - - final cached = TodoResponse( - id: 'todo_1', - ownerId: 'u1', - title: 't1', - priority: 1, - order: 0, - status: 'pending', - createdAt: DateTime(2026, 3, 20, 10), - updatedAt: DateTime(2026, 3, 20, 10), - ); - await store.write>>( - TodoRepository.pendingListKey, - CacheEntry(value: [cached], fetchedAt: DateTime(2026, 3, 20, 10, 0)), - ); - - when( - () => api.completeTodo('todo_1'), - ).thenAnswer((_) async => cached.copyWith(status: 'completed')); - - await repository.completeTodo('todo_1'); - - final updated = await store.read>>( - TodoRepository.pendingListKey, - ); - expect(updated, isNull); - expect(invalidator.wasInvalidated(TodoRepository.pendingListKey), true); - }, - ); -} diff --git a/apps/test/platform/android_manifest_notification_action_test.dart b/apps/test/platform/android_manifest_notification_action_test.dart deleted file mode 100644 index 4b30675..0000000 --- a/apps/test/platform/android_manifest_notification_action_test.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'dart:io'; - -import 'package:flutter_test/flutter_test.dart'; - -void main() { - test('AndroidManifest declares ActionBroadcastReceiver', () { - final manifestFile = File('android/app/src/main/AndroidManifest.xml'); - - expect(manifestFile.existsSync(), isTrue); - - final manifestContent = manifestFile.readAsStringSync(); - - expect( - manifestContent, - contains( - 'com.dexterous.flutterlocalnotifications.ActionBroadcastReceiver', - ), - ); - }); -} diff --git a/apps/test/platform/ios_app_delegate_notification_callback_test.dart b/apps/test/platform/ios_app_delegate_notification_callback_test.dart deleted file mode 100644 index 5d2429a..0000000 --- a/apps/test/platform/ios_app_delegate_notification_callback_test.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'dart:io'; - -import 'package:flutter_test/flutter_test.dart'; - -void main() { - test('AppDelegate registers flutter local notifications callback', () { - final appDelegateFile = File('ios/Runner/AppDelegate.swift'); - - expect(appDelegateFile.existsSync(), isTrue); - - final appDelegateContent = appDelegateFile.readAsStringSync(); - - expect( - appDelegateContent, - contains('FlutterLocalNotificationsPlugin.setPluginRegistrantCallback'), - ); - expect( - appDelegateContent, - contains('GeneratedPluginRegistrant.register(with: registry)'), - ); - }); -} diff --git a/apps/test/shared/utils/phone_display_formatter_test.dart b/apps/test/shared/utils/phone_display_formatter_test.dart deleted file mode 100644 index 5bc59ba..0000000 --- a/apps/test/shared/utils/phone_display_formatter_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:social_app/shared/utils/phone_display_formatter.dart'; - -void main() { - group('formatPhoneForDisplay', () { - test('formats +86 numbers as local masked style', () { - final formatted = formatPhoneForDisplay('+8613812345678'); - - expect(formatted, '138****5678'); - }); - - test('keeps international country code while masking middle part', () { - final formatted = formatPhoneForDisplay('+14155552671'); - - expect(formatted, '+1 ****2671'); - }); - - test('normalizes separators before formatting', () { - final formatted = formatPhoneForDisplay('(+86) 138-1234-5678'); - - expect(formatted, '138****5678'); - }); - - test('prefers longer country code in fallback detection', () { - final formatted = formatPhoneForDisplay('+33612345678'); - - expect(formatted, '+33 ****5678'); - }); - }); -} diff --git a/apps/test/shared/utils/tool_name_localizer_test.dart b/apps/test/shared/utils/tool_name_localizer_test.dart deleted file mode 100644 index 43dd05c..0000000 --- a/apps/test/shared/utils/tool_name_localizer_test.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:social_app/shared/utils/tool_name_localizer.dart'; - -void main() { - group('localizeToolName', () { - test('translates dot style tool names', () { - expect(localizeToolName('memory.write'), '写入记忆'); - expect(localizeToolName('calendar.read'), '读取日程'); - }); - - test('translates snake style aliases', () { - expect(localizeToolName('memory_write'), '写入记忆'); - expect(localizeToolName('calendar_read'), '读取日程'); - }); - - test('returns raw name for unknown tool', () { - expect(localizeToolName('unknown.tool'), 'unknown.tool'); - }); - }); -}