feat: localisation with en, de, fr and it

This commit is contained in:
cogwheel0
2025-08-23 20:09:43 +05:30
parent b898adbe40
commit a852ce7848
36 changed files with 3912 additions and 203 deletions

1
.gitignore vendored
View File

@@ -11,7 +11,6 @@
.svn/
.swiftpm/
migrate_working_dir/
AGENTS.md
# IntelliJ related
*.iml

23
AGENTS.md Normal file
View File

@@ -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

View File

@@ -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.
### InApp 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 hardcoded strings and replace with `AppLocalizations`.
- iOS strings not changing:
- Restart the app after changing system language or use the inapp 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:

View File

@@ -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'),
),
],
],

View File

@@ -79,6 +79,36 @@ class ThemeModeNotifier extends StateNotifier<ThemeMode> {
}
}
// Locale provider
final localeProvider = StateNotifierProvider<LocaleNotifier, Locale?>(
(ref) {
final storage = ref.watch(optimizedStorageServiceProvider);
return LocaleNotifier(storage);
},
);
class LocaleNotifier extends StateNotifier<Locale?> {
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<void> setLocale(Locale? locale) async {
state = locale;
await _storage.setLocaleCode(locale?.languageCode);
}
}
// Server connection providers - optimized with caching
final serverConfigsProvider = FutureProvider<List<ServerConfig>>((ref) async {
final storage = ref.watch(optimizedStorageServiceProvider);

View File

@@ -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<void> setLocaleCode(String? code) async {
if (code == null || code.isEmpty) {
await _prefs.remove(_localeCodeKey);
} else {
await _prefs.setString(_localeCodeKey, code);
}
}
/// Onboarding
Future<bool> getOnboardingSeen() async {
return _prefs.getBool(_onboardingSeenKey) ?? false;

View File

@@ -182,12 +182,12 @@ class UserFriendlyErrorHandler {
List<ErrorRecoveryAction> _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<ErrorRecoveryAction> _getGenericRecoveryActions() {
return [
ErrorRecoveryAction(
label: 'Try Again',
label: 'Retry',
action: ErrorActionType.retry,
description: 'Retry the operation',
),

View File

@@ -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<ErrorBoundary> {
),
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<ErrorBoundary> {
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),
),
],
],

View File

@@ -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<AuthenticationPage> {
}
Future<void> _signIn() async {
final l10n = AppLocalizations.of(context)!;
if (!_formKey.currentState!.validate()) return;
setState(() {
@@ -87,7 +89,7 @@ class _AuthenticationPageState extends ConsumerState<AuthenticationPage> {
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<AuthenticationPage> {
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<AuthenticationPage> {
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<AuthenticationPage> {
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<AuthenticationPage> {
),
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<AuthenticationPage> {
),
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<AuthenticationPage> {
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<AuthenticationPage> {
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<AuthenticationPage> {
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<AuthenticationPage> {
(value) => InputValidationService.validateMinLength(
value,
10,
fieldName: 'API Key',
fieldName: AppLocalizations.of(context)!.apiKey,
),
]),
obscureText: _obscurePassword,
@@ -513,7 +515,7 @@ class _AuthenticationPageState extends ConsumerState<AuthenticationPage> {
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<AuthenticationPage> {
),
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<AuthenticationPage> {
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

View File

@@ -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,9 +27,9 @@ 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,
),
),
),

View File

@@ -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<ServerConnectionPage> {
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<ServerConnectionPage> {
// 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<ServerConnectionPage> {
// 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<ServerConnectionPage> {
} 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<ServerConnectionPage> {
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<ServerConnectionPage> {
),
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<ServerConnectionPage> {
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<ServerConnectionPage> {
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<ServerConnectionPage> {
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<ServerConnectionPage> {
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<ServerConnectionPage> {
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),

View File

@@ -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<AssistantMessageWidget>
icon: Platform.isIOS
? CupertinoIcons.arrow_clockwise
: Icons.refresh,
label: 'Retry',
label: AppLocalizations.of(context)!.retry,
onTap: widget.onRegenerate,
),
] else ...[

View File

@@ -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<void> _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<void> _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);

View File

@@ -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<ModernChatInput>
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<ModernChatInput>
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<ModernChatInput>
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<ModernChatInput>
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<ModernChatInput>
_showUnifiedToolsModal();
}
: null,
tooltip: 'Tools',
tooltip: AppLocalizations.of(context)!.tools,
isActive:
ref
.watch(selectedToolIdsProvider)
@@ -391,7 +392,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
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<ModernChatInput>
// 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<ModernChatInput>
// 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<ModernChatInput>
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<ModernChatInput>
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<ModernChatInput>
icon: Platform.isIOS
? CupertinoIcons.camera
: Icons.camera_alt,
label: 'Camera',
label: AppLocalizations.of(context)!.camera,
onTap: () {
HapticFeedback.lightImpact();
Navigator.pop(context); // Close modal

View File

@@ -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<WorkspacePage>
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<WorkspacePage>
Expanded(
child: _buildTabButton(
index: 0,
label: 'Recent Files',
label: AppLocalizations.of(context)!.recentFiles,
isSelected: _selectedTab == 0,
),
),
@@ -170,7 +171,7 @@ class _WorkspacePageState extends ConsumerState<WorkspacePage>
Expanded(
child: _buildTabButton(
index: 1,
label: 'Knowledge Base',
label: AppLocalizations.of(context)!.knowledgeBase,
isSelected: _selectedTab == 1,
),
),
@@ -229,11 +230,10 @@ class _WorkspacePageState extends ConsumerState<WorkspacePage>
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<WorkspacePage>
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<WorkspacePage>
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<WorkspacePage>
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<WorkspacePage>
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<WorkspacePage>
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<WorkspacePage>
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,
);
}
}

View File

@@ -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<OnboardingSheet> {
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 topleft 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<OnboardingSheet> {
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<OnboardingSheet> {
),
),
),
child: Text(_index == _pages.length - 1 ? 'Done' : 'Next'),
child: Text(
_index == _pages.length - 1
? AppLocalizations.of(context)!.done
: AppLocalizations.of(context)!.next,
),
),
],
),

View File

@@ -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<String?> _showLanguageSelector(BuildContext context, String current) {
return showModalBottomSheet<String>(
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,

145
lib/l10n/app_de.arb Normal file
View File

@@ -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 EMail",
"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"
}

169
lib/l10n/app_en.arb Normal file
View File

@@ -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 topleft 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"
}

145
lib/l10n/app_fr.arb Normal file
View File

@@ -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 email",
"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"
}

145
lib/l10n/app_it.arb Normal file
View File

@@ -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 email",
"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"
}

View File

@@ -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, youll need to edit this
/// file.
///
/// First, open your projects ios/Runner.xcworkspace Xcode workspace file.
/// Then, in the Project Navigator, open the Info.plist file under the Runner
/// projects 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<AppLocalizations>(context, AppLocalizations);
}
static const LocalizationsDelegate<AppLocalizations> 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<LocalizationsDelegate<dynamic>> localizationsDelegates = <LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[
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 topleft 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<AppLocalizations> {
const _AppLocalizationsDelegate();
@override
Future<AppLocalizations> load(Locale locale) {
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
}
@override
bool isSupported(Locale locale) => <String>['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.'
);
}

View File

@@ -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 EMail';
@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';
}

View File

@@ -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 topleft 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';
}

View File

@@ -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 email';
@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';
}

View File

@@ -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 email';
@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';
}

View File

@@ -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<ConduitApp> {
? 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<ConduitApp> {
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<ConduitApp> {
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<ConduitApp> {
),
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<ConduitApp> {
backgroundColor: context.conduitTheme.buttonPrimary,
foregroundColor: context.conduitTheme.buttonPrimaryText,
),
child: const Text('Retry'),
child: Text(AppLocalizations.of(context)!.retry),
),
],
),

View File

@@ -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,
),
),
],
),

View File

@@ -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,

View File

@@ -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<ImprovedLoadingState>
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),
),
],
],

View File

@@ -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<T> 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<T> extends StatelessWidget {
),
const SizedBox(height: Spacing.md),
Text(
'Something went wrong',
AppLocalizations.of(context)!.errorMessage,
style: TextStyle(
color: context.conduitTheme.textSecondary,
fontSize: AppTypography.headlineSmall,

View File

@@ -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),
),
),

View File

@@ -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,

View File

@@ -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<T> extends ConsumerState<OptimizedList<T>> {
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<T> extends ConsumerState<OptimizedList<T>> {
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<T> 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,
);
}),
);
}

View File

@@ -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:

View File

@@ -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/