From a852ce78486adac82774b77668e6bf2b5df9201d Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Sat, 23 Aug 2025 20:09:43 +0530 Subject: [PATCH] feat: localisation with en, de, fr and it --- .gitignore | 1 - AGENTS.md | 23 + README.md | 66 +- lib/core/error/enhanced_error_service.dart | 11 +- lib/core/providers/app_providers.dart | 30 + .../services/optimized_storage_service.dart | 15 + .../services/user_friendly_error_handler.dart | 12 +- lib/core/widgets/error_boundary.dart | 7 +- .../auth/views/authentication_page.dart | 38 +- .../auth/views/connect_signin_page.dart | 7 +- .../auth/views/server_connection_page.dart | 49 +- .../widgets/assistant_message_widget.dart | 3 +- .../widgets/enhanced_image_attachment.dart | 19 +- .../chat/widgets/modern_chat_input.dart | 25 +- lib/features/files/views/workspace_page.dart | 44 +- .../onboarding/views/onboarding_sheet.dart | 78 +- lib/features/profile/views/profile_page.dart | 183 +++- lib/l10n/app_de.arb | 145 +++ lib/l10n/app_en.arb | 169 +++ lib/l10n/app_fr.arb | 145 +++ lib/l10n/app_it.arb | 145 +++ lib/l10n/app_localizations.dart | 969 ++++++++++++++++++ lib/l10n/app_localizations_de.dart | 444 ++++++++ lib/l10n/app_localizations_en.dart | 444 ++++++++ lib/l10n/app_localizations_fr.dart | 444 ++++++++ lib/l10n/app_localizations_it.dart | 444 ++++++++ lib/main.dart | 35 +- lib/shared/utils/ui_utils.dart | 15 +- lib/shared/widgets/conduit_components.dart | 10 +- .../widgets/improved_loading_states.dart | 9 +- lib/shared/widgets/loading_states.dart | 7 +- .../widgets/markdown/markdown_config.dart | 11 +- lib/shared/widgets/offline_indicator.dart | 13 +- lib/shared/widgets/optimized_list.dart | 23 +- pubspec.lock | 13 + pubspec.yaml | 19 + 36 files changed, 3912 insertions(+), 203 deletions(-) create mode 100644 AGENTS.md create mode 100644 lib/l10n/app_de.arb create mode 100644 lib/l10n/app_en.arb create mode 100644 lib/l10n/app_fr.arb create mode 100644 lib/l10n/app_it.arb create mode 100644 lib/l10n/app_localizations.dart create mode 100644 lib/l10n/app_localizations_de.dart create mode 100644 lib/l10n/app_localizations_en.dart create mode 100644 lib/l10n/app_localizations_fr.dart create mode 100644 lib/l10n/app_localizations_it.dart diff --git a/.gitignore b/.gitignore index f976826..c6e013c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ .svn/ .swiftpm/ migrate_working_dir/ -AGENTS.md # IntelliJ related *.iml diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..9404256 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,23 @@ +# Conduit - Flutter Mobile App for Open-WebUI + +## Build & Test Commands +```bash +flutter pub get # Install dependencies +flutter pub run build_runner build --delete-conflicting-outputs # Generate code +flutter analyze # Run static analysis +flutter run -d ios/android # Run debug build +flutter build apk --release # Build Android release +flutter build ipa --release # Build iOS release +``` + +## Code Style Guidelines +- **State Management**: Use Riverpod providers in `providers/` folders +- **Architecture**: Follow clean architecture - `core/`, `features/`, `shared/` +- **Imports**: Group by package/relative, use absolute paths for project files +- **Models**: Use Freezed for data classes with `.freezed.dart` and `.g.dart` generated files +- **Error Handling**: Use ApiErrorHandler and error interceptors, avoid print statements +- **Naming**: snake_case files, PascalCase classes, camelCase methods/variables +- **Async**: Prefer async/await over raw Futures, handle errors with try-catch +- **Widgets**: Separate presentation (widgets/) from business logic (services/) +- **UI Design**: Use AppTheme colors/styles and ConduitThemeExtension for consistent design +- **Dependencies**: Check pubspec.yaml before adding packages - prefer existing solutions \ No newline at end of file diff --git a/README.md b/README.md index 7775b8d..d6ff839 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,70 @@ The app will request permissions for: - Camera access - Photo library access +## Localization (i18n) + +- Supported locales: `en`, `de`, `fr`, `it`. +- Uses Flutter's `gen_l10n` with ARB files and the `intl` package for date/number formatting. + +### Install & Generate + +- Install packages: + - `flutter_localizations` (Flutter SDK) + - `intl: ^0.20.2` +- Files are under `lib/l10n/*.arb`. The template is `app_en.arb`. +- Generate localizations: + - `flutter gen-l10n` + - or run a full build: `flutter pub get && flutter gen-l10n` + +### Usage Examples + +- Basic text: + - `Text(AppLocalizations.of(context)!.appTitle)` +- With placeholder: + - `Text(AppLocalizations.of(context)!.dynamicContentWithPlaceholder('Alex'))` +- Pluralization: + - `Text(AppLocalizations.of(context)!.itemsCount(3))` +- Date/time formatting: + - `final dateText = DateFormat.yMMMMEEEEd(Localizations.localeOf(context).toString()).format(DateTime.now());` + - `Text(dateText)` +- Number formatting: + - `final price = NumberFormat.currency(locale: Localizations.localeOf(context).toString(), symbol: '€').format(1234.56);` + - `Text(price)` + +### Add a New Language + +- Create a new ARB file in `lib/l10n/`, e.g. `app_es.arb`. +- Copy keys from `app_en.arb` and provide translated values. +- Ensure placeholders and plural rules match the template. +- Add the locale to `supportedLocales` in `MaterialApp` (see `lib/main.dart`). +- Regenerate: `flutter gen-l10n`. + +### Best Practices + +- Key naming: use lowerCamelCase (e.g., `loginButton`, `errorMessage`). +- Include `@` metadata with `description` for context and `placeholders` with examples. +- Prefer ICU plural/select syntax in ARB for quantities and genders. +- Avoid concatenating strings at runtime; use placeholders in ARB. + +### In‑App Locale Switching + +- Open the Profile page → Settings tile → choose `System`, `English`, `Deutsch`, `Français`, or `Italiano`. +- Selection persists across app launches. + +### Troubleshooting + +- Build fails with ARB placeholder errors: + - Ensure every placeholder has an example string and correct type. +- Missing translation at runtime: + - Flutter falls back to English; search for hard‑coded strings and replace with `AppLocalizations`. +- iOS strings not changing: + - Restart the app after changing system language or use the in‑app language selector. + +### References + +- Flutter localization: https://docs.flutter.dev/ui/accessibility-and-localization/internationalization +- Intl package: https://pub.dev/packages/intl + ## Architecture The app follows a clean architecture pattern with: @@ -157,4 +221,4 @@ This project is licensed under the GPL3 License - see the LICENSE file for detai ## Support -For issues and feature requests, please use the [GitHub Issues](https://github.com/cogwheel0/conduit/issues) page. \ No newline at end of file +For issues and feature requests, please use the [GitHub Issues](https://github.com/cogwheel0/conduit/issues) page. diff --git a/lib/core/error/enhanced_error_service.dart b/lib/core/error/enhanced_error_service.dart index bdbe646..11ff82b 100644 --- a/lib/core/error/enhanced_error_service.dart +++ b/lib/core/error/enhanced_error_service.dart @@ -6,6 +6,7 @@ import 'api_error_handler.dart'; import 'api_error_interceptor.dart'; import '../../shared/theme/app_theme.dart'; import '../../shared/theme/theme_extensions.dart'; +import 'package:conduit/l10n/app_localizations.dart'; /// Enhanced error service with comprehensive error handling capabilities /// Provides unified error management across the application @@ -136,8 +137,8 @@ class EnhancedErrorService { action: isRetryableError && onRetry != null ? SnackBarAction( label: retryDelay != null && retryDelay.inSeconds > 5 - ? 'Retry (${retryDelay.inSeconds}s)' - : 'Retry', + ? "${AppLocalizations.of(context)!.retry} (${retryDelay.inSeconds}s)" + : AppLocalizations.of(context)!.retry, textColor: AppTheme.neutral50, onPressed: onRetry, ) @@ -208,14 +209,14 @@ class EnhancedErrorService { Navigator.of(context).pop(); onRetry(); }, - child: const Text('Retry'), + child: Text(AppLocalizations.of(context)!.retry), ), TextButton( onPressed: () { Navigator.of(context).pop(); onDismiss?.call(); }, - child: const Text('OK'), + child: Text(AppLocalizations.of(context)!.ok), ), ], ); @@ -281,7 +282,7 @@ class EnhancedErrorService { ElevatedButton.icon( onPressed: onRetry, icon: const Icon(Icons.refresh), - label: const Text('Try Again'), + label: const Text('Retry'), ), ], ], diff --git a/lib/core/providers/app_providers.dart b/lib/core/providers/app_providers.dart index 1836967..ecad4a3 100644 --- a/lib/core/providers/app_providers.dart +++ b/lib/core/providers/app_providers.dart @@ -79,6 +79,36 @@ class ThemeModeNotifier extends StateNotifier { } } +// Locale provider +final localeProvider = StateNotifierProvider( + (ref) { + final storage = ref.watch(optimizedStorageServiceProvider); + return LocaleNotifier(storage); + }, +); + +class LocaleNotifier extends StateNotifier { + final OptimizedStorageService _storage; + + LocaleNotifier(this._storage) : super(null) { + _loadLocale(); + } + + void _loadLocale() { + final code = _storage.getLocaleCode(); + if (code != null && code.isNotEmpty) { + state = Locale(code); + } else { + state = null; // system + } + } + + Future setLocale(Locale? locale) async { + state = locale; + await _storage.setLocaleCode(locale?.languageCode); + } +} + // Server connection providers - optimized with caching final serverConfigsProvider = FutureProvider>((ref) async { final storage = ref.watch(optimizedStorageServiceProvider); diff --git a/lib/core/services/optimized_storage_service.dart b/lib/core/services/optimized_storage_service.dart index 884f67b..a06db1c 100644 --- a/lib/core/services/optimized_storage_service.dart +++ b/lib/core/services/optimized_storage_service.dart @@ -23,6 +23,7 @@ class OptimizedStorageService { static const String _activeServerIdKey = 'active_server_id'; static const String _rememberCredentialsKey = 'remember_credentials'; static const String _themeModeKey = 'theme_mode'; + static const String _localeCodeKey = 'locale_code_v1'; static const String _localConversationsKey = 'local_conversations'; static const String _onboardingSeenKey = 'onboarding_seen_v1'; static const String _reviewerModeKey = 'reviewer_mode_v1'; @@ -226,6 +227,20 @@ class OptimizedStorageService { await _prefs.setString(_themeModeKey, mode); } + /// Locale Management + String? getLocaleCode() { + // Returns a locale code like 'en', 'de', 'fr', 'it'. Null means system. + return _prefs.getString(_localeCodeKey); + } + + Future setLocaleCode(String? code) async { + if (code == null || code.isEmpty) { + await _prefs.remove(_localeCodeKey); + } else { + await _prefs.setString(_localeCodeKey, code); + } + } + /// Onboarding Future getOnboardingSeen() async { return _prefs.getBool(_onboardingSeenKey) ?? false; diff --git a/lib/core/services/user_friendly_error_handler.dart b/lib/core/services/user_friendly_error_handler.dart index d05f542..7ceec73 100644 --- a/lib/core/services/user_friendly_error_handler.dart +++ b/lib/core/services/user_friendly_error_handler.dart @@ -182,12 +182,12 @@ class UserFriendlyErrorHandler { List _getServerRecoveryActions() { return [ ErrorRecoveryAction( - label: 'Try Again', + label: 'Retry', action: ErrorActionType.retry, description: 'Retry your request', ), ErrorRecoveryAction( - label: 'Wait & Retry', + label: 'Retry', action: ErrorActionType.retryLater, description: 'Wait a moment then try again', ), @@ -223,7 +223,7 @@ class UserFriendlyErrorHandler { description: 'Sign in to your account', ), ErrorRecoveryAction( - label: 'Try Again', + label: 'Retry', action: ErrorActionType.retry, description: 'Retry the request', ), @@ -280,7 +280,7 @@ class UserFriendlyErrorHandler { description: 'Select another file', ), ErrorRecoveryAction( - label: 'Try Again', + label: 'Retry', action: ErrorActionType.retry, description: 'Retry the operation', ), @@ -314,7 +314,7 @@ class UserFriendlyErrorHandler { description: 'Open app settings to grant permissions', ), ErrorRecoveryAction( - label: 'Try Again', + label: 'Retry', action: ErrorActionType.retry, description: 'Retry after granting permission', ), @@ -324,7 +324,7 @@ class UserFriendlyErrorHandler { List _getGenericRecoveryActions() { return [ ErrorRecoveryAction( - label: 'Try Again', + label: 'Retry', action: ErrorActionType.retry, description: 'Retry the operation', ), diff --git a/lib/core/widgets/error_boundary.dart b/lib/core/widgets/error_boundary.dart index 2b3f236..b37950e 100644 --- a/lib/core/widgets/error_boundary.dart +++ b/lib/core/widgets/error_boundary.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../shared/theme/theme_extensions.dart'; import '../error/enhanced_error_service.dart'; +import 'package:conduit/l10n/app_localizations.dart'; /// Error boundary widget that catches and handles errors in child widgets class ErrorBoundary extends ConsumerStatefulWidget { @@ -122,7 +123,7 @@ class _ErrorBoundaryState extends ConsumerState { ), const SizedBox(height: 16), Text( - 'Something went wrong', + AppLocalizations.of(context)!.errorMessage, style: Theme.of(context).textTheme.headlineSmall?.copyWith( color: context.conduitTheme.textPrimary, ), @@ -140,7 +141,7 @@ class _ErrorBoundaryState extends ConsumerState { FilledButton.icon( onPressed: _retry, icon: const Icon(Icons.refresh), - label: const Text('Try Again'), + label: Text(AppLocalizations.of(context)!.retry), ), ], ], @@ -239,7 +240,7 @@ class AsyncErrorBoundary extends ConsumerWidget { (context as Element).markNeedsBuild(); }, icon: const Icon(Icons.refresh), - label: const Text('Retry'), + label: Text(AppLocalizations.of(context)!.retry), ), ], ], diff --git a/lib/features/auth/views/authentication_page.dart b/lib/features/auth/views/authentication_page.dart index d951344..7e5650d 100644 --- a/lib/features/auth/views/authentication_page.dart +++ b/lib/features/auth/views/authentication_page.dart @@ -15,6 +15,7 @@ import '../../../shared/theme/theme_extensions.dart'; import '../../../shared/widgets/conduit_components.dart'; import '../../../core/auth/auth_state_manager.dart'; import '../../../core/utils/debug_logger.dart'; +import 'package:conduit/l10n/app_localizations.dart'; class AuthenticationPage extends ConsumerStatefulWidget { final ServerConfig serverConfig; @@ -61,6 +62,7 @@ class _AuthenticationPageState extends ConsumerState { } Future _signIn() async { + final l10n = AppLocalizations.of(context)!; if (!_formKey.currentState!.validate()) return; setState(() { @@ -87,7 +89,7 @@ class _AuthenticationPageState extends ConsumerState { if (!success) { final authState = ref.read(authStateManagerProvider); - throw Exception(authState.error ?? 'Login failed'); + throw Exception(authState.error ?? l10n.loginFailed); } // Success - navigation will be handled by auth state change @@ -106,15 +108,15 @@ class _AuthenticationPageState extends ConsumerState { String _formatLoginError(String error) { if (error.contains('401') || error.contains('Unauthorized')) { - return 'Invalid username or password. Please try again.'; + return AppLocalizations.of(context)!.invalidCredentials; } else if (error.contains('redirect')) { - return 'The server is redirecting requests. Check your server\'s HTTPS configuration.'; + return AppLocalizations.of(context)!.serverRedirectingHttps; } else if (error.contains('SocketException')) { - return 'Unable to connect to server. Please check your connection.'; + return AppLocalizations.of(context)!.unableToConnectServer; } else if (error.contains('timeout')) { - return 'The request timed out. Please try again.'; + return AppLocalizations.of(context)!.requestTimedOut; } - return 'We couldn\'t sign you in. Check your credentials and server settings.'; + return AppLocalizations.of(context)!.genericSignInFailed; } @override @@ -201,7 +203,7 @@ class _AuthenticationPageState extends ConsumerState { ConduitIconButton( icon: Platform.isIOS ? CupertinoIcons.back : Icons.arrow_back, onPressed: () => Navigator.pop(context), - tooltip: 'Back to server setup', + tooltip: AppLocalizations.of(context)!.backToServerSetup, ), const Spacer(), // Progress indicator (step 2 of 2) @@ -263,7 +265,7 @@ class _AuthenticationPageState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Connected to Server', + AppLocalizations.of(context)!.connectedToServer, style: context.conduitTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, color: context.conduitTheme.success, @@ -302,7 +304,7 @@ class _AuthenticationPageState extends ConsumerState { ), const SizedBox(height: Spacing.lg), Text( - 'Sign In', + AppLocalizations.of(context)!.signIn, textAlign: TextAlign.center, style: context.conduitTheme.headingLarge?.copyWith( fontWeight: FontWeight.w700, @@ -314,7 +316,7 @@ class _AuthenticationPageState extends ConsumerState { ), const SizedBox(height: Spacing.sm), Text( - 'Enter your credentials to access your AI conversations', + AppLocalizations.of(context)!.enterCredentials, textAlign: TextAlign.center, style: context.conduitTheme.bodyLarge?.copyWith( color: context.conduitTheme.textSecondary, @@ -370,7 +372,7 @@ class _AuthenticationPageState extends ConsumerState { icon: Platform.isIOS ? CupertinoIcons.person_circle : Icons.account_circle_outlined, - label: 'Credentials', + label: AppLocalizations.of(context)!.credentials, isSelected: !_useApiKey, onTap: () => setState(() => _useApiKey = false), ), @@ -380,7 +382,7 @@ class _AuthenticationPageState extends ConsumerState { icon: Platform.isIOS ? CupertinoIcons.lock_shield : Icons.vpn_key_outlined, - label: 'API Key', + label: AppLocalizations.of(context)!.apiKey, isSelected: _useApiKey, onTap: () => setState(() => _useApiKey = true), ), @@ -469,7 +471,7 @@ class _AuthenticationPageState extends ConsumerState { key: const ValueKey('api_key_form'), children: [ AccessibleFormField( - label: 'API Key', + label: AppLocalizations.of(context)!.apiKey, hint: 'sk-...', controller: _apiKeyController, validator: InputValidationService.combine([ @@ -477,7 +479,7 @@ class _AuthenticationPageState extends ConsumerState { (value) => InputValidationService.validateMinLength( value, 10, - fieldName: 'API Key', + fieldName: AppLocalizations.of(context)!.apiKey, ), ]), obscureText: _obscurePassword, @@ -513,7 +515,7 @@ class _AuthenticationPageState extends ConsumerState { key: const ValueKey('credentials_form'), children: [ AccessibleFormField( - label: 'Username or Email', + label: AppLocalizations.of(context)!.usernameOrEmail, hint: 'Enter your username or email', controller: _usernameController, validator: InputValidationService.combine([ @@ -531,7 +533,7 @@ class _AuthenticationPageState extends ConsumerState { ), const SizedBox(height: Spacing.lg), AccessibleFormField( - label: 'Password', + label: AppLocalizations.of(context)!.password, hint: 'Enter your password', controller: _passwordController, validator: InputValidationService.combine([ @@ -576,8 +578,8 @@ class _AuthenticationPageState extends ConsumerState { text: _isSigningIn ? 'Signing in...' : _useApiKey - ? 'Sign in with API Key' - : 'Sign In', + ? AppLocalizations.of(context)!.signInWithApiKey + : AppLocalizations.of(context)!.signIn, icon: _isSigningIn ? null : (Platform.isIOS diff --git a/lib/features/auth/views/connect_signin_page.dart b/lib/features/auth/views/connect_signin_page.dart index 612f1d5..71d10a8 100644 --- a/lib/features/auth/views/connect_signin_page.dart +++ b/lib/features/auth/views/connect_signin_page.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/widgets/error_boundary.dart'; import '../../../shared/theme/theme_extensions.dart'; import '../../../shared/widgets/conduit_components.dart'; +import 'package:conduit/l10n/app_localizations.dart'; import 'server_connection_page.dart'; /// Entry point for the connection and sign-in flow @@ -26,12 +27,12 @@ class ConnectAndSignInPage extends ConsumerWidget { return ErrorBoundary( child: Scaffold( backgroundColor: context.conduitTheme.surfaceBackground, - body: const Center( + body: Center( child: ConduitLoadingIndicator( - message: 'Loading...', + message: AppLocalizations.of(context)!.loadingContent, ), ), ), ); } -} \ No newline at end of file +} diff --git a/lib/features/auth/views/server_connection_page.dart b/lib/features/auth/views/server_connection_page.dart index 130b45c..5542448 100644 --- a/lib/features/auth/views/server_connection_page.dart +++ b/lib/features/auth/views/server_connection_page.dart @@ -6,6 +6,7 @@ import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter/services.dart'; import 'package:uuid/uuid.dart'; +import 'package:conduit/l10n/app_localizations.dart'; import '../../../core/models/server_config.dart'; import '../../../core/providers/app_providers.dart'; @@ -116,7 +117,7 @@ class _ServerConnectionPageState extends ConsumerState { String _validateAndFormatUrl(String input) { if (input.isEmpty) { - throw Exception('Server URL cannot be empty'); + throw Exception(AppLocalizations.of(context)!.serverUrlEmpty); } // Clean up the input @@ -135,29 +136,29 @@ class _ServerConnectionPageState extends ConsumerState { // Parse and validate the URI final uri = Uri.tryParse(url); if (uri == null) { - throw Exception('Invalid URL format. Please check your input.'); + throw Exception(AppLocalizations.of(context)!.invalidUrlFormat); } // Validate scheme if (uri.scheme != 'http' && uri.scheme != 'https') { - throw Exception('Only HTTP and HTTPS protocols are supported.'); + throw Exception(AppLocalizations.of(context)!.onlyHttpHttps); } // Validate host if (uri.host.isEmpty) { - throw Exception('Server address is required (e.g., 192.168.1.10 or example.com).'); + throw Exception(AppLocalizations.of(context)!.serverAddressRequired); } // Validate port if specified if (uri.hasPort) { if (uri.port < 1 || uri.port > 65535) { - throw Exception('Port must be between 1 and 65535.'); + throw Exception(AppLocalizations.of(context)!.portRange); } } // Validate IP address format if it looks like an IP if (_isIPAddress(uri.host) && !_isValidIPAddress(uri.host)) { - throw Exception('Invalid IP address format. Use format like 192.168.1.10.'); + throw Exception(AppLocalizations.of(context)!.invalidIpFormat); } return url; @@ -192,15 +193,15 @@ class _ServerConnectionPageState extends ConsumerState { // Handle specific error types if (error.contains('SocketException')) { - return 'We couldn\'t reach the server. Check your connection and that the server is running.'; + return AppLocalizations.of(context)!.weCouldntReachServer; } else if (error.contains('timeout')) { - return 'Connection timed out. The server might be busy or blocked by a firewall.'; + return AppLocalizations.of(context)!.connectionTimedOut; } else if (error.contains('Server URL cannot be empty')) { - return 'Please enter a server address.'; + return AppLocalizations.of(context)!.serverUrlEmpty; } else if (error.contains('Invalid URL format')) { - return 'Invalid server address format. Examples:\n• 192.168.1.10:3000\n• example.com\n• https://myserver.com'; + return AppLocalizations.of(context)!.invalidUrlFormat; } else if (error.contains('Only HTTP and HTTPS')) { - return 'Use http:// or https:// only.'; + return AppLocalizations.of(context)!.useHttpOrHttpsOnly; } else if (error.contains('Server address is required')) { return cleanError; } else if (error.contains('Port must be between')) { @@ -208,10 +209,10 @@ class _ServerConnectionPageState extends ConsumerState { } else if (error.contains('Invalid IP address format')) { return cleanError; } else if (error.contains('This does not appear to be an Open-WebUI server')) { - return 'This server doesn\'t appear to be running Open-WebUI. Please check the address.'; + return AppLocalizations.of(context)!.serverNotOpenWebUI; } - return 'Couldn\'t connect. Double-check the address and try again.'; + return AppLocalizations.of(context)!.couldNotConnectGeneric; } @override @@ -379,7 +380,7 @@ class _ServerConnectionPageState extends ConsumerState { return Column( children: [ Text( - 'Connect to Server', + AppLocalizations.of(context)!.connectToServer, textAlign: TextAlign.center, style: context.conduitTheme.headingLarge?.copyWith( fontWeight: FontWeight.w700, @@ -391,7 +392,7 @@ class _ServerConnectionPageState extends ConsumerState { ), const SizedBox(height: Spacing.sm), Text( - 'Enter your Open-WebUI server address to get started', + AppLocalizations.of(context)!.enterServerAddress, textAlign: TextAlign.center, style: context.conduitTheme.bodyLarge?.copyWith( color: context.conduitTheme.textSecondary, @@ -611,7 +612,7 @@ class _ServerConnectionPageState extends ConsumerState { Expanded( flex: 2, child: AccessibleFormField( - label: 'Header Name', + label: AppLocalizations.of(context)!.headerName, hint: 'X-Custom-Header', controller: _headerKeyController, validator: (value) => _validateHeaderKey(value ?? ''), @@ -624,8 +625,8 @@ class _ServerConnectionPageState extends ConsumerState { Expanded( flex: 3, child: AccessibleFormField( - label: 'Header Value', - hint: 'api-key-123 or Bearer token', + label: AppLocalizations.of(context)!.headerValue, + hint: AppLocalizations.of(context)!.headerValueHint, controller: _headerValueController, validator: (value) => _validateHeaderValue(value ?? ''), semanticLabel: 'Enter header value', @@ -638,8 +639,8 @@ class _ServerConnectionPageState extends ConsumerState { icon: Platform.isIOS ? CupertinoIcons.plus : Icons.add, onPressed: _customHeaders.length >= 10 ? null : _addCustomHeader, tooltip: _customHeaders.length >= 10 - ? 'Maximum headers reached' - : 'Add header', + ? AppLocalizations.of(context)!.maximumHeadersReached + : AppLocalizations.of(context)!.addHeader, backgroundColor: _customHeaders.length >= 10 ? context.conduitTheme.surfaceContainer : context.conduitTheme.buttonPrimary, @@ -694,7 +695,7 @@ class _ServerConnectionPageState extends ConsumerState { ConduitIconButton( icon: Platform.isIOS ? CupertinoIcons.xmark : Icons.close, onPressed: () => _removeCustomHeader(entry.key), - tooltip: 'Remove header', + tooltip: AppLocalizations.of(context)!.removeHeader, backgroundColor: context.conduitTheme.error.withValues(alpha: 0.1), iconColor: context.conduitTheme.error, isCompact: true, @@ -710,7 +711,9 @@ class _ServerConnectionPageState extends ConsumerState { return Padding( padding: const EdgeInsets.only(top: Spacing.lg), child: ConduitButton( - text: _isConnecting ? 'Connecting...' : 'Connect to Server', + text: _isConnecting + ? AppLocalizations.of(context)!.connecting + : AppLocalizations.of(context)!.connectToServerButton, icon: _isConnecting ? null : (Platform.isIOS ? CupertinoIcons.arrow_right : Icons.arrow_forward), @@ -866,4 +869,4 @@ class _ServerConnectionPageState extends ConsumerState { }); HapticFeedback.lightImpact(); } -} \ No newline at end of file +} diff --git a/lib/features/chat/widgets/assistant_message_widget.dart b/lib/features/chat/widgets/assistant_message_widget.dart index 4884c39..dfc284d 100644 --- a/lib/features/chat/widgets/assistant_message_widget.dart +++ b/lib/features/chat/widgets/assistant_message_widget.dart @@ -8,6 +8,7 @@ import '../../../shared/theme/theme_extensions.dart'; import '../../../shared/widgets/markdown/streaming_markdown_widget.dart'; import '../../../core/utils/reasoning_parser.dart'; import 'enhanced_image_attachment.dart'; +import 'package:conduit/l10n/app_localizations.dart'; class AssistantMessageWidget extends ConsumerStatefulWidget { final dynamic message; @@ -559,7 +560,7 @@ class _AssistantMessageWidgetState extends ConsumerState icon: Platform.isIOS ? CupertinoIcons.arrow_clockwise : Icons.refresh, - label: 'Retry', + label: AppLocalizations.of(context)!.retry, onTap: widget.onRegenerate, ), ] else ...[ diff --git a/lib/features/chat/widgets/enhanced_image_attachment.dart b/lib/features/chat/widgets/enhanced_image_attachment.dart index 32194d4..a6590d2 100644 --- a/lib/features/chat/widgets/enhanced_image_attachment.dart +++ b/lib/features/chat/widgets/enhanced_image_attachment.dart @@ -9,6 +9,7 @@ import 'package:dio/dio.dart' as dio; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; import '../../../shared/theme/theme_extensions.dart'; +import 'package:conduit/l10n/app_localizations.dart'; import '../../../core/providers/app_providers.dart'; import '../../auth/providers/unified_auth_providers.dart'; @@ -74,6 +75,7 @@ class _EnhancedImageAttachmentState } Future _loadImage() async { + final l10n = AppLocalizations.of(context)!; // Check global cache first if (_globalImageCache.containsKey(widget.attachmentId)) { if (mounted) { @@ -142,7 +144,7 @@ class _EnhancedImageAttachmentState return; } else { // If API service is not available, show error - final error = 'Unable to load image: API service not available'; + final error = l10n.unableToLoadImage; _globalErrorStates[widget.attachmentId] = error; _globalLoadingStates[widget.attachmentId] = false; if (mounted) { @@ -157,7 +159,7 @@ class _EnhancedImageAttachmentState final api = ref.read(apiServiceProvider); if (api == null) { - final error = 'API service not available'; + final error = l10n.apiUnavailable; _globalErrorStates[widget.attachmentId] = error; _globalLoadingStates[widget.attachmentId] = false; if (mounted) { @@ -176,7 +178,7 @@ class _EnhancedImageAttachmentState final ext = fileName.toLowerCase().split('.').last; if (!['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].contains(ext)) { - final error = 'Not an image file: $fileName'; + final error = l10n.notAnImageFile(fileName); _globalErrorStates[widget.attachmentId] = error; _globalLoadingStates[widget.attachmentId] = false; if (mounted) { @@ -214,7 +216,7 @@ class _EnhancedImageAttachmentState } } } catch (e) { - final error = 'Failed to load image: ${e.toString()}'; + final error = l10n.failedToLoadImage(e.toString()); _globalErrorStates[widget.attachmentId] = error; _globalLoadingStates[widget.attachmentId] = false; if (mounted) { @@ -443,7 +445,7 @@ class _EnhancedImageAttachmentState if (commaIndex != -1) { actualBase64 = _cachedImageData!.substring(commaIndex + 1); } else { - throw Exception('Invalid data URL format'); + throw Exception(AppLocalizations.of(context)!.invalidDataUrl); } } else { actualBase64 = _cachedImageData!; @@ -456,14 +458,14 @@ class _EnhancedImageAttachmentState fit: BoxFit.cover, gaplessPlayback: true, // Prevents flashing during rebuilds errorBuilder: (context, error, stackTrace) { - _errorMessage = 'Failed to decode image'; + _errorMessage = AppLocalizations.of(context)!.failedToDecodeImage; return _buildErrorState(); }, ); return _wrapImage(imageWidget); } catch (e) { - _errorMessage = 'Invalid image format'; + _errorMessage = AppLocalizations.of(context)!.invalidImageFormat; return _buildErrorState(); } } @@ -650,6 +652,7 @@ class FullScreenImageViewer extends ConsumerWidget { } Future _shareImage(BuildContext context, WidgetRef ref) async { + final l10n = AppLocalizations.of(context)!; try { Uint8List bytes; String? fileExtension; @@ -679,7 +682,7 @@ class FullScreenImageViewer extends ConsumerWidget { ); final data = response.data; if (data == null || data.isEmpty) { - throw Exception('Empty image data'); + throw Exception(l10n.emptyImageData); } bytes = Uint8List.fromList(data); diff --git a/lib/features/chat/widgets/modern_chat_input.dart b/lib/features/chat/widgets/modern_chat_input.dart index c8d85e2..87dbcf9 100644 --- a/lib/features/chat/widgets/modern_chat_input.dart +++ b/lib/features/chat/widgets/modern_chat_input.dart @@ -13,6 +13,7 @@ import '../../tools/widgets/unified_tools_modal.dart'; import '../../tools/providers/tools_providers.dart'; import '../../../shared/utils/platform_utils.dart'; +import 'package:conduit/l10n/app_localizations.dart'; class ModernChatInput extends ConsumerStatefulWidget { final Function(String) onSendMessage; @@ -264,7 +265,7 @@ class _ModernChatInputState extends ConsumerState onTap: widget.enabled ? _showAttachmentOptions : null, - tooltip: 'Add attachment', + tooltip: AppLocalizations.of(context)!.addAttachment, ), const SizedBox(width: Spacing.sm), ], @@ -272,8 +273,8 @@ class _ModernChatInputState extends ConsumerState Expanded( child: Semantics( textField: true, - label: 'Message input', - hint: 'Type your message', + label: AppLocalizations.of(context)!.messageInputLabel, + hint: AppLocalizations.of(context)!.messageInputHint, child: TextField( controller: _controller, focusNode: _focusNode, @@ -291,7 +292,7 @@ class _ModernChatInputState extends ConsumerState color: context.conduitTheme.inputText, ), decoration: InputDecoration( - hintText: 'Message...', + hintText: AppLocalizations.of(context)!.messageHintText, hintStyle: TextStyle( color: context.conduitTheme.inputPlaceholder, @@ -363,7 +364,7 @@ class _ModernChatInputState extends ConsumerState onTap: widget.enabled ? _showAttachmentOptions : null, - tooltip: 'Add attachment', + tooltip: AppLocalizations.of(context)!.addAttachment, ), const SizedBox(width: Spacing.sm), // Tools button @@ -374,7 +375,7 @@ class _ModernChatInputState extends ConsumerState _showUnifiedToolsModal(); } : null, - tooltip: 'Tools', + tooltip: AppLocalizations.of(context)!.tools, isActive: ref .watch(selectedToolIdsProvider) @@ -391,7 +392,7 @@ class _ModernChatInputState extends ConsumerState onTap: widget.enabled ? widget.onVoiceInput : null, - tooltip: 'Voice input', + tooltip: AppLocalizations.of(context)!.voiceInput, isActive: _isRecording, ), const SizedBox(width: Spacing.sm), @@ -431,7 +432,7 @@ class _ModernChatInputState extends ConsumerState // Generating -> STOP variant if (isGenerating) { return Tooltip( - message: 'Stop generating', + message: AppLocalizations.of(context)!.stopGenerating, child: Material( color: Colors.transparent, shape: RoundedRectangleBorder( @@ -482,7 +483,7 @@ class _ModernChatInputState extends ConsumerState // Default SEND variant return Tooltip( - message: enabled ? 'Send message' : 'Send', + message: enabled ? AppLocalizations.of(context)!.sendMessage : AppLocalizations.of(context)!.send, child: Opacity( opacity: enabled ? Alpha.primary : Alpha.disabled, child: IgnorePointer( @@ -626,7 +627,7 @@ class _ModernChatInputState extends ConsumerState Expanded( child: _buildAttachmentOption( icon: Platform.isIOS ? CupertinoIcons.doc : Icons.attach_file, - label: 'File', + label: AppLocalizations.of(context)!.file, onTap: () { HapticFeedback.lightImpact(); Navigator.pop(context); // Close modal @@ -637,7 +638,7 @@ class _ModernChatInputState extends ConsumerState Expanded( child: _buildAttachmentOption( icon: Platform.isIOS ? CupertinoIcons.photo : Icons.image, - label: 'Photo', + label: AppLocalizations.of(context)!.photo, onTap: () { HapticFeedback.lightImpact(); Navigator.pop(context); // Close modal @@ -650,7 +651,7 @@ class _ModernChatInputState extends ConsumerState icon: Platform.isIOS ? CupertinoIcons.camera : Icons.camera_alt, - label: 'Camera', + label: AppLocalizations.of(context)!.camera, onTap: () { HapticFeedback.lightImpact(); Navigator.pop(context); // Close modal diff --git a/lib/features/files/views/workspace_page.dart b/lib/features/files/views/workspace_page.dart index 7aeff59..d2c8c9a 100644 --- a/lib/features/files/views/workspace_page.dart +++ b/lib/features/files/views/workspace_page.dart @@ -9,6 +9,7 @@ import '../../../shared/widgets/improved_loading_states.dart'; import '../../../shared/utils/ui_utils.dart'; import '../../../shared/widgets/sheet_handle.dart'; +import 'package:conduit/l10n/app_localizations.dart'; /// Files page for managing documents and uploads class WorkspacePage extends ConsumerStatefulWidget { @@ -108,10 +109,10 @@ class _WorkspacePageState extends ConsumerState size: IconSize.button, ), onPressed: () => NavigationService.goBack(), - tooltip: 'Back', + tooltip: AppLocalizations.of(context)!.back, ), title: Text( - 'Workspace', + AppLocalizations.of(context)!.workspace, style: AppTypography.headlineSmallStyle.copyWith( color: context.conduitTheme.textPrimary, fontWeight: FontWeight.w600, @@ -162,7 +163,7 @@ class _WorkspacePageState extends ConsumerState Expanded( child: _buildTabButton( index: 0, - label: 'Recent Files', + label: AppLocalizations.of(context)!.recentFiles, isSelected: _selectedTab == 0, ), ), @@ -170,7 +171,7 @@ class _WorkspacePageState extends ConsumerState Expanded( child: _buildTabButton( index: 1, - label: 'Knowledge Base', + label: AppLocalizations.of(context)!.knowledgeBase, isSelected: _selectedTab == 1, ), ), @@ -229,11 +230,10 @@ class _WorkspacePageState extends ConsumerState ios: CupertinoIcons.doc, android: Icons.description_outlined, ), - title: 'No files yet', - subtitle: - 'Upload documents to reference in your conversations with Conduit', + title: AppLocalizations.of(context)!.noFilesYet, + subtitle: AppLocalizations.of(context)!.uploadDocsPrompt, onAction: _showUploadOptions, - actionLabel: 'Upload your first file', + actionLabel: AppLocalizations.of(context)!.uploadFirstFile, showAnimation: true, ), ).animate().fadeIn( @@ -251,8 +251,8 @@ class _WorkspacePageState extends ConsumerState ios: CupertinoIcons.book, android: Icons.library_books, ), - title: 'Knowledge base is empty', - subtitle: 'Create collections of related documents for easy reference', + title: AppLocalizations.of(context)!.knowledgeBaseEmpty, + subtitle: AppLocalizations.of(context)!.createCollectionsPrompt, onAction: _showKnowledgeBaseOptions, actionLabel: 'Create knowledge base', showAnimation: true, @@ -293,7 +293,7 @@ class _WorkspacePageState extends ConsumerState Padding( padding: const EdgeInsets.all(Spacing.modalPadding), child: Text( - 'Upload File', + AppLocalizations.of(context)!.uploadFileTitle, style: context.conduitTheme.headingSmall?.copyWith( color: context.conduitTheme.textPrimary, fontWeight: FontWeight.w600, @@ -307,8 +307,8 @@ class _WorkspacePageState extends ConsumerState ios: CupertinoIcons.camera, android: Icons.camera_alt, ), - title: 'Take Photo', - subtitle: 'Capture a document or image', + title: AppLocalizations.of(context)!.takePhoto, + subtitle: AppLocalizations.of(context)!.captureDocumentOrImage, onTap: () => _handleUploadOption('camera'), ), _buildUploadOption( @@ -316,8 +316,8 @@ class _WorkspacePageState extends ConsumerState ios: CupertinoIcons.photo, android: Icons.photo_library, ), - title: 'Photo Library', - subtitle: 'Choose from your photos', + title: AppLocalizations.of(context)!.chooseFromGallery, + subtitle: AppLocalizations.of(context)!.chooseFromGallery, onTap: () => _handleUploadOption('gallery'), ), _buildUploadOption( @@ -325,8 +325,8 @@ class _WorkspacePageState extends ConsumerState ios: CupertinoIcons.doc, android: Icons.description, ), - title: 'Document', - subtitle: 'PDF, Word, or text file', + title: AppLocalizations.of(context)!.document, + subtitle: AppLocalizations.of(context)!.documentHint, onTap: () => _handleUploadOption('document'), ), @@ -430,10 +430,16 @@ class _WorkspacePageState extends ConsumerState void _handleUploadOption(String type) { NavigationService.goBack(); - UiUtils.showMessage(context, 'File upload for $type is coming soon!'); + UiUtils.showMessage( + context, + AppLocalizations.of(context)!.fileUploadComingSoon(type), + ); } void _showKnowledgeBaseOptions() { - UiUtils.showMessage(context, 'Knowledge base creation is coming soon!'); + UiUtils.showMessage( + context, + AppLocalizations.of(context)!.kbCreationComingSoon, + ); } } diff --git a/lib/features/onboarding/views/onboarding_sheet.dart b/lib/features/onboarding/views/onboarding_sheet.dart index 17ad34e..0edded1 100644 --- a/lib/features/onboarding/views/onboarding_sheet.dart +++ b/lib/features/onboarding/views/onboarding_sheet.dart @@ -3,6 +3,7 @@ import 'package:flutter/cupertino.dart'; import '../../../shared/theme/theme_extensions.dart'; import 'package:flutter_animate/flutter_animate.dart'; import '../../../shared/widgets/sheet_handle.dart'; +import 'package:conduit/l10n/app_localizations.dart'; class OnboardingSheet extends StatefulWidget { const OnboardingSheet({super.key}); @@ -14,44 +15,39 @@ class OnboardingSheet extends StatefulWidget { class _OnboardingSheetState extends State { final PageController _controller = PageController(); int _index = 0; + late List<_OnboardingPage> _pages; - final List<_OnboardingPage> _pages = const [ - _OnboardingPage( - title: 'Start a conversation', - subtitle: - 'Choose a model, then type below to begin. Tap New Chat anytime.', - icon: CupertinoIcons.chat_bubble_2, - bullets: [ - 'Tap the model name in the top bar to switch models', - 'Use New Chat to reset context', - ], - ), - _OnboardingPage( - title: 'Attach context', - subtitle: 'Ground responses by adding files or images.', - icon: CupertinoIcons.doc_on_doc, - bullets: ['Files: PDFs, docs, datasets', 'Images: photos or screenshots'], - ), - _OnboardingPage( - title: 'Speak naturally', - subtitle: 'Tap the mic to dictate with live waveform feedback.', - icon: CupertinoIcons.mic_fill, - bullets: [ - 'Stop anytime; partial text is preserved', - 'Great for quick notes or long prompts', - ], - ), - _OnboardingPage( - title: 'Quick actions', - subtitle: - 'Use the top‑left menu to open the chats list and navigation.', - icon: CupertinoIcons.line_horizontal_3, - bullets: [ - 'Tap the menu to open the chats list and navigation', - 'Jump instantly to New Chat, Files, or Profile', - ], - ), - ]; + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final l10n = AppLocalizations.of(context)!; + _pages = [ + _OnboardingPage( + title: l10n.onboardStartTitle, + subtitle: l10n.onboardStartSubtitle, + icon: CupertinoIcons.chat_bubble_2, + bullets: [l10n.onboardStartBullet1, l10n.onboardStartBullet2], + ), + _OnboardingPage( + title: l10n.onboardAttachTitle, + subtitle: l10n.onboardAttachSubtitle, + icon: CupertinoIcons.doc_on_doc, + bullets: [l10n.onboardAttachBullet1, l10n.onboardAttachBullet2], + ), + _OnboardingPage( + title: l10n.onboardSpeakTitle, + subtitle: l10n.onboardSpeakSubtitle, + icon: CupertinoIcons.mic_fill, + bullets: [l10n.onboardSpeakBullet1, l10n.onboardSpeakBullet2], + ), + _OnboardingPage( + title: l10n.onboardQuickTitle, + subtitle: l10n.onboardQuickSubtitle, + icon: CupertinoIcons.line_horizontal_3, + bullets: [l10n.onboardQuickBullet1, l10n.onboardQuickBullet2], + ), + ]; + } void _next() { if (_index < _pages.length - 1) { @@ -133,7 +129,7 @@ class _OnboardingSheetState extends State { TextButton( onPressed: () => Navigator.pop(context), child: Text( - 'Skip', + AppLocalizations.of(context)!.skip, style: TextStyle( color: context.conduitTheme.textSecondary, ), @@ -155,7 +151,11 @@ class _OnboardingSheetState extends State { ), ), ), - child: Text(_index == _pages.length - 1 ? 'Done' : 'Next'), + child: Text( + _index == _pages.length - 1 + ? AppLocalizations.of(context)!.done + : AppLocalizations.of(context)!.next, + ), ), ], ), diff --git a/lib/features/profile/views/profile_page.dart b/lib/features/profile/views/profile_page.dart index 3e11867..885c087 100644 --- a/lib/features/profile/views/profile_page.dart +++ b/lib/features/profile/views/profile_page.dart @@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:url_launcher/url_launcher_string.dart'; +import 'package:conduit/l10n/app_localizations.dart'; import '../../../core/widgets/error_boundary.dart'; import '../../../shared/widgets/improved_loading_states.dart'; @@ -46,12 +47,12 @@ class ProfilePage extends ConsumerWidget { color: context.conduitTheme.textPrimary, ), onPressed: () => Navigator.of(context).maybePop(), - tooltip: 'Back', + tooltip: AppLocalizations.of(context)!.back, ), toolbarHeight: kToolbarHeight, titleSpacing: 0.0, title: Text( - 'You', + AppLocalizations.of(context)!.you, style: AppTypography.headlineSmallStyle.copyWith( color: context.conduitTheme.textPrimary, fontWeight: FontWeight.w600, @@ -106,10 +107,10 @@ class ProfilePage extends ConsumerWidget { color: context.conduitTheme.textPrimary, ), onPressed: () => Navigator.of(context).maybePop(), - tooltip: 'Back', + tooltip: AppLocalizations.of(context)!.back, ), title: Text( - 'You', + AppLocalizations.of(context)!.you, style: AppTypography.headlineSmallStyle.copyWith( color: context.conduitTheme.textPrimary, fontWeight: FontWeight.w600, @@ -117,8 +118,8 @@ class ProfilePage extends ConsumerWidget { ), centerTitle: true, ), - body: const Center( - child: ImprovedLoadingState(message: 'Loading profile...'), + body: Center( + child: ImprovedLoadingState(message: AppLocalizations.of(context)!.loadingProfile), ), ), error: (error, stack) => Scaffold( @@ -136,10 +137,10 @@ class ProfilePage extends ConsumerWidget { color: context.conduitTheme.textPrimary, ), onPressed: () => Navigator.of(context).maybePop(), - tooltip: 'Back', + tooltip: AppLocalizations.of(context)!.back, ), title: Text( - 'You', + AppLocalizations.of(context)!.you, style: AppTypography.headlineSmallStyle.copyWith( color: context.conduitTheme.textPrimary, fontWeight: FontWeight.w600, @@ -149,8 +150,8 @@ class ProfilePage extends ConsumerWidget { ), body: Center( child: ImprovedEmptyState( - title: 'Unable to load profile', - subtitle: 'Please check your connection and try again', + title: AppLocalizations.of(context)!.unableToLoadProfile, + subtitle: AppLocalizations.of(context)!.pleaseCheckConnection, icon: UiUtils.platformIcon( ios: CupertinoIcons.exclamationmark_triangle, android: Icons.error_outline, @@ -213,7 +214,7 @@ class ProfilePage extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Account', + AppLocalizations.of(context)!.account, style: context.conduitTheme.headingSmall?.copyWith( color: context.conduitTheme.textPrimary, ), @@ -227,6 +228,8 @@ class ProfilePage extends ConsumerWidget { Divider(color: context.conduitTheme.dividerColor, height: 1), _buildThemeToggleTile(context, ref), Divider(color: context.conduitTheme.dividerColor, height: 1), + _buildLanguageTile(context, ref), + Divider(color: context.conduitTheme.dividerColor, height: 1), _buildAboutTile(context), Divider(color: context.conduitTheme.dividerColor, height: 1), _buildAccountOption( @@ -234,8 +237,8 @@ class ProfilePage extends ConsumerWidget { ios: CupertinoIcons.square_arrow_left, android: Icons.logout, ), - title: 'Sign Out', - subtitle: 'End your session', + title: AppLocalizations.of(context)!.signOut, + subtitle: AppLocalizations.of(context)!.endYourSession, onTap: () => _signOut(context, ref), isDestructive: true, ), @@ -342,14 +345,14 @@ class ProfilePage extends ConsumerWidget { ), ), title: Text( - 'Default Model', + AppLocalizations.of(context)!.defaultModel, style: context.conduitTheme.bodyLarge?.copyWith( color: context.conduitTheme.textPrimary, fontWeight: FontWeight.w500, ), ), subtitle: Text( - settings.defaultModel != null ? currentModel.name : 'Auto-select', + settings.defaultModel != null ? currentModel.name : AppLocalizations.of(context)!.autoSelect, style: context.conduitTheme.bodySmall?.copyWith( color: context.conduitTheme.textSecondary, ), @@ -388,14 +391,14 @@ class ProfilePage extends ConsumerWidget { ), ), title: Text( - 'Default Model', + AppLocalizations.of(context)!.defaultModel, style: context.conduitTheme.bodyLarge?.copyWith( color: context.conduitTheme.textPrimary, fontWeight: FontWeight.w500, ), ), subtitle: Text( - 'Loading models...', + AppLocalizations.of(context)!.loadingModels, style: context.conduitTheme.bodySmall?.copyWith( color: context.conduitTheme.textSecondary, ), @@ -424,14 +427,14 @@ class ProfilePage extends ConsumerWidget { ), ), title: Text( - 'Default Model', + AppLocalizations.of(context)!.defaultModel, style: context.conduitTheme.bodyLarge?.copyWith( color: context.conduitTheme.textPrimary, fontWeight: FontWeight.w500, ), ), subtitle: Text( - 'Failed to load models', + AppLocalizations.of(context)!.failedToLoadModels, style: context.conduitTheme.bodySmall?.copyWith( color: context.conduitTheme.error, ), @@ -440,6 +443,132 @@ class ProfilePage extends ConsumerWidget { ); } + Widget _buildLanguageTile(BuildContext context, WidgetRef ref) { + final locale = ref.watch(localeProvider); + final currentCode = locale?.languageCode ?? 'system'; + final label = () { + switch (currentCode) { + case 'en': + return 'English'; + case 'de': + return 'Deutsch'; + case 'fr': + return 'Français'; + case 'it': + return 'Italiano'; + default: + return 'System'; + } + }(); + + return ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: Spacing.listItemPadding, + vertical: Spacing.sm, + ), + leading: Container( + padding: const EdgeInsets.all(Spacing.sm), + decoration: BoxDecoration( + color: context.conduitTheme.buttonPrimary.withValues( + alpha: Alpha.highlight, + ), + borderRadius: BorderRadius.circular(AppBorderRadius.small), + ), + child: Icon( + UiUtils.platformIcon( + ios: CupertinoIcons.globe, + android: Icons.language, + ), + color: context.conduitTheme.buttonPrimary, + size: IconSize.medium, + ), + ), + title: Text( + AppLocalizations.of(context)!.menuItem, + style: context.conduitTheme.bodyLarge?.copyWith( + color: context.conduitTheme.textPrimary, + fontWeight: FontWeight.w500, + ), + ), + subtitle: Text( + label, + style: context.conduitTheme.bodySmall?.copyWith( + color: context.conduitTheme.textSecondary, + ), + ), + trailing: Icon( + UiUtils.platformIcon( + ios: CupertinoIcons.chevron_right, + android: Icons.chevron_right, + ), + color: context.conduitTheme.iconSecondary, + size: IconSize.small, + ), + onTap: () async { + final selected = await _showLanguageSelector(context, currentCode); + if (selected != null) { + if (selected == 'system') { + await ref.read(localeProvider.notifier).setLocale(null); + } else { + await ref.read(localeProvider.notifier).setLocale(Locale(selected)); + } + } + }, + ); + } + + Future _showLanguageSelector(BuildContext context, String current) { + return showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + builder: (context) => Container( + decoration: BoxDecoration( + color: context.conduitTheme.surfaceBackground, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(AppBorderRadius.modal), + ), + boxShadow: ConduitShadows.modal, + ), + child: SafeArea( + top: false, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: Spacing.sm), + ListTile( + title: const Text('System'), + trailing: current == 'system' ? const Icon(Icons.check) : null, + onTap: () => Navigator.pop(context, 'system'), + ), + ListTile( + title: const Text('English'), + trailing: current == 'en' ? const Icon(Icons.check) : null, + onTap: () => Navigator.pop(context, 'en'), + ), + ListTile( + title: const Text('Deutsch'), + trailing: current == 'de' ? const Icon(Icons.check) : null, + onTap: () => Navigator.pop(context, 'de'), + ), + ListTile( + title: const Text('Français'), + trailing: current == 'fr' ? const Icon(Icons.check) : null, + onTap: () => Navigator.pop(context, 'fr'), + ), + ListTile( + title: const Text('Italiano'), + trailing: current == 'it' ? const Icon(Icons.check) : null, + onTap: () => Navigator.pop(context, 'it'), + ), + const SizedBox(height: Spacing.sm), + ], + ), + ), + ), + ); + } + Widget _buildThemeToggleTile(BuildContext context, WidgetRef ref) { final themeMode = ref.watch(themeModeProvider); final platformBrightness = MediaQuery.platformBrightnessOf(context); @@ -579,7 +708,7 @@ class ProfilePage extends ConsumerWidget { actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), - child: const Text('Close'), + child: Text(AppLocalizations.of(ctx)!.closeButtonSemantic), ), ], ); @@ -614,9 +743,9 @@ class ProfilePage extends ConsumerWidget { void _signOut(BuildContext context, WidgetRef ref) async { final confirm = await UiUtils.showConfirmationDialog( context, - title: 'Sign out?', - message: 'You\'ll need to sign in again to continue', - confirmText: 'Sign out', + title: AppLocalizations.of(context)!.signOut, + message: AppLocalizations.of(context)!.endYourSession, + confirmText: AppLocalizations.of(context)!.signOut, isDestructive: true, ); @@ -756,7 +885,7 @@ class _DefaultModelBottomSheetState extends ConsumerState<_DefaultModelBottomShe controller: _searchController, style: TextStyle(color: context.conduitTheme.textPrimary), decoration: InputDecoration( - hintText: 'Search models...', + hintText: AppLocalizations.of(context)!.searchModels, hintStyle: TextStyle( color: context.conduitTheme.inputPlaceholder, ), @@ -799,7 +928,7 @@ class _DefaultModelBottomSheetState extends ConsumerState<_DefaultModelBottomShe child: Row( children: [ Text( - 'Available Models', + AppLocalizations.of(context)!.availableModels, style: AppTypography.bodySmallStyle.copyWith( fontWeight: FontWeight.w600, color: context.conduitTheme.textSecondary, @@ -848,7 +977,7 @@ class _DefaultModelBottomSheetState extends ConsumerState<_DefaultModelBottomShe ), const SizedBox(height: Spacing.md), Text( - 'No results', + AppLocalizations.of(context)!.noResults, style: TextStyle( color: context.conduitTheme.textSecondary, fontSize: AppTypography.bodyLarge, @@ -958,7 +1087,7 @@ class _DefaultModelBottomSheetState extends ConsumerState<_DefaultModelBottomShe crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - isAutoSelect ? 'Auto-select' : model.name, + isAutoSelect ? AppLocalizations.of(context)!.autoSelect : model.name, style: TextStyle( color: context.conduitTheme.textPrimary, fontWeight: FontWeight.w600, diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb new file mode 100644 index 0000000..6303a46 --- /dev/null +++ b/lib/l10n/app_de.arb @@ -0,0 +1,145 @@ +{ + "@@locale": "de", + "appTitle": "Conduit", + "initializationFailed": "Initialisierung fehlgeschlagen", + "retry": "Erneut versuchen", + "back": "Zurück", + "you": "Du", + "loadingProfile": "Profil wird geladen...", + "unableToLoadProfile": "Profil konnte nicht geladen werden", + "pleaseCheckConnection": "Bitte überprüfe deine Verbindung und versuche es erneut", + "account": "Konto", + "signOut": "Abmelden", + "endYourSession": "Sitzung beenden", + "defaultModel": "Standardmodell", + "autoSelect": "Automatische Auswahl", + "loadingModels": "Modelle werden geladen...", + "failedToLoadModels": "Modelle konnten nicht geladen werden", + "availableModels": "Verfügbare Modelle", + "noResults": "Keine Ergebnisse", + "searchModels": "Modelle suchen...", + "errorMessage": "Etwas ist schief gelaufen. Bitte versuche es erneut.", + "loginButton": "Anmelden", + "menuItem": "Einstellungen", + "dynamicContentWithPlaceholder": "Willkommen, {name}!", + "itemsCount": "{count, plural, =0{Keine Elemente} one{1 Element} other{{count} Elemente}}", + "closeButtonSemantic": "Schließen", + "loadingContent": "Inhalt wird geladen", + "noItems": "Keine Elemente", + "noItemsToDisplay": "Keine Elemente zum Anzeigen", + "loadMore": "Mehr laden", + "workspace": "Arbeitsbereich", + "recentFiles": "Zuletzt verwendete Dateien", + "knowledgeBase": "Wissensdatenbank", + "noFilesYet": "Noch keine Dateien", + "uploadDocsPrompt": "Lade Dokumente hoch, um sie in deinen Unterhaltungen mit Conduit zu verwenden", + "uploadFirstFile": "Erste Datei hochladen", + "knowledgeBaseEmpty": "Wissensdatenbank ist leer", + "createCollectionsPrompt": "Erstelle Sammlungen verwandter Dokumente zur einfachen Referenz", + "chooseSourcePhoto": "Quelle auswählen", + "takePhoto": "Foto aufnehmen", + "chooseFromGallery": "Aus Fotos auswählen", + "document": "Dokument", + "documentHint": "PDF-, Word- oder Textdatei", + "uploadFileTitle": "Datei hochladen", + "fileUploadComingSoon": "Dateiupload für {type} kommt bald!", + "kbCreationComingSoon": "Erstellung der Wissensdatenbank kommt bald!", + "backToServerSetup": "Zur Servereinrichtung zurück", + "connectedToServer": "Mit Server verbunden", + "signIn": "Anmelden", + "enterCredentials": "Gib deine Anmeldedaten ein, um auf deine KI-Unterhaltungen zuzugreifen", + "credentials": "Zugangsdaten", + "apiKey": "API-Schlüssel", + "usernameOrEmail": "Benutzername oder E‑Mail", + "password": "Passwort", + "signInWithApiKey": "Mit API-Schlüssel anmelden", + "connectToServer": "Mit Server verbinden", + "enterServerAddress": "Gib die Adresse deines Open-WebUI-Servers ein, um zu beginnen", + "serverUrl": "Server-URL", + "serverUrlHint": "https://dein-server.com", + "enterServerUrlSemantic": "Gib deine Server-URL oder IP-Adresse ein", + "headerName": "Header-Name", + "headerValue": "Header-Wert", + "headerValueHint": "api-key-123 oder Bearer-Token", + "addHeader": "Header hinzufügen", + "maximumHeadersReached": "Maximale Anzahl erreicht", + "removeHeader": "Header entfernen", + "connecting": "Verbindung wird hergestellt...", + "connectToServerButton": "Mit Server verbinden", + "demoModeActive": "Demo-Modus aktiv", + "skipServerSetupTryDemo": "Servereinrichtung überspringen und Demo testen", + "enterDemo": "Demo starten", + "demoBadge": "Demo", + "serverNotOpenWebUI": "Dies scheint kein Open-WebUI-Server zu sein.", + "serverUrlEmpty": "Server-URL darf nicht leer sein", + "invalidUrlFormat": "Ungültiges URL-Format. Bitte Eingabe prüfen.", + "onlyHttpHttps": "Nur HTTP- und HTTPS-Protokolle werden unterstützt.", + "serverAddressRequired": "Serveradresse erforderlich (z. B. 192.168.1.10 oder example.com).", + "portRange": "Port muss zwischen 1 und 65535 liegen.", + "invalidIpFormat": "Ungültiges IP-Format. Beispiel: 192.168.1.10.", + "couldNotConnectGeneric": "Verbindung fehlgeschlagen. Adresse prüfen und erneut versuchen.", + "weCouldntReachServer": "Server nicht erreichbar. Verbindung und Serverstatus prüfen.", + "connectionTimedOut": "Zeitüberschreitung. Server eventuell ausgelastet oder blockiert.", + "useHttpOrHttpsOnly": "Nur http:// oder https:// verwenden.", + "loginFailed": "Anmeldung fehlgeschlagen", + "invalidCredentials": "Ungültiger Benutzername oder Passwort. Bitte erneut versuchen.", + "serverRedirectingHttps": "Server leitet um. HTTPS-Konfiguration prüfen.", + "unableToConnectServer": "Verbindung zum Server nicht möglich. Bitte Verbindung prüfen.", + "requestTimedOut": "Zeitüberschreitung. Bitte erneut versuchen.", + "genericSignInFailed": "Anmeldung nicht möglich. Zugangsdaten und Server prüfen.", + "skip": "Überspringen", + "next": "Weiter", + "done": "Fertig", + "onboardStartTitle": "Unterhaltung starten", + "onboardStartSubtitle": "Wähle ein Modell und tippe los. Tippe jederzeit auf Neuer Chat.", + "onboardStartBullet1": "Modellname oben antippen, um zu wechseln", + "onboardStartBullet2": "Mit Neuer Chat den Kontext zurücksetzen", + "onboardAttachTitle": "Kontext anhängen", + "onboardAttachSubtitle": "Antworten mit Dateien oder Bildern untermauern.", + "onboardAttachBullet1": "Dateien: PDFs, Dokumente, Datensätze", + "onboardAttachBullet2": "Bilder: Fotos oder Screenshots", + "onboardSpeakTitle": "Natürlich sprechen", + "onboardSpeakSubtitle": "Auf das Mikro tippen, um zu diktieren.", + "onboardSpeakBullet1": "Jederzeit stoppen; Text bleibt erhalten", + "onboardSpeakBullet2": "Ideal für kurze Notizen oder lange Prompts", + "onboardQuickTitle": "Schnellaktionen", + "onboardQuickSubtitle": "Links oben das Menü für Chats und Navigation öffnen.", + "onboardQuickBullet1": "Menü tippen, um Chats und Navigation zu öffnen", + "onboardQuickBullet2": "Schnell zu Neuer Chat, Dateien oder Profil springen" + , + "addAttachment": "Anhang hinzufügen", + "tools": "Werkzeuge", + "voiceInput": "Spracheingabe", + "messageInputLabel": "Nachrichteneingabe", + "messageInputHint": "Nachricht eingeben", + "messageHintText": "Nachricht...", + "stopGenerating": "Generierung stoppen", + "send": "Senden", + "sendMessage": "Nachricht senden", + "file": "Datei", + "photo": "Foto", + "camera": "Kamera", + "apiUnavailable": "API-Dienst nicht verfügbar", + "unableToLoadImage": "Bild kann nicht geladen werden", + "notAnImageFile": "Keine Bilddatei: {fileName}", + "failedToLoadImage": "Bild konnte nicht geladen werden: {error}", + "invalidDataUrl": "Ungültiges Data-URL-Format", + "failedToDecodeImage": "Bild konnte nicht decodiert werden", + "invalidImageFormat": "Ungültiges Bildformat", + "emptyImageData": "Leere Bilddaten" + , + "offlineBanner": "Du bist offline. Einige Funktionen sind eingeschränkt.", + "featureRequiresInternet": "Diese Funktion erfordert eine Internetverbindung", + "messagesWillSendWhenOnline": "Nachrichten werden gesendet, sobald du wieder online bist", + "confirm": "Bestätigen", + "cancel": "Abbrechen" + , + "ok": "OK", + "inputField": "Eingabefeld", + "captureDocumentOrImage": "Dokument oder Bild aufnehmen", + "checkConnection": "Verbindung prüfen", + "openSettings": "Einstellungen öffnen", + "chooseDifferentFile": "Andere Datei wählen", + "goBack": "Zurück", + "technicalDetails": "Technische Details" +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb new file mode 100644 index 0000000..f024d88 --- /dev/null +++ b/lib/l10n/app_en.arb @@ -0,0 +1,169 @@ +{ + "@@locale": "en", + "appTitle": "Conduit", + "initializationFailed": "Initialization Failed", + "retry": "Retry", + "back": "Back", + "you": "You", + "loadingProfile": "Loading profile...", + "unableToLoadProfile": "Unable to load profile", + "pleaseCheckConnection": "Please check your connection and try again", + "account": "Account", + "signOut": "Sign Out", + "endYourSession": "End your session", + "defaultModel": "Default Model", + "autoSelect": "Auto-select", + "loadingModels": "Loading models...", + "failedToLoadModels": "Failed to load models", + "availableModels": "Available Models", + "noResults": "No results", + "searchModels": "Search models...", + "errorMessage": "Something went wrong. Please try again.", + "loginButton": "Login", + "menuItem": "Settings", + "dynamicContentWithPlaceholder": "Welcome, {name}!", + "@dynamicContentWithPlaceholder": { + "description": "Greeting message with a dynamic user name.", + "placeholders": { + "name": { + "type": "String", + "example": "Alex" + } + } + }, + "itemsCount": "{count, plural, =0{No items} one{1 item} other{{count} items}}", + "@itemsCount": { + "description": "Pluralized count of items.", + "placeholders": { + "count": { + "type": "int", + "example": "3" + } + } + }, + "closeButtonSemantic": "Close", + "loadingContent": "Loading content", + "noItems": "No items", + "noItemsToDisplay": "No items to display", + "loadMore": "Load More", + "workspace": "Workspace", + "recentFiles": "Recent Files", + "knowledgeBase": "Knowledge Base", + "noFilesYet": "No files yet", + "uploadDocsPrompt": "Upload documents to reference in your conversations with Conduit", + "uploadFirstFile": "Upload your first file", + "knowledgeBaseEmpty": "Knowledge base is empty", + "createCollectionsPrompt": "Create collections of related documents for easy reference", + "chooseSourcePhoto": "Choose your source", + "takePhoto": "Take a photo", + "chooseFromGallery": "Choose from your photos", + "document": "Document", + "documentHint": "PDF, Word, or text file", + "uploadFileTitle": "Upload File", + "fileUploadComingSoon": "File upload for {type} is coming soon!", + "@fileUploadComingSoon": { + "placeholders": {"type": {"type": "String", "example": "gallery"}}, + "description": "Temporary message for upcoming upload feature by type" + }, + "kbCreationComingSoon": "Knowledge base creation is coming soon!", + "backToServerSetup": "Back to server setup", + "connectedToServer": "Connected to Server", + "signIn": "Sign In", + "enterCredentials": "Enter your credentials to access your AI conversations", + "credentials": "Credentials", + "apiKey": "API Key", + "usernameOrEmail": "Username or Email", + "password": "Password", + "signInWithApiKey": "Sign in with API Key", + "connectToServer": "Connect to Server", + "enterServerAddress": "Enter your Open-WebUI server address to get started", + "serverUrl": "Server URL", + "serverUrlHint": "https://your-server.com", + "enterServerUrlSemantic": "Enter your server URL or IP address", + "headerName": "Header Name", + "headerValue": "Header Value", + "headerValueHint": "api-key-123 or Bearer token", + "addHeader": "Add header", + "maximumHeadersReached": "Maximum headers reached", + "removeHeader": "Remove header", + "connecting": "Connecting...", + "connectToServerButton": "Connect to Server", + "demoModeActive": "Demo Mode Active", + "skipServerSetupTryDemo": "Skip server setup and try the demo", + "enterDemo": "Enter Demo", + "demoBadge": "Demo", + "serverNotOpenWebUI": "This does not appear to be an Open-WebUI server.", + "serverUrlEmpty": "Server URL cannot be empty", + "invalidUrlFormat": "Invalid URL format. Please check your input.", + "onlyHttpHttps": "Only HTTP and HTTPS protocols are supported.", + "serverAddressRequired": "Server address is required (e.g., 192.168.1.10 or example.com).", + "portRange": "Port must be between 1 and 65535.", + "invalidIpFormat": "Invalid IP address format. Use format like 192.168.1.10.", + "couldNotConnectGeneric": "Couldn't connect. Double-check the address and try again.", + "weCouldntReachServer": "We couldn't reach the server. Check your connection and that the server is running.", + "connectionTimedOut": "Connection timed out. The server might be busy or blocked by a firewall.", + "useHttpOrHttpsOnly": "Use http:// or https:// only.", + "loginFailed": "Login failed", + "invalidCredentials": "Invalid username or password. Please try again.", + "serverRedirectingHttps": "The server is redirecting requests. Check your server's HTTPS configuration.", + "unableToConnectServer": "Unable to connect to server. Please check your connection.", + "requestTimedOut": "The request timed out. Please try again.", + "genericSignInFailed": "We couldn't sign you in. Check your credentials and server settings.", + "skip": "Skip", + "next": "Next", + "done": "Done", + "onboardStartTitle": "Start a conversation", + "onboardStartSubtitle": "Choose a model, then type below to begin. Tap New Chat anytime.", + "onboardStartBullet1": "Tap the model name in the top bar to switch models", + "onboardStartBullet2": "Use New Chat to reset context", + "onboardAttachTitle": "Attach context", + "onboardAttachSubtitle": "Ground responses by adding files or images.", + "onboardAttachBullet1": "Files: PDFs, docs, datasets", + "onboardAttachBullet2": "Images: photos or screenshots", + "onboardSpeakTitle": "Speak naturally", + "onboardSpeakSubtitle": "Tap the mic to dictate with live waveform feedback.", + "onboardSpeakBullet1": "Stop anytime; partial text is preserved", + "onboardSpeakBullet2": "Great for quick notes or long prompts", + "onboardQuickTitle": "Quick actions", + "onboardQuickSubtitle": "Use the top‑left menu to open the chats list and navigation.", + "onboardQuickBullet1": "Tap the menu to open the chats list and navigation", + "onboardQuickBullet2": "Jump instantly to New Chat, Files, or Profile" + , + "addAttachment": "Add attachment", + "tools": "Tools", + "voiceInput": "Voice input", + "messageInputLabel": "Message input", + "messageInputHint": "Type your message", + "messageHintText": "Message...", + "stopGenerating": "Stop generating", + "send": "Send", + "sendMessage": "Send message", + "file": "File", + "photo": "Photo", + "camera": "Camera", + "apiUnavailable": "API service not available", + "unableToLoadImage": "Unable to load image", + "notAnImageFile": "Not an image file: {fileName}", + "@notAnImageFile": {"placeholders": {"fileName": {"type": "String", "example": "image.txt"}}}, + "failedToLoadImage": "Failed to load image: {error}", + "@failedToLoadImage": {"placeholders": {"error": {"type": "String", "example": "Network error"}}}, + "invalidDataUrl": "Invalid data URL format", + "failedToDecodeImage": "Failed to decode image", + "invalidImageFormat": "Invalid image format", + "emptyImageData": "Empty image data" + , + "offlineBanner": "You're offline. Some features may be limited.", + "featureRequiresInternet": "This feature requires an internet connection", + "messagesWillSendWhenOnline": "Messages will be sent when you're back online", + "confirm": "Confirm", + "cancel": "Cancel" + , + "ok": "OK", + "inputField": "Input field", + "captureDocumentOrImage": "Capture a document or image", + "checkConnection": "Check Connection", + "openSettings": "Open Settings", + "chooseDifferentFile": "Choose Different File", + "goBack": "Go Back", + "technicalDetails": "Technical Details" +} diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb new file mode 100644 index 0000000..274694b --- /dev/null +++ b/lib/l10n/app_fr.arb @@ -0,0 +1,145 @@ +{ + "@@locale": "fr", + "appTitle": "Conduit", + "initializationFailed": "Échec de l'initialisation", + "retry": "Réessayer", + "back": "Retour", + "you": "Vous", + "loadingProfile": "Chargement du profil...", + "unableToLoadProfile": "Impossible de charger le profil", + "pleaseCheckConnection": "Veuillez vérifier votre connexion et réessayer", + "account": "Compte", + "signOut": "Se déconnecter", + "endYourSession": "Terminer votre session", + "defaultModel": "Modèle par défaut", + "autoSelect": "Sélection automatique", + "loadingModels": "Chargement des modèles...", + "failedToLoadModels": "Échec du chargement des modèles", + "availableModels": "Modèles disponibles", + "noResults": "Aucun résultat", + "searchModels": "Rechercher des modèles...", + "errorMessage": "Une erreur s'est produite. Veuillez réessayer.", + "loginButton": "Connexion", + "menuItem": "Paramètres", + "dynamicContentWithPlaceholder": "Bienvenue, {name} !", + "itemsCount": "{count, plural, =0{Aucun élément} one{1 élément} other{{count} éléments}}", + "closeButtonSemantic": "Fermer", + "loadingContent": "Chargement du contenu", + "noItems": "Aucun élément", + "noItemsToDisplay": "Aucun élément à afficher", + "loadMore": "Charger plus", + "workspace": "Espace de travail", + "recentFiles": "Fichiers récents", + "knowledgeBase": "Base de connaissances", + "noFilesYet": "Pas encore de fichiers", + "uploadDocsPrompt": "Importez des documents à utiliser dans vos conversations avec Conduit", + "uploadFirstFile": "Importer votre premier fichier", + "knowledgeBaseEmpty": "La base de connaissances est vide", + "createCollectionsPrompt": "Créez des collections de documents liés pour une référence facile", + "chooseSourcePhoto": "Choisir la source", + "takePhoto": "Prendre une photo", + "chooseFromGallery": "Choisir depuis vos photos", + "document": "Document", + "documentHint": "Fichier PDF, Word ou texte", + "uploadFileTitle": "Importer un fichier", + "fileUploadComingSoon": "Le téléversement de fichiers pour {type} arrive bientôt !", + "kbCreationComingSoon": "La création de la base de connaissances arrive bientôt !", + "backToServerSetup": "Retour à la configuration du serveur", + "connectedToServer": "Connecté au serveur", + "signIn": "Se connecter", + "enterCredentials": "Entrez vos identifiants pour accéder à vos conversations IA", + "credentials": "Identifiants", + "apiKey": "Clé API", + "usernameOrEmail": "Nom d'utilisateur ou e‑mail", + "password": "Mot de passe", + "signInWithApiKey": "Se connecter avec une clé API", + "connectToServer": "Se connecter au serveur", + "enterServerAddress": "Saisissez l'adresse de votre serveur Open-WebUI pour commencer", + "serverUrl": "URL du serveur", + "serverUrlHint": "https://votre-serveur.com", + "enterServerUrlSemantic": "Saisissez l'URL ou l'adresse IP de votre serveur", + "headerName": "Nom de l'en-tête", + "headerValue": "Valeur de l'en-tête", + "headerValueHint": "api-key-123 ou jeton Bearer", + "addHeader": "Ajouter l'en-tête", + "maximumHeadersReached": "Nombre maximal atteint", + "removeHeader": "Supprimer l'en-tête", + "connecting": "Connexion en cours...", + "connectToServerButton": "Se connecter au serveur", + "demoModeActive": "Mode démo activé", + "skipServerSetupTryDemo": "Ignorer la configuration et essayer la démo", + "enterDemo": "Entrer en démo", + "demoBadge": "Démo", + "serverNotOpenWebUI": "Ceci ne semble pas être un serveur Open-WebUI.", + "serverUrlEmpty": "L'URL du serveur ne peut pas être vide", + "invalidUrlFormat": "Format d'URL invalide. Veuillez vérifier votre saisie.", + "onlyHttpHttps": "Seuls les protocoles HTTP et HTTPS sont pris en charge.", + "serverAddressRequired": "Adresse du serveur requise (ex. 192.168.1.10 ou example.com).", + "portRange": "Le port doit être compris entre 1 et 65535.", + "invalidIpFormat": "Format d'IP invalide. Exemple : 192.168.1.10.", + "couldNotConnectGeneric": "Connexion impossible. Vérifiez l'adresse et réessayez.", + "weCouldntReachServer": "Impossible d'atteindre le serveur. Vérifiez la connexion et l'état du serveur.", + "connectionTimedOut": "Délai d'attente dépassé. Le serveur est peut-être occupé ou bloqué.", + "useHttpOrHttpsOnly": "Utilisez uniquement http:// ou https://.", + "loginFailed": "Échec de la connexion", + "invalidCredentials": "Nom d'utilisateur ou mot de passe invalide. Réessayez.", + "serverRedirectingHttps": "Le serveur redirige les requêtes. Vérifiez la configuration HTTPS.", + "unableToConnectServer": "Impossible de se connecter au serveur. Vérifiez votre connexion.", + "requestTimedOut": "Délai d'attente dépassé. Réessayez.", + "genericSignInFailed": "Connexion impossible. Vérifiez vos identifiants et le serveur.", + "skip": "Ignorer", + "next": "Suivant", + "done": "Terminé", + "onboardStartTitle": "Commencer une conversation", + "onboardStartSubtitle": "Choisissez un modèle puis commencez à écrire. Touchez Nouveau chat à tout moment.", + "onboardStartBullet1": "Touchez le nom du modèle en haut pour changer", + "onboardStartBullet2": "Utilisez Nouveau chat pour réinitialiser le contexte", + "onboardAttachTitle": "Ajouter du contexte", + "onboardAttachSubtitle": "Améliorez les réponses en ajoutant des fichiers ou des images.", + "onboardAttachBullet1": "Fichiers : PDF, documents, jeux de données", + "onboardAttachBullet2": "Images : photos ou captures d'écran", + "onboardSpeakTitle": "Parlez naturellement", + "onboardSpeakSubtitle": "Touchez le micro pour dicter avec retour visuel.", + "onboardSpeakBullet1": "Arrêtez à tout moment ; le texte partiel est conservé", + "onboardSpeakBullet2": "Idéal pour des notes rapides ou de longs prompts", + "onboardQuickTitle": "Actions rapides", + "onboardQuickSubtitle": "Utilisez le menu en haut à gauche pour ouvrir la liste des chats et la navigation.", + "onboardQuickBullet1": "Touchez le menu pour ouvrir les chats et la navigation", + "onboardQuickBullet2": "Accédez rapidement à Nouveau chat, Fichiers ou Profil" + , + "addAttachment": "Ajouter une pièce jointe", + "tools": "Outils", + "voiceInput": "Entrée vocale", + "messageInputLabel": "Saisie du message", + "messageInputHint": "Saisissez votre message", + "messageHintText": "Message...", + "stopGenerating": "Arrêter la génération", + "send": "Envoyer", + "sendMessage": "Envoyer le message", + "file": "Fichier", + "photo": "Photo", + "camera": "Appareil photo", + "apiUnavailable": "Service API indisponible", + "unableToLoadImage": "Impossible de charger l'image", + "notAnImageFile": "Ce n'est pas un fichier image : {fileName}", + "failedToLoadImage": "Échec du chargement de l'image : {error}", + "invalidDataUrl": "Format d'URL de données invalide", + "failedToDecodeImage": "Échec du décodage de l'image", + "invalidImageFormat": "Format d'image invalide", + "emptyImageData": "Données d'image vides" + , + "offlineBanner": "Vous êtes hors ligne. Certaines fonctions peuvent être limitées.", + "featureRequiresInternet": "Cette fonctionnalité nécessite une connexion Internet", + "messagesWillSendWhenOnline": "Les messages seront envoyés lorsque vous serez de nouveau en ligne", + "confirm": "Confirmer", + "cancel": "Annuler" + , + "ok": "OK", + "inputField": "Champ de saisie", + "captureDocumentOrImage": "Capturer un document ou une image", + "checkConnection": "Vérifier la connexion", + "openSettings": "Ouvrir les réglages", + "chooseDifferentFile": "Choisir un autre fichier", + "goBack": "Retour", + "technicalDetails": "Détails techniques" +} diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb new file mode 100644 index 0000000..6334c6e --- /dev/null +++ b/lib/l10n/app_it.arb @@ -0,0 +1,145 @@ +{ + "@@locale": "it", + "appTitle": "Conduit", + "initializationFailed": "Inizializzazione non riuscita", + "retry": "Riprova", + "back": "Indietro", + "you": "Tu", + "loadingProfile": "Caricamento profilo...", + "unableToLoadProfile": "Impossibile caricare il profilo", + "pleaseCheckConnection": "Controlla la connessione e riprova", + "account": "Account", + "signOut": "Esci", + "endYourSession": "Termina la sessione", + "defaultModel": "Modello predefinito", + "autoSelect": "Selezione automatica", + "loadingModels": "Caricamento modelli...", + "failedToLoadModels": "Impossibile caricare i modelli", + "availableModels": "Modelli disponibili", + "noResults": "Nessun risultato", + "searchModels": "Cerca modelli...", + "errorMessage": "Qualcosa è andato storto. Riprova.", + "loginButton": "Accedi", + "menuItem": "Impostazioni", + "dynamicContentWithPlaceholder": "Benvenuto, {name}!", + "itemsCount": "{count, plural, =0{Nessun elemento} one{1 elemento} other{{count} elementi}}", + "closeButtonSemantic": "Chiudi", + "loadingContent": "Caricamento contenuto", + "noItems": "Nessun elemento", + "noItemsToDisplay": "Nessun elemento da visualizzare", + "loadMore": "Carica altro", + "workspace": "Spazio di lavoro", + "recentFiles": "File recenti", + "knowledgeBase": "Base di conoscenza", + "noFilesYet": "Ancora nessun file", + "uploadDocsPrompt": "Carica documenti da usare nelle conversazioni con Conduit", + "uploadFirstFile": "Carica il tuo primo file", + "knowledgeBaseEmpty": "La base di conoscenza è vuota", + "createCollectionsPrompt": "Crea raccolte di documenti correlati per un rapido riferimento", + "chooseSourcePhoto": "Scegli origine", + "takePhoto": "Scatta una foto", + "chooseFromGallery": "Scegli dalle foto", + "document": "Documento", + "documentHint": "File PDF, Word o di testo", + "uploadFileTitle": "Carica file", + "fileUploadComingSoon": "Il caricamento file per {type} arriverà presto!", + "kbCreationComingSoon": "La creazione della base di conoscenza arriverà presto!", + "backToServerSetup": "Torna alla configurazione del server", + "connectedToServer": "Connesso al server", + "signIn": "Accedi", + "enterCredentials": "Inserisci le credenziali per accedere alle conversazioni IA", + "credentials": "Credenziali", + "apiKey": "Chiave API", + "usernameOrEmail": "Username o e‑mail", + "password": "Password", + "signInWithApiKey": "Accedi con chiave API", + "connectToServer": "Connetti al server", + "enterServerAddress": "Inserisci l'indirizzo del server Open-WebUI per iniziare", + "serverUrl": "URL del server", + "serverUrlHint": "https://tuo-server.com", + "enterServerUrlSemantic": "Inserisci l'URL o l'indirizzo IP del server", + "headerName": "Nome header", + "headerValue": "Valore header", + "headerValueHint": "api-key-123 o token Bearer", + "addHeader": "Aggiungi header", + "maximumHeadersReached": "Numero massimo raggiunto", + "removeHeader": "Rimuovi header", + "connecting": "Connessione in corso...", + "connectToServerButton": "Connetti al server", + "demoModeActive": "Modalità demo attiva", + "skipServerSetupTryDemo": "Salta configurazione server e prova la demo", + "enterDemo": "Entra in demo", + "demoBadge": "Demo", + "serverNotOpenWebUI": "Questo non sembra un server Open-WebUI.", + "serverUrlEmpty": "L'URL del server non può essere vuoto", + "invalidUrlFormat": "Formato URL non valido. Controlla l'input.", + "onlyHttpHttps": "Sono supportati solo i protocolli HTTP e HTTPS.", + "serverAddressRequired": "Indirizzo server richiesto (es. 192.168.1.10 o example.com).", + "portRange": "La porta deve essere tra 1 e 65535.", + "invalidIpFormat": "Formato IP non valido. Esempio: 192.168.1.10.", + "couldNotConnectGeneric": "Impossibile connettersi. Verifica l'indirizzo e riprova.", + "weCouldntReachServer": "Impossibile raggiungere il server. Verifica connessione e stato del server.", + "connectionTimedOut": "Tempo scaduto. Il server potrebbe essere occupato o bloccato.", + "useHttpOrHttpsOnly": "Usa solo http:// o https://.", + "loginFailed": "Accesso non riuscito", + "invalidCredentials": "Nome utente o password non validi. Riprova.", + "serverRedirectingHttps": "Il server sta reindirizzando. Controlla la configurazione HTTPS.", + "unableToConnectServer": "Impossibile connettersi al server. Controlla la connessione.", + "requestTimedOut": "Richiesta scaduta. Riprova.", + "genericSignInFailed": "Impossibile accedere. Controlla credenziali e server.", + "skip": "Salta", + "next": "Avanti", + "done": "Fatto", + "onboardStartTitle": "Inizia una conversazione", + "onboardStartSubtitle": "Scegli un modello e inizia a scrivere. Tocca Nuova chat in qualsiasi momento.", + "onboardStartBullet1": "Tocca il nome del modello in alto per cambiare", + "onboardStartBullet2": "Usa Nuova chat per azzerare il contesto", + "onboardAttachTitle": "Aggiungi contesto", + "onboardAttachSubtitle": "Migliora le risposte aggiungendo file o immagini.", + "onboardAttachBullet1": "File: PDF, documenti, dataset", + "onboardAttachBullet2": "Immagini: foto o screenshot", + "onboardSpeakTitle": "Parla in modo naturale", + "onboardSpeakSubtitle": "Tocca il microfono per dettare con feedback visivo.", + "onboardSpeakBullet1": "Interrompi in qualsiasi momento; il testo parziale viene mantenuto", + "onboardSpeakBullet2": "Ottimo per note rapide o prompt lunghi", + "onboardQuickTitle": "Azioni rapide", + "onboardQuickSubtitle": "Usa il menu in alto a sinistra per aprire l'elenco chat e la navigazione.", + "onboardQuickBullet1": "Tocca il menu per aprire chat e navigazione", + "onboardQuickBullet2": "Vai subito a Nuova chat, File o Profilo" + , + "addAttachment": "Aggiungi allegato", + "tools": "Strumenti", + "voiceInput": "Input vocale", + "messageInputLabel": "Input messaggio", + "messageInputHint": "Scrivi il tuo messaggio", + "messageHintText": "Messaggio...", + "stopGenerating": "Interrompi generazione", + "send": "Invia", + "sendMessage": "Invia messaggio", + "file": "File", + "photo": "Foto", + "camera": "Fotocamera", + "apiUnavailable": "Servizio API non disponibile", + "unableToLoadImage": "Impossibile caricare l'immagine", + "notAnImageFile": "Non è un file immagine: {fileName}", + "failedToLoadImage": "Impossibile caricare l'immagine: {error}", + "invalidDataUrl": "Formato data URL non valido", + "failedToDecodeImage": "Impossibile decodificare l'immagine", + "invalidImageFormat": "Formato immagine non valido", + "emptyImageData": "Dati immagine vuoti" + , + "offlineBanner": "Sei offline. Alcune funzioni potrebbero essere limitate.", + "featureRequiresInternet": "Questa funzione richiede una connessione Internet", + "messagesWillSendWhenOnline": "I messaggi verranno inviati quando tornerai online", + "confirm": "Conferma", + "cancel": "Annulla" + , + "ok": "OK", + "inputField": "Campo di input", + "captureDocumentOrImage": "Acquisisci un documento o un'immagine", + "checkConnection": "Controlla connessione", + "openSettings": "Apri impostazioni", + "chooseDifferentFile": "Scegli un altro file", + "goBack": "Indietro", + "technicalDetails": "Dettagli tecnici" +} diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart new file mode 100644 index 0000000..b6ca4b6 --- /dev/null +++ b/lib/l10n/app_localizations.dart @@ -0,0 +1,969 @@ +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_de.dart'; +import 'app_localizations_en.dart'; +import 'app_localizations_fr.dart'; +import 'app_localizations_it.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('de'), + Locale('en'), + Locale('fr'), + Locale('it') + ]; + + /// No description provided for @appTitle. + /// + /// In en, this message translates to: + /// **'Conduit'** + String get appTitle; + + /// No description provided for @initializationFailed. + /// + /// In en, this message translates to: + /// **'Initialization Failed'** + String get initializationFailed; + + /// No description provided for @retry. + /// + /// In en, this message translates to: + /// **'Retry'** + String get retry; + + /// No description provided for @back. + /// + /// In en, this message translates to: + /// **'Back'** + String get back; + + /// No description provided for @you. + /// + /// In en, this message translates to: + /// **'You'** + String get you; + + /// No description provided for @loadingProfile. + /// + /// In en, this message translates to: + /// **'Loading profile...'** + String get loadingProfile; + + /// No description provided for @unableToLoadProfile. + /// + /// In en, this message translates to: + /// **'Unable to load profile'** + String get unableToLoadProfile; + + /// No description provided for @pleaseCheckConnection. + /// + /// In en, this message translates to: + /// **'Please check your connection and try again'** + String get pleaseCheckConnection; + + /// No description provided for @account. + /// + /// In en, this message translates to: + /// **'Account'** + String get account; + + /// No description provided for @signOut. + /// + /// In en, this message translates to: + /// **'Sign Out'** + String get signOut; + + /// No description provided for @endYourSession. + /// + /// In en, this message translates to: + /// **'End your session'** + String get endYourSession; + + /// No description provided for @defaultModel. + /// + /// In en, this message translates to: + /// **'Default Model'** + String get defaultModel; + + /// No description provided for @autoSelect. + /// + /// In en, this message translates to: + /// **'Auto-select'** + String get autoSelect; + + /// No description provided for @loadingModels. + /// + /// In en, this message translates to: + /// **'Loading models...'** + String get loadingModels; + + /// No description provided for @failedToLoadModels. + /// + /// In en, this message translates to: + /// **'Failed to load models'** + String get failedToLoadModels; + + /// No description provided for @availableModels. + /// + /// In en, this message translates to: + /// **'Available Models'** + String get availableModels; + + /// No description provided for @noResults. + /// + /// In en, this message translates to: + /// **'No results'** + String get noResults; + + /// No description provided for @searchModels. + /// + /// In en, this message translates to: + /// **'Search models...'** + String get searchModels; + + /// No description provided for @errorMessage. + /// + /// In en, this message translates to: + /// **'Something went wrong. Please try again.'** + String get errorMessage; + + /// No description provided for @loginButton. + /// + /// In en, this message translates to: + /// **'Login'** + String get loginButton; + + /// No description provided for @menuItem. + /// + /// In en, this message translates to: + /// **'Settings'** + String get menuItem; + + /// Greeting message with a dynamic user name. + /// + /// In en, this message translates to: + /// **'Welcome, {name}!'** + String dynamicContentWithPlaceholder(String name); + + /// Pluralized count of items. + /// + /// In en, this message translates to: + /// **'{count, plural, =0{No items} one{1 item} other{{count} items}}'** + String itemsCount(int count); + + /// No description provided for @closeButtonSemantic. + /// + /// In en, this message translates to: + /// **'Close'** + String get closeButtonSemantic; + + /// No description provided for @loadingContent. + /// + /// In en, this message translates to: + /// **'Loading content'** + String get loadingContent; + + /// No description provided for @noItems. + /// + /// In en, this message translates to: + /// **'No items'** + String get noItems; + + /// No description provided for @noItemsToDisplay. + /// + /// In en, this message translates to: + /// **'No items to display'** + String get noItemsToDisplay; + + /// No description provided for @loadMore. + /// + /// In en, this message translates to: + /// **'Load More'** + String get loadMore; + + /// No description provided for @workspace. + /// + /// In en, this message translates to: + /// **'Workspace'** + String get workspace; + + /// No description provided for @recentFiles. + /// + /// In en, this message translates to: + /// **'Recent Files'** + String get recentFiles; + + /// No description provided for @knowledgeBase. + /// + /// In en, this message translates to: + /// **'Knowledge Base'** + String get knowledgeBase; + + /// No description provided for @noFilesYet. + /// + /// In en, this message translates to: + /// **'No files yet'** + String get noFilesYet; + + /// No description provided for @uploadDocsPrompt. + /// + /// In en, this message translates to: + /// **'Upload documents to reference in your conversations with Conduit'** + String get uploadDocsPrompt; + + /// No description provided for @uploadFirstFile. + /// + /// In en, this message translates to: + /// **'Upload your first file'** + String get uploadFirstFile; + + /// No description provided for @knowledgeBaseEmpty. + /// + /// In en, this message translates to: + /// **'Knowledge base is empty'** + String get knowledgeBaseEmpty; + + /// No description provided for @createCollectionsPrompt. + /// + /// In en, this message translates to: + /// **'Create collections of related documents for easy reference'** + String get createCollectionsPrompt; + + /// No description provided for @chooseSourcePhoto. + /// + /// In en, this message translates to: + /// **'Choose your source'** + String get chooseSourcePhoto; + + /// No description provided for @takePhoto. + /// + /// In en, this message translates to: + /// **'Take a photo'** + String get takePhoto; + + /// No description provided for @chooseFromGallery. + /// + /// In en, this message translates to: + /// **'Choose from your photos'** + String get chooseFromGallery; + + /// No description provided for @document. + /// + /// In en, this message translates to: + /// **'Document'** + String get document; + + /// No description provided for @documentHint. + /// + /// In en, this message translates to: + /// **'PDF, Word, or text file'** + String get documentHint; + + /// No description provided for @uploadFileTitle. + /// + /// In en, this message translates to: + /// **'Upload File'** + String get uploadFileTitle; + + /// Temporary message for upcoming upload feature by type + /// + /// In en, this message translates to: + /// **'File upload for {type} is coming soon!'** + String fileUploadComingSoon(String type); + + /// No description provided for @kbCreationComingSoon. + /// + /// In en, this message translates to: + /// **'Knowledge base creation is coming soon!'** + String get kbCreationComingSoon; + + /// No description provided for @backToServerSetup. + /// + /// In en, this message translates to: + /// **'Back to server setup'** + String get backToServerSetup; + + /// No description provided for @connectedToServer. + /// + /// In en, this message translates to: + /// **'Connected to Server'** + String get connectedToServer; + + /// No description provided for @signIn. + /// + /// In en, this message translates to: + /// **'Sign In'** + String get signIn; + + /// No description provided for @enterCredentials. + /// + /// In en, this message translates to: + /// **'Enter your credentials to access your AI conversations'** + String get enterCredentials; + + /// No description provided for @credentials. + /// + /// In en, this message translates to: + /// **'Credentials'** + String get credentials; + + /// No description provided for @apiKey. + /// + /// In en, this message translates to: + /// **'API Key'** + String get apiKey; + + /// No description provided for @usernameOrEmail. + /// + /// In en, this message translates to: + /// **'Username or Email'** + String get usernameOrEmail; + + /// No description provided for @password. + /// + /// In en, this message translates to: + /// **'Password'** + String get password; + + /// No description provided for @signInWithApiKey. + /// + /// In en, this message translates to: + /// **'Sign in with API Key'** + String get signInWithApiKey; + + /// No description provided for @connectToServer. + /// + /// In en, this message translates to: + /// **'Connect to Server'** + String get connectToServer; + + /// No description provided for @enterServerAddress. + /// + /// In en, this message translates to: + /// **'Enter your Open-WebUI server address to get started'** + String get enterServerAddress; + + /// No description provided for @serverUrl. + /// + /// In en, this message translates to: + /// **'Server URL'** + String get serverUrl; + + /// No description provided for @serverUrlHint. + /// + /// In en, this message translates to: + /// **'https://your-server.com'** + String get serverUrlHint; + + /// No description provided for @enterServerUrlSemantic. + /// + /// In en, this message translates to: + /// **'Enter your server URL or IP address'** + String get enterServerUrlSemantic; + + /// No description provided for @headerName. + /// + /// In en, this message translates to: + /// **'Header Name'** + String get headerName; + + /// No description provided for @headerValue. + /// + /// In en, this message translates to: + /// **'Header Value'** + String get headerValue; + + /// No description provided for @headerValueHint. + /// + /// In en, this message translates to: + /// **'api-key-123 or Bearer token'** + String get headerValueHint; + + /// No description provided for @addHeader. + /// + /// In en, this message translates to: + /// **'Add header'** + String get addHeader; + + /// No description provided for @maximumHeadersReached. + /// + /// In en, this message translates to: + /// **'Maximum headers reached'** + String get maximumHeadersReached; + + /// No description provided for @removeHeader. + /// + /// In en, this message translates to: + /// **'Remove header'** + String get removeHeader; + + /// No description provided for @connecting. + /// + /// In en, this message translates to: + /// **'Connecting...'** + String get connecting; + + /// No description provided for @connectToServerButton. + /// + /// In en, this message translates to: + /// **'Connect to Server'** + String get connectToServerButton; + + /// No description provided for @demoModeActive. + /// + /// In en, this message translates to: + /// **'Demo Mode Active'** + String get demoModeActive; + + /// No description provided for @skipServerSetupTryDemo. + /// + /// In en, this message translates to: + /// **'Skip server setup and try the demo'** + String get skipServerSetupTryDemo; + + /// No description provided for @enterDemo. + /// + /// In en, this message translates to: + /// **'Enter Demo'** + String get enterDemo; + + /// No description provided for @demoBadge. + /// + /// In en, this message translates to: + /// **'Demo'** + String get demoBadge; + + /// No description provided for @serverNotOpenWebUI. + /// + /// In en, this message translates to: + /// **'This does not appear to be an Open-WebUI server.'** + String get serverNotOpenWebUI; + + /// No description provided for @serverUrlEmpty. + /// + /// In en, this message translates to: + /// **'Server URL cannot be empty'** + String get serverUrlEmpty; + + /// No description provided for @invalidUrlFormat. + /// + /// In en, this message translates to: + /// **'Invalid URL format. Please check your input.'** + String get invalidUrlFormat; + + /// No description provided for @onlyHttpHttps. + /// + /// In en, this message translates to: + /// **'Only HTTP and HTTPS protocols are supported.'** + String get onlyHttpHttps; + + /// No description provided for @serverAddressRequired. + /// + /// In en, this message translates to: + /// **'Server address is required (e.g., 192.168.1.10 or example.com).'** + String get serverAddressRequired; + + /// No description provided for @portRange. + /// + /// In en, this message translates to: + /// **'Port must be between 1 and 65535.'** + String get portRange; + + /// No description provided for @invalidIpFormat. + /// + /// In en, this message translates to: + /// **'Invalid IP address format. Use format like 192.168.1.10.'** + String get invalidIpFormat; + + /// No description provided for @couldNotConnectGeneric. + /// + /// In en, this message translates to: + /// **'Couldn\'t connect. Double-check the address and try again.'** + String get couldNotConnectGeneric; + + /// No description provided for @weCouldntReachServer. + /// + /// In en, this message translates to: + /// **'We couldn\'t reach the server. Check your connection and that the server is running.'** + String get weCouldntReachServer; + + /// No description provided for @connectionTimedOut. + /// + /// In en, this message translates to: + /// **'Connection timed out. The server might be busy or blocked by a firewall.'** + String get connectionTimedOut; + + /// No description provided for @useHttpOrHttpsOnly. + /// + /// In en, this message translates to: + /// **'Use http:// or https:// only.'** + String get useHttpOrHttpsOnly; + + /// No description provided for @loginFailed. + /// + /// In en, this message translates to: + /// **'Login failed'** + String get loginFailed; + + /// No description provided for @invalidCredentials. + /// + /// In en, this message translates to: + /// **'Invalid username or password. Please try again.'** + String get invalidCredentials; + + /// No description provided for @serverRedirectingHttps. + /// + /// In en, this message translates to: + /// **'The server is redirecting requests. Check your server\'s HTTPS configuration.'** + String get serverRedirectingHttps; + + /// No description provided for @unableToConnectServer. + /// + /// In en, this message translates to: + /// **'Unable to connect to server. Please check your connection.'** + String get unableToConnectServer; + + /// No description provided for @requestTimedOut. + /// + /// In en, this message translates to: + /// **'The request timed out. Please try again.'** + String get requestTimedOut; + + /// No description provided for @genericSignInFailed. + /// + /// In en, this message translates to: + /// **'We couldn\'t sign you in. Check your credentials and server settings.'** + String get genericSignInFailed; + + /// No description provided for @skip. + /// + /// In en, this message translates to: + /// **'Skip'** + String get skip; + + /// No description provided for @next. + /// + /// In en, this message translates to: + /// **'Next'** + String get next; + + /// No description provided for @done. + /// + /// In en, this message translates to: + /// **'Done'** + String get done; + + /// No description provided for @onboardStartTitle. + /// + /// In en, this message translates to: + /// **'Start a conversation'** + String get onboardStartTitle; + + /// No description provided for @onboardStartSubtitle. + /// + /// In en, this message translates to: + /// **'Choose a model, then type below to begin. Tap New Chat anytime.'** + String get onboardStartSubtitle; + + /// No description provided for @onboardStartBullet1. + /// + /// In en, this message translates to: + /// **'Tap the model name in the top bar to switch models'** + String get onboardStartBullet1; + + /// No description provided for @onboardStartBullet2. + /// + /// In en, this message translates to: + /// **'Use New Chat to reset context'** + String get onboardStartBullet2; + + /// No description provided for @onboardAttachTitle. + /// + /// In en, this message translates to: + /// **'Attach context'** + String get onboardAttachTitle; + + /// No description provided for @onboardAttachSubtitle. + /// + /// In en, this message translates to: + /// **'Ground responses by adding files or images.'** + String get onboardAttachSubtitle; + + /// No description provided for @onboardAttachBullet1. + /// + /// In en, this message translates to: + /// **'Files: PDFs, docs, datasets'** + String get onboardAttachBullet1; + + /// No description provided for @onboardAttachBullet2. + /// + /// In en, this message translates to: + /// **'Images: photos or screenshots'** + String get onboardAttachBullet2; + + /// No description provided for @onboardSpeakTitle. + /// + /// In en, this message translates to: + /// **'Speak naturally'** + String get onboardSpeakTitle; + + /// No description provided for @onboardSpeakSubtitle. + /// + /// In en, this message translates to: + /// **'Tap the mic to dictate with live waveform feedback.'** + String get onboardSpeakSubtitle; + + /// No description provided for @onboardSpeakBullet1. + /// + /// In en, this message translates to: + /// **'Stop anytime; partial text is preserved'** + String get onboardSpeakBullet1; + + /// No description provided for @onboardSpeakBullet2. + /// + /// In en, this message translates to: + /// **'Great for quick notes or long prompts'** + String get onboardSpeakBullet2; + + /// No description provided for @onboardQuickTitle. + /// + /// In en, this message translates to: + /// **'Quick actions'** + String get onboardQuickTitle; + + /// No description provided for @onboardQuickSubtitle. + /// + /// In en, this message translates to: + /// **'Use the top‑left menu to open the chats list and navigation.'** + String get onboardQuickSubtitle; + + /// No description provided for @onboardQuickBullet1. + /// + /// In en, this message translates to: + /// **'Tap the menu to open the chats list and navigation'** + String get onboardQuickBullet1; + + /// No description provided for @onboardQuickBullet2. + /// + /// In en, this message translates to: + /// **'Jump instantly to New Chat, Files, or Profile'** + String get onboardQuickBullet2; + + /// No description provided for @addAttachment. + /// + /// In en, this message translates to: + /// **'Add attachment'** + String get addAttachment; + + /// No description provided for @tools. + /// + /// In en, this message translates to: + /// **'Tools'** + String get tools; + + /// No description provided for @voiceInput. + /// + /// In en, this message translates to: + /// **'Voice input'** + String get voiceInput; + + /// No description provided for @messageInputLabel. + /// + /// In en, this message translates to: + /// **'Message input'** + String get messageInputLabel; + + /// No description provided for @messageInputHint. + /// + /// In en, this message translates to: + /// **'Type your message'** + String get messageInputHint; + + /// No description provided for @messageHintText. + /// + /// In en, this message translates to: + /// **'Message...'** + String get messageHintText; + + /// No description provided for @stopGenerating. + /// + /// In en, this message translates to: + /// **'Stop generating'** + String get stopGenerating; + + /// No description provided for @send. + /// + /// In en, this message translates to: + /// **'Send'** + String get send; + + /// No description provided for @sendMessage. + /// + /// In en, this message translates to: + /// **'Send message'** + String get sendMessage; + + /// No description provided for @file. + /// + /// In en, this message translates to: + /// **'File'** + String get file; + + /// No description provided for @photo. + /// + /// In en, this message translates to: + /// **'Photo'** + String get photo; + + /// No description provided for @camera. + /// + /// In en, this message translates to: + /// **'Camera'** + String get camera; + + /// No description provided for @apiUnavailable. + /// + /// In en, this message translates to: + /// **'API service not available'** + String get apiUnavailable; + + /// No description provided for @unableToLoadImage. + /// + /// In en, this message translates to: + /// **'Unable to load image'** + String get unableToLoadImage; + + /// No description provided for @notAnImageFile. + /// + /// In en, this message translates to: + /// **'Not an image file: {fileName}'** + String notAnImageFile(String fileName); + + /// No description provided for @failedToLoadImage. + /// + /// In en, this message translates to: + /// **'Failed to load image: {error}'** + String failedToLoadImage(String error); + + /// No description provided for @invalidDataUrl. + /// + /// In en, this message translates to: + /// **'Invalid data URL format'** + String get invalidDataUrl; + + /// No description provided for @failedToDecodeImage. + /// + /// In en, this message translates to: + /// **'Failed to decode image'** + String get failedToDecodeImage; + + /// No description provided for @invalidImageFormat. + /// + /// In en, this message translates to: + /// **'Invalid image format'** + String get invalidImageFormat; + + /// No description provided for @emptyImageData. + /// + /// In en, this message translates to: + /// **'Empty image data'** + String get emptyImageData; + + /// No description provided for @offlineBanner. + /// + /// In en, this message translates to: + /// **'You\'re offline. Some features may be limited.'** + String get offlineBanner; + + /// No description provided for @featureRequiresInternet. + /// + /// In en, this message translates to: + /// **'This feature requires an internet connection'** + String get featureRequiresInternet; + + /// No description provided for @messagesWillSendWhenOnline. + /// + /// In en, this message translates to: + /// **'Messages will be sent when you\'re back online'** + String get messagesWillSendWhenOnline; + + /// No description provided for @confirm. + /// + /// In en, this message translates to: + /// **'Confirm'** + String get confirm; + + /// No description provided for @cancel. + /// + /// In en, this message translates to: + /// **'Cancel'** + String get cancel; + + /// No description provided for @ok. + /// + /// In en, this message translates to: + /// **'OK'** + String get ok; + + /// No description provided for @inputField. + /// + /// In en, this message translates to: + /// **'Input field'** + String get inputField; + + /// No description provided for @captureDocumentOrImage. + /// + /// In en, this message translates to: + /// **'Capture a document or image'** + String get captureDocumentOrImage; + + /// No description provided for @checkConnection. + /// + /// In en, this message translates to: + /// **'Check Connection'** + String get checkConnection; + + /// No description provided for @openSettings. + /// + /// In en, this message translates to: + /// **'Open Settings'** + String get openSettings; + + /// No description provided for @chooseDifferentFile. + /// + /// In en, this message translates to: + /// **'Choose Different File'** + String get chooseDifferentFile; + + /// No description provided for @goBack. + /// + /// In en, this message translates to: + /// **'Go Back'** + String get goBack; + + /// No description provided for @technicalDetails. + /// + /// In en, this message translates to: + /// **'Technical Details'** + String get technicalDetails; +} + +class _AppLocalizationsDelegate extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => ['de', 'en', 'fr', 'it'].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 'de': return AppLocalizationsDe(); + case 'en': return AppLocalizationsEn(); + case 'fr': return AppLocalizationsFr(); + case 'it': return AppLocalizationsIt(); + } + + 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/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart new file mode 100644 index 0000000..830e9f3 --- /dev/null +++ b/lib/l10n/app_localizations_de.dart @@ -0,0 +1,444 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for German (`de`). +class AppLocalizationsDe extends AppLocalizations { + AppLocalizationsDe([String locale = 'de']) : super(locale); + + @override + String get appTitle => 'Conduit'; + + @override + String get initializationFailed => 'Initialisierung fehlgeschlagen'; + + @override + String get retry => 'Erneut versuchen'; + + @override + String get back => 'Zurück'; + + @override + String get you => 'Du'; + + @override + String get loadingProfile => 'Profil wird geladen...'; + + @override + String get unableToLoadProfile => 'Profil konnte nicht geladen werden'; + + @override + String get pleaseCheckConnection => 'Bitte überprüfe deine Verbindung und versuche es erneut'; + + @override + String get account => 'Konto'; + + @override + String get signOut => 'Abmelden'; + + @override + String get endYourSession => 'Sitzung beenden'; + + @override + String get defaultModel => 'Standardmodell'; + + @override + String get autoSelect => 'Automatische Auswahl'; + + @override + String get loadingModels => 'Modelle werden geladen...'; + + @override + String get failedToLoadModels => 'Modelle konnten nicht geladen werden'; + + @override + String get availableModels => 'Verfügbare Modelle'; + + @override + String get noResults => 'Keine Ergebnisse'; + + @override + String get searchModels => 'Modelle suchen...'; + + @override + String get errorMessage => 'Etwas ist schief gelaufen. Bitte versuche es erneut.'; + + @override + String get loginButton => 'Anmelden'; + + @override + String get menuItem => 'Einstellungen'; + + @override + String dynamicContentWithPlaceholder(String name) { + return 'Willkommen, $name!'; + } + + @override + String itemsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Elemente', + one: '1 Element', + zero: 'Keine Elemente', + ); + return '$_temp0'; + } + + @override + String get closeButtonSemantic => 'Schließen'; + + @override + String get loadingContent => 'Inhalt wird geladen'; + + @override + String get noItems => 'Keine Elemente'; + + @override + String get noItemsToDisplay => 'Keine Elemente zum Anzeigen'; + + @override + String get loadMore => 'Mehr laden'; + + @override + String get workspace => 'Arbeitsbereich'; + + @override + String get recentFiles => 'Zuletzt verwendete Dateien'; + + @override + String get knowledgeBase => 'Wissensdatenbank'; + + @override + String get noFilesYet => 'Noch keine Dateien'; + + @override + String get uploadDocsPrompt => 'Lade Dokumente hoch, um sie in deinen Unterhaltungen mit Conduit zu verwenden'; + + @override + String get uploadFirstFile => 'Erste Datei hochladen'; + + @override + String get knowledgeBaseEmpty => 'Wissensdatenbank ist leer'; + + @override + String get createCollectionsPrompt => 'Erstelle Sammlungen verwandter Dokumente zur einfachen Referenz'; + + @override + String get chooseSourcePhoto => 'Quelle auswählen'; + + @override + String get takePhoto => 'Foto aufnehmen'; + + @override + String get chooseFromGallery => 'Aus Fotos auswählen'; + + @override + String get document => 'Dokument'; + + @override + String get documentHint => 'PDF-, Word- oder Textdatei'; + + @override + String get uploadFileTitle => 'Datei hochladen'; + + @override + String fileUploadComingSoon(String type) { + return 'Dateiupload für $type kommt bald!'; + } + + @override + String get kbCreationComingSoon => 'Erstellung der Wissensdatenbank kommt bald!'; + + @override + String get backToServerSetup => 'Zur Servereinrichtung zurück'; + + @override + String get connectedToServer => 'Mit Server verbunden'; + + @override + String get signIn => 'Anmelden'; + + @override + String get enterCredentials => 'Gib deine Anmeldedaten ein, um auf deine KI-Unterhaltungen zuzugreifen'; + + @override + String get credentials => 'Zugangsdaten'; + + @override + String get apiKey => 'API-Schlüssel'; + + @override + String get usernameOrEmail => 'Benutzername oder E‑Mail'; + + @override + String get password => 'Passwort'; + + @override + String get signInWithApiKey => 'Mit API-Schlüssel anmelden'; + + @override + String get connectToServer => 'Mit Server verbinden'; + + @override + String get enterServerAddress => 'Gib die Adresse deines Open-WebUI-Servers ein, um zu beginnen'; + + @override + String get serverUrl => 'Server-URL'; + + @override + String get serverUrlHint => 'https://dein-server.com'; + + @override + String get enterServerUrlSemantic => 'Gib deine Server-URL oder IP-Adresse ein'; + + @override + String get headerName => 'Header-Name'; + + @override + String get headerValue => 'Header-Wert'; + + @override + String get headerValueHint => 'api-key-123 oder Bearer-Token'; + + @override + String get addHeader => 'Header hinzufügen'; + + @override + String get maximumHeadersReached => 'Maximale Anzahl erreicht'; + + @override + String get removeHeader => 'Header entfernen'; + + @override + String get connecting => 'Verbindung wird hergestellt...'; + + @override + String get connectToServerButton => 'Mit Server verbinden'; + + @override + String get demoModeActive => 'Demo-Modus aktiv'; + + @override + String get skipServerSetupTryDemo => 'Servereinrichtung überspringen und Demo testen'; + + @override + String get enterDemo => 'Demo starten'; + + @override + String get demoBadge => 'Demo'; + + @override + String get serverNotOpenWebUI => 'Dies scheint kein Open-WebUI-Server zu sein.'; + + @override + String get serverUrlEmpty => 'Server-URL darf nicht leer sein'; + + @override + String get invalidUrlFormat => 'Ungültiges URL-Format. Bitte Eingabe prüfen.'; + + @override + String get onlyHttpHttps => 'Nur HTTP- und HTTPS-Protokolle werden unterstützt.'; + + @override + String get serverAddressRequired => 'Serveradresse erforderlich (z. B. 192.168.1.10 oder example.com).'; + + @override + String get portRange => 'Port muss zwischen 1 und 65535 liegen.'; + + @override + String get invalidIpFormat => 'Ungültiges IP-Format. Beispiel: 192.168.1.10.'; + + @override + String get couldNotConnectGeneric => 'Verbindung fehlgeschlagen. Adresse prüfen und erneut versuchen.'; + + @override + String get weCouldntReachServer => 'Server nicht erreichbar. Verbindung und Serverstatus prüfen.'; + + @override + String get connectionTimedOut => 'Zeitüberschreitung. Server eventuell ausgelastet oder blockiert.'; + + @override + String get useHttpOrHttpsOnly => 'Nur http:// oder https:// verwenden.'; + + @override + String get loginFailed => 'Anmeldung fehlgeschlagen'; + + @override + String get invalidCredentials => 'Ungültiger Benutzername oder Passwort. Bitte erneut versuchen.'; + + @override + String get serverRedirectingHttps => 'Server leitet um. HTTPS-Konfiguration prüfen.'; + + @override + String get unableToConnectServer => 'Verbindung zum Server nicht möglich. Bitte Verbindung prüfen.'; + + @override + String get requestTimedOut => 'Zeitüberschreitung. Bitte erneut versuchen.'; + + @override + String get genericSignInFailed => 'Anmeldung nicht möglich. Zugangsdaten und Server prüfen.'; + + @override + String get skip => 'Überspringen'; + + @override + String get next => 'Weiter'; + + @override + String get done => 'Fertig'; + + @override + String get onboardStartTitle => 'Unterhaltung starten'; + + @override + String get onboardStartSubtitle => 'Wähle ein Modell und tippe los. Tippe jederzeit auf Neuer Chat.'; + + @override + String get onboardStartBullet1 => 'Modellname oben antippen, um zu wechseln'; + + @override + String get onboardStartBullet2 => 'Mit Neuer Chat den Kontext zurücksetzen'; + + @override + String get onboardAttachTitle => 'Kontext anhängen'; + + @override + String get onboardAttachSubtitle => 'Antworten mit Dateien oder Bildern untermauern.'; + + @override + String get onboardAttachBullet1 => 'Dateien: PDFs, Dokumente, Datensätze'; + + @override + String get onboardAttachBullet2 => 'Bilder: Fotos oder Screenshots'; + + @override + String get onboardSpeakTitle => 'Natürlich sprechen'; + + @override + String get onboardSpeakSubtitle => 'Auf das Mikro tippen, um zu diktieren.'; + + @override + String get onboardSpeakBullet1 => 'Jederzeit stoppen; Text bleibt erhalten'; + + @override + String get onboardSpeakBullet2 => 'Ideal für kurze Notizen oder lange Prompts'; + + @override + String get onboardQuickTitle => 'Schnellaktionen'; + + @override + String get onboardQuickSubtitle => 'Links oben das Menü für Chats und Navigation öffnen.'; + + @override + String get onboardQuickBullet1 => 'Menü tippen, um Chats und Navigation zu öffnen'; + + @override + String get onboardQuickBullet2 => 'Schnell zu Neuer Chat, Dateien oder Profil springen'; + + @override + String get addAttachment => 'Anhang hinzufügen'; + + @override + String get tools => 'Werkzeuge'; + + @override + String get voiceInput => 'Spracheingabe'; + + @override + String get messageInputLabel => 'Nachrichteneingabe'; + + @override + String get messageInputHint => 'Nachricht eingeben'; + + @override + String get messageHintText => 'Nachricht...'; + + @override + String get stopGenerating => 'Generierung stoppen'; + + @override + String get send => 'Senden'; + + @override + String get sendMessage => 'Nachricht senden'; + + @override + String get file => 'Datei'; + + @override + String get photo => 'Foto'; + + @override + String get camera => 'Kamera'; + + @override + String get apiUnavailable => 'API-Dienst nicht verfügbar'; + + @override + String get unableToLoadImage => 'Bild kann nicht geladen werden'; + + @override + String notAnImageFile(String fileName) { + return 'Keine Bilddatei: $fileName'; + } + + @override + String failedToLoadImage(String error) { + return 'Bild konnte nicht geladen werden: $error'; + } + + @override + String get invalidDataUrl => 'Ungültiges Data-URL-Format'; + + @override + String get failedToDecodeImage => 'Bild konnte nicht decodiert werden'; + + @override + String get invalidImageFormat => 'Ungültiges Bildformat'; + + @override + String get emptyImageData => 'Leere Bilddaten'; + + @override + String get offlineBanner => 'Du bist offline. Einige Funktionen sind eingeschränkt.'; + + @override + String get featureRequiresInternet => 'Diese Funktion erfordert eine Internetverbindung'; + + @override + String get messagesWillSendWhenOnline => 'Nachrichten werden gesendet, sobald du wieder online bist'; + + @override + String get confirm => 'Bestätigen'; + + @override + String get cancel => 'Abbrechen'; + + @override + String get ok => 'OK'; + + @override + String get inputField => 'Eingabefeld'; + + @override + String get captureDocumentOrImage => 'Dokument oder Bild aufnehmen'; + + @override + String get checkConnection => 'Verbindung prüfen'; + + @override + String get openSettings => 'Einstellungen öffnen'; + + @override + String get chooseDifferentFile => 'Andere Datei wählen'; + + @override + String get goBack => 'Zurück'; + + @override + String get technicalDetails => 'Technische Details'; +} diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart new file mode 100644 index 0000000..8058663 --- /dev/null +++ b/lib/l10n/app_localizations_en.dart @@ -0,0 +1,444 @@ +// 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 => 'Conduit'; + + @override + String get initializationFailed => 'Initialization Failed'; + + @override + String get retry => 'Retry'; + + @override + String get back => 'Back'; + + @override + String get you => 'You'; + + @override + String get loadingProfile => 'Loading profile...'; + + @override + String get unableToLoadProfile => 'Unable to load profile'; + + @override + String get pleaseCheckConnection => 'Please check your connection and try again'; + + @override + String get account => 'Account'; + + @override + String get signOut => 'Sign Out'; + + @override + String get endYourSession => 'End your session'; + + @override + String get defaultModel => 'Default Model'; + + @override + String get autoSelect => 'Auto-select'; + + @override + String get loadingModels => 'Loading models...'; + + @override + String get failedToLoadModels => 'Failed to load models'; + + @override + String get availableModels => 'Available Models'; + + @override + String get noResults => 'No results'; + + @override + String get searchModels => 'Search models...'; + + @override + String get errorMessage => 'Something went wrong. Please try again.'; + + @override + String get loginButton => 'Login'; + + @override + String get menuItem => 'Settings'; + + @override + String dynamicContentWithPlaceholder(String name) { + return 'Welcome, $name!'; + } + + @override + String itemsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items', + one: '1 item', + zero: 'No items', + ); + return '$_temp0'; + } + + @override + String get closeButtonSemantic => 'Close'; + + @override + String get loadingContent => 'Loading content'; + + @override + String get noItems => 'No items'; + + @override + String get noItemsToDisplay => 'No items to display'; + + @override + String get loadMore => 'Load More'; + + @override + String get workspace => 'Workspace'; + + @override + String get recentFiles => 'Recent Files'; + + @override + String get knowledgeBase => 'Knowledge Base'; + + @override + String get noFilesYet => 'No files yet'; + + @override + String get uploadDocsPrompt => 'Upload documents to reference in your conversations with Conduit'; + + @override + String get uploadFirstFile => 'Upload your first file'; + + @override + String get knowledgeBaseEmpty => 'Knowledge base is empty'; + + @override + String get createCollectionsPrompt => 'Create collections of related documents for easy reference'; + + @override + String get chooseSourcePhoto => 'Choose your source'; + + @override + String get takePhoto => 'Take a photo'; + + @override + String get chooseFromGallery => 'Choose from your photos'; + + @override + String get document => 'Document'; + + @override + String get documentHint => 'PDF, Word, or text file'; + + @override + String get uploadFileTitle => 'Upload File'; + + @override + String fileUploadComingSoon(String type) { + return 'File upload for $type is coming soon!'; + } + + @override + String get kbCreationComingSoon => 'Knowledge base creation is coming soon!'; + + @override + String get backToServerSetup => 'Back to server setup'; + + @override + String get connectedToServer => 'Connected to Server'; + + @override + String get signIn => 'Sign In'; + + @override + String get enterCredentials => 'Enter your credentials to access your AI conversations'; + + @override + String get credentials => 'Credentials'; + + @override + String get apiKey => 'API Key'; + + @override + String get usernameOrEmail => 'Username or Email'; + + @override + String get password => 'Password'; + + @override + String get signInWithApiKey => 'Sign in with API Key'; + + @override + String get connectToServer => 'Connect to Server'; + + @override + String get enterServerAddress => 'Enter your Open-WebUI server address to get started'; + + @override + String get serverUrl => 'Server URL'; + + @override + String get serverUrlHint => 'https://your-server.com'; + + @override + String get enterServerUrlSemantic => 'Enter your server URL or IP address'; + + @override + String get headerName => 'Header Name'; + + @override + String get headerValue => 'Header Value'; + + @override + String get headerValueHint => 'api-key-123 or Bearer token'; + + @override + String get addHeader => 'Add header'; + + @override + String get maximumHeadersReached => 'Maximum headers reached'; + + @override + String get removeHeader => 'Remove header'; + + @override + String get connecting => 'Connecting...'; + + @override + String get connectToServerButton => 'Connect to Server'; + + @override + String get demoModeActive => 'Demo Mode Active'; + + @override + String get skipServerSetupTryDemo => 'Skip server setup and try the demo'; + + @override + String get enterDemo => 'Enter Demo'; + + @override + String get demoBadge => 'Demo'; + + @override + String get serverNotOpenWebUI => 'This does not appear to be an Open-WebUI server.'; + + @override + String get serverUrlEmpty => 'Server URL cannot be empty'; + + @override + String get invalidUrlFormat => 'Invalid URL format. Please check your input.'; + + @override + String get onlyHttpHttps => 'Only HTTP and HTTPS protocols are supported.'; + + @override + String get serverAddressRequired => 'Server address is required (e.g., 192.168.1.10 or example.com).'; + + @override + String get portRange => 'Port must be between 1 and 65535.'; + + @override + String get invalidIpFormat => 'Invalid IP address format. Use format like 192.168.1.10.'; + + @override + String get couldNotConnectGeneric => 'Couldn\'t connect. Double-check the address and try again.'; + + @override + String get weCouldntReachServer => 'We couldn\'t reach the server. Check your connection and that the server is running.'; + + @override + String get connectionTimedOut => 'Connection timed out. The server might be busy or blocked by a firewall.'; + + @override + String get useHttpOrHttpsOnly => 'Use http:// or https:// only.'; + + @override + String get loginFailed => 'Login failed'; + + @override + String get invalidCredentials => 'Invalid username or password. Please try again.'; + + @override + String get serverRedirectingHttps => 'The server is redirecting requests. Check your server\'s HTTPS configuration.'; + + @override + String get unableToConnectServer => 'Unable to connect to server. Please check your connection.'; + + @override + String get requestTimedOut => 'The request timed out. Please try again.'; + + @override + String get genericSignInFailed => 'We couldn\'t sign you in. Check your credentials and server settings.'; + + @override + String get skip => 'Skip'; + + @override + String get next => 'Next'; + + @override + String get done => 'Done'; + + @override + String get onboardStartTitle => 'Start a conversation'; + + @override + String get onboardStartSubtitle => 'Choose a model, then type below to begin. Tap New Chat anytime.'; + + @override + String get onboardStartBullet1 => 'Tap the model name in the top bar to switch models'; + + @override + String get onboardStartBullet2 => 'Use New Chat to reset context'; + + @override + String get onboardAttachTitle => 'Attach context'; + + @override + String get onboardAttachSubtitle => 'Ground responses by adding files or images.'; + + @override + String get onboardAttachBullet1 => 'Files: PDFs, docs, datasets'; + + @override + String get onboardAttachBullet2 => 'Images: photos or screenshots'; + + @override + String get onboardSpeakTitle => 'Speak naturally'; + + @override + String get onboardSpeakSubtitle => 'Tap the mic to dictate with live waveform feedback.'; + + @override + String get onboardSpeakBullet1 => 'Stop anytime; partial text is preserved'; + + @override + String get onboardSpeakBullet2 => 'Great for quick notes or long prompts'; + + @override + String get onboardQuickTitle => 'Quick actions'; + + @override + String get onboardQuickSubtitle => 'Use the top‑left menu to open the chats list and navigation.'; + + @override + String get onboardQuickBullet1 => 'Tap the menu to open the chats list and navigation'; + + @override + String get onboardQuickBullet2 => 'Jump instantly to New Chat, Files, or Profile'; + + @override + String get addAttachment => 'Add attachment'; + + @override + String get tools => 'Tools'; + + @override + String get voiceInput => 'Voice input'; + + @override + String get messageInputLabel => 'Message input'; + + @override + String get messageInputHint => 'Type your message'; + + @override + String get messageHintText => 'Message...'; + + @override + String get stopGenerating => 'Stop generating'; + + @override + String get send => 'Send'; + + @override + String get sendMessage => 'Send message'; + + @override + String get file => 'File'; + + @override + String get photo => 'Photo'; + + @override + String get camera => 'Camera'; + + @override + String get apiUnavailable => 'API service not available'; + + @override + String get unableToLoadImage => 'Unable to load image'; + + @override + String notAnImageFile(String fileName) { + return 'Not an image file: $fileName'; + } + + @override + String failedToLoadImage(String error) { + return 'Failed to load image: $error'; + } + + @override + String get invalidDataUrl => 'Invalid data URL format'; + + @override + String get failedToDecodeImage => 'Failed to decode image'; + + @override + String get invalidImageFormat => 'Invalid image format'; + + @override + String get emptyImageData => 'Empty image data'; + + @override + String get offlineBanner => 'You\'re offline. Some features may be limited.'; + + @override + String get featureRequiresInternet => 'This feature requires an internet connection'; + + @override + String get messagesWillSendWhenOnline => 'Messages will be sent when you\'re back online'; + + @override + String get confirm => 'Confirm'; + + @override + String get cancel => 'Cancel'; + + @override + String get ok => 'OK'; + + @override + String get inputField => 'Input field'; + + @override + String get captureDocumentOrImage => 'Capture a document or image'; + + @override + String get checkConnection => 'Check Connection'; + + @override + String get openSettings => 'Open Settings'; + + @override + String get chooseDifferentFile => 'Choose Different File'; + + @override + String get goBack => 'Go Back'; + + @override + String get technicalDetails => 'Technical Details'; +} diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart new file mode 100644 index 0000000..5a46386 --- /dev/null +++ b/lib/l10n/app_localizations_fr.dart @@ -0,0 +1,444 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for French (`fr`). +class AppLocalizationsFr extends AppLocalizations { + AppLocalizationsFr([String locale = 'fr']) : super(locale); + + @override + String get appTitle => 'Conduit'; + + @override + String get initializationFailed => 'Échec de l\'initialisation'; + + @override + String get retry => 'Réessayer'; + + @override + String get back => 'Retour'; + + @override + String get you => 'Vous'; + + @override + String get loadingProfile => 'Chargement du profil...'; + + @override + String get unableToLoadProfile => 'Impossible de charger le profil'; + + @override + String get pleaseCheckConnection => 'Veuillez vérifier votre connexion et réessayer'; + + @override + String get account => 'Compte'; + + @override + String get signOut => 'Se déconnecter'; + + @override + String get endYourSession => 'Terminer votre session'; + + @override + String get defaultModel => 'Modèle par défaut'; + + @override + String get autoSelect => 'Sélection automatique'; + + @override + String get loadingModels => 'Chargement des modèles...'; + + @override + String get failedToLoadModels => 'Échec du chargement des modèles'; + + @override + String get availableModels => 'Modèles disponibles'; + + @override + String get noResults => 'Aucun résultat'; + + @override + String get searchModels => 'Rechercher des modèles...'; + + @override + String get errorMessage => 'Une erreur s\'est produite. Veuillez réessayer.'; + + @override + String get loginButton => 'Connexion'; + + @override + String get menuItem => 'Paramètres'; + + @override + String dynamicContentWithPlaceholder(String name) { + return 'Bienvenue, $name !'; + } + + @override + String itemsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count éléments', + one: '1 élément', + zero: 'Aucun élément', + ); + return '$_temp0'; + } + + @override + String get closeButtonSemantic => 'Fermer'; + + @override + String get loadingContent => 'Chargement du contenu'; + + @override + String get noItems => 'Aucun élément'; + + @override + String get noItemsToDisplay => 'Aucun élément à afficher'; + + @override + String get loadMore => 'Charger plus'; + + @override + String get workspace => 'Espace de travail'; + + @override + String get recentFiles => 'Fichiers récents'; + + @override + String get knowledgeBase => 'Base de connaissances'; + + @override + String get noFilesYet => 'Pas encore de fichiers'; + + @override + String get uploadDocsPrompt => 'Importez des documents à utiliser dans vos conversations avec Conduit'; + + @override + String get uploadFirstFile => 'Importer votre premier fichier'; + + @override + String get knowledgeBaseEmpty => 'La base de connaissances est vide'; + + @override + String get createCollectionsPrompt => 'Créez des collections de documents liés pour une référence facile'; + + @override + String get chooseSourcePhoto => 'Choisir la source'; + + @override + String get takePhoto => 'Prendre une photo'; + + @override + String get chooseFromGallery => 'Choisir depuis vos photos'; + + @override + String get document => 'Document'; + + @override + String get documentHint => 'Fichier PDF, Word ou texte'; + + @override + String get uploadFileTitle => 'Importer un fichier'; + + @override + String fileUploadComingSoon(String type) { + return 'Le téléversement de fichiers pour $type arrive bientôt !'; + } + + @override + String get kbCreationComingSoon => 'La création de la base de connaissances arrive bientôt !'; + + @override + String get backToServerSetup => 'Retour à la configuration du serveur'; + + @override + String get connectedToServer => 'Connecté au serveur'; + + @override + String get signIn => 'Se connecter'; + + @override + String get enterCredentials => 'Entrez vos identifiants pour accéder à vos conversations IA'; + + @override + String get credentials => 'Identifiants'; + + @override + String get apiKey => 'Clé API'; + + @override + String get usernameOrEmail => 'Nom d\'utilisateur ou e‑mail'; + + @override + String get password => 'Mot de passe'; + + @override + String get signInWithApiKey => 'Se connecter avec une clé API'; + + @override + String get connectToServer => 'Se connecter au serveur'; + + @override + String get enterServerAddress => 'Saisissez l\'adresse de votre serveur Open-WebUI pour commencer'; + + @override + String get serverUrl => 'URL du serveur'; + + @override + String get serverUrlHint => 'https://votre-serveur.com'; + + @override + String get enterServerUrlSemantic => 'Saisissez l\'URL ou l\'adresse IP de votre serveur'; + + @override + String get headerName => 'Nom de l\'en-tête'; + + @override + String get headerValue => 'Valeur de l\'en-tête'; + + @override + String get headerValueHint => 'api-key-123 ou jeton Bearer'; + + @override + String get addHeader => 'Ajouter l\'en-tête'; + + @override + String get maximumHeadersReached => 'Nombre maximal atteint'; + + @override + String get removeHeader => 'Supprimer l\'en-tête'; + + @override + String get connecting => 'Connexion en cours...'; + + @override + String get connectToServerButton => 'Se connecter au serveur'; + + @override + String get demoModeActive => 'Mode démo activé'; + + @override + String get skipServerSetupTryDemo => 'Ignorer la configuration et essayer la démo'; + + @override + String get enterDemo => 'Entrer en démo'; + + @override + String get demoBadge => 'Démo'; + + @override + String get serverNotOpenWebUI => 'Ceci ne semble pas être un serveur Open-WebUI.'; + + @override + String get serverUrlEmpty => 'L\'URL du serveur ne peut pas être vide'; + + @override + String get invalidUrlFormat => 'Format d\'URL invalide. Veuillez vérifier votre saisie.'; + + @override + String get onlyHttpHttps => 'Seuls les protocoles HTTP et HTTPS sont pris en charge.'; + + @override + String get serverAddressRequired => 'Adresse du serveur requise (ex. 192.168.1.10 ou example.com).'; + + @override + String get portRange => 'Le port doit être compris entre 1 et 65535.'; + + @override + String get invalidIpFormat => 'Format d\'IP invalide. Exemple : 192.168.1.10.'; + + @override + String get couldNotConnectGeneric => 'Connexion impossible. Vérifiez l\'adresse et réessayez.'; + + @override + String get weCouldntReachServer => 'Impossible d\'atteindre le serveur. Vérifiez la connexion et l\'état du serveur.'; + + @override + String get connectionTimedOut => 'Délai d\'attente dépassé. Le serveur est peut-être occupé ou bloqué.'; + + @override + String get useHttpOrHttpsOnly => 'Utilisez uniquement http:// ou https://.'; + + @override + String get loginFailed => 'Échec de la connexion'; + + @override + String get invalidCredentials => 'Nom d\'utilisateur ou mot de passe invalide. Réessayez.'; + + @override + String get serverRedirectingHttps => 'Le serveur redirige les requêtes. Vérifiez la configuration HTTPS.'; + + @override + String get unableToConnectServer => 'Impossible de se connecter au serveur. Vérifiez votre connexion.'; + + @override + String get requestTimedOut => 'Délai d\'attente dépassé. Réessayez.'; + + @override + String get genericSignInFailed => 'Connexion impossible. Vérifiez vos identifiants et le serveur.'; + + @override + String get skip => 'Ignorer'; + + @override + String get next => 'Suivant'; + + @override + String get done => 'Terminé'; + + @override + String get onboardStartTitle => 'Commencer une conversation'; + + @override + String get onboardStartSubtitle => 'Choisissez un modèle puis commencez à écrire. Touchez Nouveau chat à tout moment.'; + + @override + String get onboardStartBullet1 => 'Touchez le nom du modèle en haut pour changer'; + + @override + String get onboardStartBullet2 => 'Utilisez Nouveau chat pour réinitialiser le contexte'; + + @override + String get onboardAttachTitle => 'Ajouter du contexte'; + + @override + String get onboardAttachSubtitle => 'Améliorez les réponses en ajoutant des fichiers ou des images.'; + + @override + String get onboardAttachBullet1 => 'Fichiers : PDF, documents, jeux de données'; + + @override + String get onboardAttachBullet2 => 'Images : photos ou captures d\'écran'; + + @override + String get onboardSpeakTitle => 'Parlez naturellement'; + + @override + String get onboardSpeakSubtitle => 'Touchez le micro pour dicter avec retour visuel.'; + + @override + String get onboardSpeakBullet1 => 'Arrêtez à tout moment ; le texte partiel est conservé'; + + @override + String get onboardSpeakBullet2 => 'Idéal pour des notes rapides ou de longs prompts'; + + @override + String get onboardQuickTitle => 'Actions rapides'; + + @override + String get onboardQuickSubtitle => 'Utilisez le menu en haut à gauche pour ouvrir la liste des chats et la navigation.'; + + @override + String get onboardQuickBullet1 => 'Touchez le menu pour ouvrir les chats et la navigation'; + + @override + String get onboardQuickBullet2 => 'Accédez rapidement à Nouveau chat, Fichiers ou Profil'; + + @override + String get addAttachment => 'Ajouter une pièce jointe'; + + @override + String get tools => 'Outils'; + + @override + String get voiceInput => 'Entrée vocale'; + + @override + String get messageInputLabel => 'Saisie du message'; + + @override + String get messageInputHint => 'Saisissez votre message'; + + @override + String get messageHintText => 'Message...'; + + @override + String get stopGenerating => 'Arrêter la génération'; + + @override + String get send => 'Envoyer'; + + @override + String get sendMessage => 'Envoyer le message'; + + @override + String get file => 'Fichier'; + + @override + String get photo => 'Photo'; + + @override + String get camera => 'Appareil photo'; + + @override + String get apiUnavailable => 'Service API indisponible'; + + @override + String get unableToLoadImage => 'Impossible de charger l\'image'; + + @override + String notAnImageFile(String fileName) { + return 'Ce n\'est pas un fichier image : $fileName'; + } + + @override + String failedToLoadImage(String error) { + return 'Échec du chargement de l\'image : $error'; + } + + @override + String get invalidDataUrl => 'Format d\'URL de données invalide'; + + @override + String get failedToDecodeImage => 'Échec du décodage de l\'image'; + + @override + String get invalidImageFormat => 'Format d\'image invalide'; + + @override + String get emptyImageData => 'Données d\'image vides'; + + @override + String get offlineBanner => 'Vous êtes hors ligne. Certaines fonctions peuvent être limitées.'; + + @override + String get featureRequiresInternet => 'Cette fonctionnalité nécessite une connexion Internet'; + + @override + String get messagesWillSendWhenOnline => 'Les messages seront envoyés lorsque vous serez de nouveau en ligne'; + + @override + String get confirm => 'Confirmer'; + + @override + String get cancel => 'Annuler'; + + @override + String get ok => 'OK'; + + @override + String get inputField => 'Champ de saisie'; + + @override + String get captureDocumentOrImage => 'Capturer un document ou une image'; + + @override + String get checkConnection => 'Vérifier la connexion'; + + @override + String get openSettings => 'Ouvrir les réglages'; + + @override + String get chooseDifferentFile => 'Choisir un autre fichier'; + + @override + String get goBack => 'Retour'; + + @override + String get technicalDetails => 'Détails techniques'; +} diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart new file mode 100644 index 0000000..d5fcc83 --- /dev/null +++ b/lib/l10n/app_localizations_it.dart @@ -0,0 +1,444 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Italian (`it`). +class AppLocalizationsIt extends AppLocalizations { + AppLocalizationsIt([String locale = 'it']) : super(locale); + + @override + String get appTitle => 'Conduit'; + + @override + String get initializationFailed => 'Inizializzazione non riuscita'; + + @override + String get retry => 'Riprova'; + + @override + String get back => 'Indietro'; + + @override + String get you => 'Tu'; + + @override + String get loadingProfile => 'Caricamento profilo...'; + + @override + String get unableToLoadProfile => 'Impossibile caricare il profilo'; + + @override + String get pleaseCheckConnection => 'Controlla la connessione e riprova'; + + @override + String get account => 'Account'; + + @override + String get signOut => 'Esci'; + + @override + String get endYourSession => 'Termina la sessione'; + + @override + String get defaultModel => 'Modello predefinito'; + + @override + String get autoSelect => 'Selezione automatica'; + + @override + String get loadingModels => 'Caricamento modelli...'; + + @override + String get failedToLoadModels => 'Impossibile caricare i modelli'; + + @override + String get availableModels => 'Modelli disponibili'; + + @override + String get noResults => 'Nessun risultato'; + + @override + String get searchModels => 'Cerca modelli...'; + + @override + String get errorMessage => 'Qualcosa è andato storto. Riprova.'; + + @override + String get loginButton => 'Accedi'; + + @override + String get menuItem => 'Impostazioni'; + + @override + String dynamicContentWithPlaceholder(String name) { + return 'Benvenuto, $name!'; + } + + @override + String itemsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count elementi', + one: '1 elemento', + zero: 'Nessun elemento', + ); + return '$_temp0'; + } + + @override + String get closeButtonSemantic => 'Chiudi'; + + @override + String get loadingContent => 'Caricamento contenuto'; + + @override + String get noItems => 'Nessun elemento'; + + @override + String get noItemsToDisplay => 'Nessun elemento da visualizzare'; + + @override + String get loadMore => 'Carica altro'; + + @override + String get workspace => 'Spazio di lavoro'; + + @override + String get recentFiles => 'File recenti'; + + @override + String get knowledgeBase => 'Base di conoscenza'; + + @override + String get noFilesYet => 'Ancora nessun file'; + + @override + String get uploadDocsPrompt => 'Carica documenti da usare nelle conversazioni con Conduit'; + + @override + String get uploadFirstFile => 'Carica il tuo primo file'; + + @override + String get knowledgeBaseEmpty => 'La base di conoscenza è vuota'; + + @override + String get createCollectionsPrompt => 'Crea raccolte di documenti correlati per un rapido riferimento'; + + @override + String get chooseSourcePhoto => 'Scegli origine'; + + @override + String get takePhoto => 'Scatta una foto'; + + @override + String get chooseFromGallery => 'Scegli dalle foto'; + + @override + String get document => 'Documento'; + + @override + String get documentHint => 'File PDF, Word o di testo'; + + @override + String get uploadFileTitle => 'Carica file'; + + @override + String fileUploadComingSoon(String type) { + return 'Il caricamento file per $type arriverà presto!'; + } + + @override + String get kbCreationComingSoon => 'La creazione della base di conoscenza arriverà presto!'; + + @override + String get backToServerSetup => 'Torna alla configurazione del server'; + + @override + String get connectedToServer => 'Connesso al server'; + + @override + String get signIn => 'Accedi'; + + @override + String get enterCredentials => 'Inserisci le credenziali per accedere alle conversazioni IA'; + + @override + String get credentials => 'Credenziali'; + + @override + String get apiKey => 'Chiave API'; + + @override + String get usernameOrEmail => 'Username o e‑mail'; + + @override + String get password => 'Password'; + + @override + String get signInWithApiKey => 'Accedi con chiave API'; + + @override + String get connectToServer => 'Connetti al server'; + + @override + String get enterServerAddress => 'Inserisci l\'indirizzo del server Open-WebUI per iniziare'; + + @override + String get serverUrl => 'URL del server'; + + @override + String get serverUrlHint => 'https://tuo-server.com'; + + @override + String get enterServerUrlSemantic => 'Inserisci l\'URL o l\'indirizzo IP del server'; + + @override + String get headerName => 'Nome header'; + + @override + String get headerValue => 'Valore header'; + + @override + String get headerValueHint => 'api-key-123 o token Bearer'; + + @override + String get addHeader => 'Aggiungi header'; + + @override + String get maximumHeadersReached => 'Numero massimo raggiunto'; + + @override + String get removeHeader => 'Rimuovi header'; + + @override + String get connecting => 'Connessione in corso...'; + + @override + String get connectToServerButton => 'Connetti al server'; + + @override + String get demoModeActive => 'Modalità demo attiva'; + + @override + String get skipServerSetupTryDemo => 'Salta configurazione server e prova la demo'; + + @override + String get enterDemo => 'Entra in demo'; + + @override + String get demoBadge => 'Demo'; + + @override + String get serverNotOpenWebUI => 'Questo non sembra un server Open-WebUI.'; + + @override + String get serverUrlEmpty => 'L\'URL del server non può essere vuoto'; + + @override + String get invalidUrlFormat => 'Formato URL non valido. Controlla l\'input.'; + + @override + String get onlyHttpHttps => 'Sono supportati solo i protocolli HTTP e HTTPS.'; + + @override + String get serverAddressRequired => 'Indirizzo server richiesto (es. 192.168.1.10 o example.com).'; + + @override + String get portRange => 'La porta deve essere tra 1 e 65535.'; + + @override + String get invalidIpFormat => 'Formato IP non valido. Esempio: 192.168.1.10.'; + + @override + String get couldNotConnectGeneric => 'Impossibile connettersi. Verifica l\'indirizzo e riprova.'; + + @override + String get weCouldntReachServer => 'Impossibile raggiungere il server. Verifica connessione e stato del server.'; + + @override + String get connectionTimedOut => 'Tempo scaduto. Il server potrebbe essere occupato o bloccato.'; + + @override + String get useHttpOrHttpsOnly => 'Usa solo http:// o https://.'; + + @override + String get loginFailed => 'Accesso non riuscito'; + + @override + String get invalidCredentials => 'Nome utente o password non validi. Riprova.'; + + @override + String get serverRedirectingHttps => 'Il server sta reindirizzando. Controlla la configurazione HTTPS.'; + + @override + String get unableToConnectServer => 'Impossibile connettersi al server. Controlla la connessione.'; + + @override + String get requestTimedOut => 'Richiesta scaduta. Riprova.'; + + @override + String get genericSignInFailed => 'Impossibile accedere. Controlla credenziali e server.'; + + @override + String get skip => 'Salta'; + + @override + String get next => 'Avanti'; + + @override + String get done => 'Fatto'; + + @override + String get onboardStartTitle => 'Inizia una conversazione'; + + @override + String get onboardStartSubtitle => 'Scegli un modello e inizia a scrivere. Tocca Nuova chat in qualsiasi momento.'; + + @override + String get onboardStartBullet1 => 'Tocca il nome del modello in alto per cambiare'; + + @override + String get onboardStartBullet2 => 'Usa Nuova chat per azzerare il contesto'; + + @override + String get onboardAttachTitle => 'Aggiungi contesto'; + + @override + String get onboardAttachSubtitle => 'Migliora le risposte aggiungendo file o immagini.'; + + @override + String get onboardAttachBullet1 => 'File: PDF, documenti, dataset'; + + @override + String get onboardAttachBullet2 => 'Immagini: foto o screenshot'; + + @override + String get onboardSpeakTitle => 'Parla in modo naturale'; + + @override + String get onboardSpeakSubtitle => 'Tocca il microfono per dettare con feedback visivo.'; + + @override + String get onboardSpeakBullet1 => 'Interrompi in qualsiasi momento; il testo parziale viene mantenuto'; + + @override + String get onboardSpeakBullet2 => 'Ottimo per note rapide o prompt lunghi'; + + @override + String get onboardQuickTitle => 'Azioni rapide'; + + @override + String get onboardQuickSubtitle => 'Usa il menu in alto a sinistra per aprire l\'elenco chat e la navigazione.'; + + @override + String get onboardQuickBullet1 => 'Tocca il menu per aprire chat e navigazione'; + + @override + String get onboardQuickBullet2 => 'Vai subito a Nuova chat, File o Profilo'; + + @override + String get addAttachment => 'Aggiungi allegato'; + + @override + String get tools => 'Strumenti'; + + @override + String get voiceInput => 'Input vocale'; + + @override + String get messageInputLabel => 'Input messaggio'; + + @override + String get messageInputHint => 'Scrivi il tuo messaggio'; + + @override + String get messageHintText => 'Messaggio...'; + + @override + String get stopGenerating => 'Interrompi generazione'; + + @override + String get send => 'Invia'; + + @override + String get sendMessage => 'Invia messaggio'; + + @override + String get file => 'File'; + + @override + String get photo => 'Foto'; + + @override + String get camera => 'Fotocamera'; + + @override + String get apiUnavailable => 'Servizio API non disponibile'; + + @override + String get unableToLoadImage => 'Impossibile caricare l\'immagine'; + + @override + String notAnImageFile(String fileName) { + return 'Non è un file immagine: $fileName'; + } + + @override + String failedToLoadImage(String error) { + return 'Impossibile caricare l\'immagine: $error'; + } + + @override + String get invalidDataUrl => 'Formato data URL non valido'; + + @override + String get failedToDecodeImage => 'Impossibile decodificare l\'immagine'; + + @override + String get invalidImageFormat => 'Formato immagine non valido'; + + @override + String get emptyImageData => 'Dati immagine vuoti'; + + @override + String get offlineBanner => 'Sei offline. Alcune funzioni potrebbero essere limitate.'; + + @override + String get featureRequiresInternet => 'Questa funzione richiede una connessione Internet'; + + @override + String get messagesWillSendWhenOnline => 'I messaggi verranno inviati quando tornerai online'; + + @override + String get confirm => 'Conferma'; + + @override + String get cancel => 'Annulla'; + + @override + String get ok => 'OK'; + + @override + String get inputField => 'Campo di input'; + + @override + String get captureDocumentOrImage => 'Acquisisci un documento o un\'immagine'; + + @override + String get checkConnection => 'Controlla connessione'; + + @override + String get openSettings => 'Apri impostazioni'; + + @override + String get chooseDifferentFile => 'Scegli un altro file'; + + @override + String get goBack => 'Indietro'; + + @override + String get technicalDetails => 'Dettagli tecnici'; +} diff --git a/lib/main.dart b/lib/main.dart index 3877183..175db81 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,6 +15,8 @@ import 'core/auth/auth_state_manager.dart'; import 'core/utils/debug_logger.dart'; import 'features/onboarding/views/onboarding_sheet.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:conduit/l10n/app_localizations.dart'; import 'features/chat/views/chat_page.dart'; import 'features/navigation/views/splash_launcher_page.dart'; @@ -85,17 +87,40 @@ class _ConduitAppState extends ConsumerState { ? AppTheme.conduitDarkTheme : AppTheme.conduitLightTheme; + final locale = ref.watch(localeProvider); + return AnimatedThemeWrapper( theme: currentTheme, duration: AnimationDuration.medium, child: ErrorBoundary( child: MaterialApp( - title: 'Conduit', + onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle, theme: AppTheme.conduitLightTheme, darkTheme: AppTheme.conduitDarkTheme, themeMode: themeMode, debugShowCheckedModeBanner: false, navigatorKey: NavigationService.navigatorKey, + locale: locale, + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [ + Locale('en'), + Locale('de'), + Locale('fr'), + Locale('it'), + ], + localeResolutionCallback: (deviceLocale, supported) { + if (locale != null) return locale; // User override wins + if (deviceLocale == null) return const Locale('en'); + for (final loc in supported) { + if (loc.languageCode == deviceLocale.languageCode) return loc; + } + return const Locale('en'); + }, builder: (context, child) { // Keep a subtle fade for navigation transitions only final wrapped = OfflineIndicator( @@ -159,7 +184,7 @@ class _ConduitAppState extends ConsumerState { if (authNavState == AuthNavigationState.error) { return _buildErrorState( - ref.watch(authErrorProvider3) ?? 'Authentication error', + ref.watch(authErrorProvider3) ?? AppLocalizations.of(context)!.errorMessage, ); } @@ -174,7 +199,7 @@ class _ConduitAppState extends ConsumerState { loading: () => _buildInitialLoadingSkeleton(context), error: (error, stackTrace) { DebugLogger.error('Server provider error', error); - return _buildErrorState('Server connection failed: $error'); + return _buildErrorState(AppLocalizations.of(context)!.unableToConnectServer); }, ); }, @@ -268,7 +293,7 @@ class _ConduitAppState extends ConsumerState { ), const SizedBox(height: Spacing.md), Text( - 'Initialization Failed', + AppLocalizations.of(context)!.initializationFailed, style: TextStyle( fontSize: AppTypography.headlineLarge, fontWeight: FontWeight.bold, @@ -291,7 +316,7 @@ class _ConduitAppState extends ConsumerState { backgroundColor: context.conduitTheme.buttonPrimary, foregroundColor: context.conduitTheme.buttonPrimaryText, ), - child: const Text('Retry'), + child: Text(AppLocalizations.of(context)!.retry), ), ], ), diff --git a/lib/shared/utils/ui_utils.dart b/lib/shared/utils/ui_utils.dart index 33ba3ed..e2bd11d 100644 --- a/lib/shared/utils/ui_utils.dart +++ b/lib/shared/utils/ui_utils.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'dart:io' show Platform; import '../theme/theme_extensions.dart'; +import 'package:conduit/l10n/app_localizations.dart'; /// Utility functions for common UI patterns and helpers /// Following Conduit design principles @@ -104,7 +105,7 @@ class UiUtils { behavior: SnackBarBehavior.floating, action: onRetry != null ? SnackBarAction( - label: 'Try again', + label: AppLocalizations.of(context)!.retry, textColor: context.conduitTheme.textInverse, onPressed: onRetry, ) @@ -138,7 +139,11 @@ class UiUtils { actions: [ TextButton( onPressed: () => Navigator.pop(context, false), - child: Text(cancelText), + child: Text( + cancelText.isNotEmpty + ? cancelText + : AppLocalizations.of(context)!.cancel, + ), ), TextButton( onPressed: () => Navigator.pop(context, true), @@ -147,7 +152,11 @@ class UiUtils { foregroundColor: context.conduitTheme.error, ) : null, - child: Text(confirmText), + child: Text( + confirmText.isNotEmpty + ? confirmText + : AppLocalizations.of(context)!.confirm, + ), ), ], ), diff --git a/lib/shared/widgets/conduit_components.dart b/lib/shared/widgets/conduit_components.dart index f0e1d19..66ffbdb 100644 --- a/lib/shared/widgets/conduit_components.dart +++ b/lib/shared/widgets/conduit_components.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../theme/theme_extensions.dart'; import '../services/brand_service.dart'; import '../../core/services/enhanced_accessibility_service.dart'; +import 'package:conduit/l10n/app_localizations.dart'; import '../../core/services/platform_service.dart'; import '../../core/services/settings_service.dart'; @@ -53,7 +54,8 @@ class ConduitButton extends ConsumerWidget { // Build semantic label String semanticLabel = text; if (isLoading) { - semanticLabel = 'Loading: $text'; + final l10n = AppLocalizations.of(context); + semanticLabel = '${l10n?.loadingContent ?? 'Loading'}: $text'; } else if (isDestructive) { semanticLabel = 'Warning: $text'; } @@ -99,7 +101,7 @@ class ConduitButton extends ConsumerWidget { ), child: isLoading ? Semantics( - label: 'Loading', + label: AppLocalizations.of(context)?.loadingContent ?? 'Loading', excludeSemantics: true, child: SizedBox( width: IconSize.small, @@ -204,7 +206,7 @@ class ConduitInput extends StatelessWidget { SizedBox(height: Spacing.sm), ], Semantics( - label: semanticLabel ?? label ?? 'Input field', + label: semanticLabel ?? label ?? (AppLocalizations.of(context)?.inputField ?? 'Input field'), textField: true, child: TextField( controller: controller, @@ -772,7 +774,7 @@ class AccessibleFormField extends StatelessWidget { SizedBox(height: isCompact ? Spacing.xs : Spacing.sm), ], Semantics( - label: semanticLabel ?? label ?? 'Input field', + label: semanticLabel ?? label ?? (AppLocalizations.of(context)?.inputField ?? 'Input field'), textField: true, child: TextFormField( controller: controller, diff --git a/lib/shared/widgets/improved_loading_states.dart b/lib/shared/widgets/improved_loading_states.dart index 824301e..2aebdd6 100644 --- a/lib/shared/widgets/improved_loading_states.dart +++ b/lib/shared/widgets/improved_loading_states.dart @@ -4,6 +4,7 @@ import 'package:flutter_animate/flutter_animate.dart'; import 'skeleton_loader.dart'; import '../theme/theme_extensions.dart'; import 'conduit_components.dart'; +import 'package:conduit/l10n/app_localizations.dart'; /// Improved loading state widget with accessibility and better hierarchy class ImprovedLoadingState extends StatefulWidget { @@ -81,7 +82,7 @@ class _ImprovedLoadingStateState extends State opacity: _fadeAnimation, child: Center( child: Semantics( - label: widget.message ?? 'Loading content', + label: widget.message ?? AppLocalizations.of(context)!.loadingContent, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -414,7 +415,7 @@ class LoadingCard extends StatelessWidget { return ConduitCard( isCompact: isCompact, child: ImprovedLoadingState( - message: 'Loading...', + message: AppLocalizations.of(context)!.loadingContent, isCompact: isCompact, ), ); @@ -620,7 +621,7 @@ class ErrorStateWidget extends StatelessWidget { Icon(Icons.error_outline, size: 64, color: theme.colorScheme.error), const SizedBox(height: 16), Text( - 'Oops! Something went wrong', + AppLocalizations.of(context)!.errorMessage, style: theme.textTheme.headlineSmall, textAlign: TextAlign.center, ), @@ -655,7 +656,7 @@ class ErrorStateWidget extends StatelessWidget { FilledButton.icon( onPressed: onRetry, icon: const Icon(Icons.refresh), - label: const Text('Try Again'), + label: Text(AppLocalizations.of(context)!.retry), ), ], ], diff --git a/lib/shared/widgets/loading_states.dart b/lib/shared/widgets/loading_states.dart index 5cafad0..88d8631 100644 --- a/lib/shared/widgets/loading_states.dart +++ b/lib/shared/widgets/loading_states.dart @@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'dart:io' show Platform; import '../services/brand_service.dart'; import '../theme/app_theme.dart'; +import 'package:conduit/l10n/app_localizations.dart'; /// Standard loading indicators following Conduit design patterns class ConduitLoading { @@ -332,8 +333,8 @@ class LoadingStateWrapper extends StatelessWidget { return asyncValue.when( data: builder, loading: () => showLoadingOverlay - ? ConduitLoading.overlay(message: 'Loading...') - : loadingWidget ?? ConduitLoading.primary(message: 'Loading...'), + ? ConduitLoading.overlay(message: AppLocalizations.of(context)!.loadingContent) + : loadingWidget ?? ConduitLoading.primary(message: AppLocalizations.of(context)!.loadingContent), error: (error, stackTrace) { if (errorBuilder != null) { return errorBuilder!(error, stackTrace); @@ -352,7 +353,7 @@ class LoadingStateWrapper extends StatelessWidget { ), const SizedBox(height: Spacing.md), Text( - 'Something went wrong', + AppLocalizations.of(context)!.errorMessage, style: TextStyle( color: context.conduitTheme.textSecondary, fontSize: AppTypography.headlineSmall, diff --git a/lib/shared/widgets/markdown/markdown_config.dart b/lib/shared/widgets/markdown/markdown_config.dart index ae28422..47eb7fc 100644 --- a/lib/shared/widgets/markdown/markdown_config.dart +++ b/lib/shared/widgets/markdown/markdown_config.dart @@ -6,6 +6,7 @@ import 'package:flutter_highlight/themes/atom-one-light.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:conduit/shared/theme/theme_extensions.dart'; +import 'package:conduit/l10n/app_localizations.dart'; class ConduitMarkdownConfig { static MarkdownConfig getConfig({ @@ -56,7 +57,7 @@ class ConduitMarkdownConfig { builder: (url, attributes) { // Check if it's a base64 data URL if (url.startsWith('data:')) { - return _buildBase64Image(url, theme); + return _buildBase64Image(url, context, theme); } // Network image return CachedNetworkImage( @@ -94,7 +95,7 @@ class ConduitMarkdownConfig { ), const SizedBox(height: Spacing.xs), Text( - 'Failed to load image', + AppLocalizations.of(context)!.failedToLoadImage(''), style: TextStyle(color: theme.error, fontSize: 12), ), ], @@ -151,7 +152,7 @@ class ConduitMarkdownConfig { ); } - static Widget _buildBase64Image(String dataUrl, ConduitThemeExtension theme) { + static Widget _buildBase64Image(String dataUrl, BuildContext context, ConduitThemeExtension theme) { try { // Extract base64 part from data URL final commaIndex = dataUrl.indexOf(','); @@ -187,7 +188,7 @@ class ConduitMarkdownConfig { Icon(Icons.error_outline, color: theme.error, size: 32), const SizedBox(height: Spacing.xs), Text( - 'Invalid image data', + AppLocalizations.of(context)!.invalidImageFormat, style: TextStyle(color: theme.error, fontSize: 12), ), ], @@ -206,7 +207,7 @@ class ConduitMarkdownConfig { ), child: Center( child: Text( - 'Invalid image format', + AppLocalizations.of(context)!.invalidImageFormat, style: TextStyle(color: theme.error, fontSize: 12), ), ), diff --git a/lib/shared/widgets/offline_indicator.dart b/lib/shared/widgets/offline_indicator.dart index d8e3d0b..68742cb 100644 --- a/lib/shared/widgets/offline_indicator.dart +++ b/lib/shared/widgets/offline_indicator.dart @@ -5,6 +5,7 @@ import 'package:flutter_animate/flutter_animate.dart'; import 'dart:io' show Platform; import '../../core/services/connectivity_service.dart'; import '../theme/theme_extensions.dart'; +import 'package:conduit/l10n/app_localizations.dart'; class OfflineIndicator extends ConsumerWidget { final Widget child; @@ -70,7 +71,7 @@ class _OfflineBanner extends StatelessWidget { const SizedBox(width: Spacing.xs), Expanded( child: Text( - 'You\'re offline. Some features may be limited.', + AppLocalizations.of(context)!.offlineBanner, style: TextStyle( color: context.conduitTheme.textInverse, fontSize: AppTypography.labelLarge, @@ -101,7 +102,7 @@ class InlineOfflineIndicator extends ConsumerWidget { const InlineOfflineIndicator({ super.key, - this.message = 'This feature requires an internet connection', + this.message = '', this.icon, this.backgroundColor, }); @@ -138,7 +139,9 @@ class InlineOfflineIndicator extends ConsumerWidget { const SizedBox(width: Spacing.xs), Expanded( child: Text( - message, + message.isNotEmpty + ? message + : AppLocalizations.of(context)!.featureRequiresInternet, style: TextStyle( color: context.conduitTheme.warning, fontSize: AppTypography.labelLarge, @@ -174,7 +177,7 @@ class OfflineAwareButton extends ConsumerWidget { return Tooltip( message: !enabled - ? (offlineTooltip ?? 'This action requires an internet connection') + ? (offlineTooltip ?? AppLocalizations.of(context)!.featureRequiresInternet) : '', child: FilledButton(onPressed: enabled ? onPressed : null, child: child), ); @@ -217,7 +220,7 @@ class ChatOfflineOverlay extends ConsumerWidget { ), const SizedBox(width: Spacing.sm), Text( - 'Messages will be sent when you\'re back online', + AppLocalizations.of(context)!.messagesWillSendWhenOnline, style: TextStyle( color: context.conduitTheme.warning, fontSize: AppTypography.bodySmall, diff --git a/lib/shared/widgets/optimized_list.dart b/lib/shared/widgets/optimized_list.dart index 4afe19d..7a6c13d 100644 --- a/lib/shared/widgets/optimized_list.dart +++ b/lib/shared/widgets/optimized_list.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'skeleton_loader.dart'; +import 'package:conduit/l10n/app_localizations.dart'; import 'improved_loading_states.dart'; /// Optimized list widget with virtualization and performance enhancements @@ -127,8 +128,8 @@ class _OptimizedListState extends ConsumerState> { if (widget.items.isEmpty) { return widget.emptyWidget ?? ImprovedEmptyState( - title: 'No items', - subtitle: widget.emptyMessage ?? 'No items to display', + title: AppLocalizations.of(context)!.noItems, + subtitle: widget.emptyMessage ?? AppLocalizations.of(context)!.noItemsToDisplay, icon: Icons.inbox_outlined, ); } @@ -208,7 +209,10 @@ class _OptimizedListState extends ConsumerState> { alignment: Alignment.center, child: _isLoadingMore ? const CircularProgressIndicator() - : TextButton(onPressed: _loadMore, child: const Text('Load More')), + : TextButton( + onPressed: _loadMore, + child: Text(AppLocalizations.of(context)!.loadMore), + ), ); } @@ -267,11 +271,14 @@ class OptimizedSliverList extends ConsumerWidget { return SliverToBoxAdapter( child: emptyWidget ?? - ImprovedEmptyState( - title: 'No items', - subtitle: emptyMessage ?? 'No items to display', - icon: Icons.inbox_outlined, - ), + Builder(builder: (context) { + final l10n = AppLocalizations.of(context)!; + return ImprovedEmptyState( + title: l10n.noItems, + subtitle: emptyMessage ?? l10n.noItemsToDisplay, + icon: Icons.inbox_outlined, + ); + }), ); } diff --git a/pubspec.lock b/pubspec.lock index 45f6c42..84aab48 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -366,6 +366,11 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_native_splash: dependency: "direct dev" description: @@ -616,6 +621,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.1+1" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" io: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index cfc1305..aca5bfe 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,6 +9,8 @@ environment: dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter # State Management flutter_riverpod: ^2.6.1 @@ -45,6 +47,7 @@ dependencies: crypto: ^3.0.3 package_info_plus: ^8.0.2 url_launcher: ^6.3.0 + intl: ^0.20.2 # Icons & Theming cupertino_icons: ^1.0.8 @@ -69,6 +72,22 @@ dependency_overrides: flutter: uses-material-design: true + generate: true + # Localization configuration + # The default synthetic package is used for generated localizations: + # import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + # ARB files live in lib/l10n + # You can customize output via the `l10n:` section if needed. + # + # l10n: + # arb-dir: lib/l10n + # template-arb-file: app_en.arb + # output-localization-file: app_localizations.dart + # preferred-supported-locales: + # - en + # - de + # - fr + # - it assets: - assets/icons/