feat: localisation with en, de, fr and it
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../core/widgets/error_boundary.dart';
|
||||
import '../../../shared/theme/theme_extensions.dart';
|
||||
import '../../../shared/widgets/conduit_components.dart';
|
||||
import 'package:conduit/l10n/app_localizations.dart';
|
||||
import 'server_connection_page.dart';
|
||||
|
||||
/// Entry point for the connection and sign-in flow
|
||||
@@ -26,12 +27,12 @@ class ConnectAndSignInPage extends ConsumerWidget {
|
||||
return ErrorBoundary(
|
||||
child: Scaffold(
|
||||
backgroundColor: context.conduitTheme.surfaceBackground,
|
||||
body: const Center(
|
||||
body: Center(
|
||||
child: ConduitLoadingIndicator(
|
||||
message: 'Loading...',
|
||||
message: AppLocalizations.of(context)!.loadingContent,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
@@ -866,4 +869,4 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
|
||||
});
|
||||
HapticFeedback.lightImpact();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user