feat(i18n): localize onboarding & reports; logout

- add en/ms keys for welcome, onboarding, capture/review, errors, and
  settings labels
- replace hardcoded strings with I18n.t in WelcomeScreen, OnboardingFlow,
  CaptureScreen, ReviewScreen, Map severity legend, and Settings
- update StartRouter: onboarded guests go to MainScreen; non-guest users
  see SignIn; skip marks user as onboarded guest
- add logout action in Settings that clears onboarding/user mode prefs
  and returns to the welcome flow
- refine settings diagnostics and app section; move labels to i18n
- leave TODOs for app subtitle, processing status, and ReportCard menu
  labels for future i18n coverage
This commit is contained in:
2025-09-26 14:57:47 +08:00
parent 7085a54290
commit 9849df6a8c
8 changed files with 288 additions and 80 deletions

View File

@@ -97,5 +97,60 @@
"auth.title": "Sign in", "auth.title": "Sign in",
"auth.signInWithApple": "Sign in with Apple", "auth.signInWithApple": "Sign in with Apple",
"auth.signInWithGoogle": "Sign in with Google", "auth.signInWithGoogle": "Sign in with Google",
"auth.comingSoon": "Coming soon" "auth.comingSoon": "Coming soon",
"welcome.title": "Spot it. Snap it. Fix it.",
"welcome.subtitle": "Report city issues in seconds with AI-powered detection. Help create safer, better communities together.",
"cta.continueGuest": "Continue as Guest",
"cta.signIn": "Sign In",
"cta.skip": "Skip for now",
"onboarding.header": "Welcome to FixMate",
"onboarding.title1": "Fast Issue Reporting",
"onboarding.subtitle1": "AI-Powered Detection",
"onboarding.body1": "Simply take a photo of any urban issue - our AI automatically identifies and categorizes the problem in seconds.",
"onboarding.title2": "Smart City Mapping",
"onboarding.subtitle2": "Real-Time Visualization",
"onboarding.body2": "View all reported issues on an interactive map with intelligent clustering and filtering options.",
"onboarding.title3": "Track Progress",
"onboarding.subtitle3": "Stay Informed",
"onboarding.body3": "Follow the status of your reports from submission to resolution. Help make your community better.",
"onboarding.skip": "Skip",
"onboarding.next": "Next",
"onboarding.getStarted": "Get Started",
"review.title": "Review & Submit",
"review.submit": "Submit",
"review.aiAnalysis": "AI Analysis Complete",
"review.aiConfidence": "Smart detection with {0}% confidence",
"review.useSuggestion": "Use AI Suggestion",
"review.editManually": "Edit Manually",
"review.category": "Issue Category",
"review.severity": "Severity Level",
"review.notes": "Additional Notes",
"review.notesHint": "Add any additional details or context...",
"review.location": "Location Details",
"review.coordinates": "Coordinates",
"review.accuracy": "Accuracy: {0}m",
"error.saving": "Error saving report: {0}",
"error.imagePick": "Error picking image: {0}",
"error.location": "Unable to get location. Please try again.",
"error.imageProcessing": "Error processing image: {0}",
"capture.title": "Report Issue",
"capture.subtitle": "Report City Issues",
"capture.description": "Take a photo of any urban maintenance issue like potholes, broken streetlights, or damaged signage.",
"capture.processing": "Processing image...",
"capture.takePhoto": "Take Photo",
"capture.gallery": "Choose from Gallery",
"report.viewDetails": "View Details",
"report.updateStatus": "Update Status",
"report.delete": "Delete",
"report.comingSoon": "Coming Soon!",
"settings.loading": "Loading...",
"settings.app": "App",
"settings.version": "v1.0.0",
"settings.account": "Account",
"settings.account.guest": "Guest Mode",
"btn.logout": "Logout",
"confirm.logout.title": "Logout?",
"confirm.logout.message": "You will be returned to the welcome screen.",
"label.createdAt": "Created At",
"error.clearData": "Failed to clear data"
} }

View File

@@ -97,5 +97,60 @@
"auth.title": "Log masuk", "auth.title": "Log masuk",
"auth.signInWithApple": "Log masuk dengan Apple", "auth.signInWithApple": "Log masuk dengan Apple",
"auth.signInWithGoogle": "Log masuk dengan Google", "auth.signInWithGoogle": "Log masuk dengan Google",
"auth.comingSoon": "Akan datang" "auth.comingSoon": "Akan datang",
"welcome.title": "Nampak. Tangkap. Baiki.",
"welcome.subtitle": "Lapor isu bandar dalam beberapa saat dengan pengesanan berkuasa AI. Bantu mencipta komuniti yang lebih selamat dan lebih baik.",
"cta.continueGuest": "Teruskan sebagai Tetamu",
"cta.signIn": "Log Masuk",
"cta.skip": "Langkau buat masa ini",
"onboarding.header": "Selamat Datang ke FixMate",
"onboarding.title1": "Laporan Isu Pantas",
"onboarding.subtitle1": "Pengesanan Berkuasa AI",
"onboarding.body1": "Hanya ambil gambar mana-mana isu bandar - AI kami secara automatik mengenal pasti dan mengkategorikan masalah dalam beberapa saat.",
"onboarding.title2": "Pemetaan Bandar Pintar",
"onboarding.subtitle2": "Penglihatan Masa Nyata",
"onboarding.body2": "Lihat semua isu yang dilaporkan di peta interaktif dengan pengelompokan dan penapisan pintar.",
"onboarding.title3": "Jejak Kemajuan",
"onboarding.subtitle3": "Kekal Maklum",
"onboarding.body3": "Ikuti status laporan anda dari penyerahan hingga penyelesaian. Bantu memperbaiki komuniti anda.",
"onboarding.skip": "Langkau",
"onboarding.next": "Seterusnya",
"onboarding.getStarted": "Mula",
"review.title": "Semak & Hantar",
"review.submit": "Hantar",
"review.aiAnalysis": "Analisis AI Selesai",
"review.aiConfidence": "Pengesanan pintar dengan keyakinan {0}%",
"review.useSuggestion": "Guna Cadangan AI",
"review.editManually": "Edit Secara Manual",
"review.category": "Kategori Isu",
"review.severity": "Tahap Keterukan",
"review.notes": "Nota Tambahan",
"review.notesHint": "Tambah sebarang butiran atau konteks tambahan...",
"review.location": "Butiran Lokasi",
"review.coordinates": "Koordinat",
"review.accuracy": "Ketepatan: {0}m",
"error.saving": "Ralat menyimpan laporan: {0}",
"error.imagePick": "Ralat memilih imej: {0}",
"error.location": "Tidak dapat mendapatkan lokasi. Sila cuba lagi.",
"error.imageProcessing": "Ralat memproses imej: {0}",
"capture.title": "Lapor Isu",
"capture.subtitle": "Lapor Isu Bandar",
"capture.description": "Ambil gambar mana-mana isu penyelenggaraan bandar seperti lubang jalan, lampu jalan rosak, atau papan tanda rosak.",
"capture.processing": "Memproses imej...",
"capture.takePhoto": "Ambil Gambar",
"capture.gallery": "Pilih dari Galeri",
"report.viewDetails": "Lihat Butiran",
"report.updateStatus": "Kemas Kini Status",
"report.delete": "Padam",
"report.comingSoon": "Akan Datang!",
"settings.loading": "Memuatkan...",
"settings.app": "Apl",
"settings.version": "v1.0.0",
"settings.account": "Akaun",
"settings.account.guest": "Mod Tetamu",
"btn.logout": "Log Keluar",
"confirm.logout.title": "Log keluar?",
"confirm.logout.message": "Anda akan dibawa kembali ke skrin selamat datang.",
"label.createdAt": "Dicipta Pada",
"error.clearData": "Gagal mengosongkan data"
} }

View File

@@ -281,10 +281,6 @@ class _StartRouterState extends State<StartRouter> {
if (_loading) { if (_loading) {
screen = const Scaffold(body: Center(child: CircularProgressIndicator())); screen = const Scaffold(body: Center(child: CircularProgressIndicator()));
screenKey = 'loading'; screenKey = 'loading';
} else if (_userMode == 'guest') {
// If user is known to be in guest mode, take them to sign-in / sign-up first
screen = const SignInScreen();
screenKey = 'signin';
} else if (!_onboarded) { } else if (!_onboarded) {
screen = WelcomeScreen( screen = WelcomeScreen(
onContinue: () async { onContinue: () async {
@@ -305,14 +301,19 @@ class _StartRouterState extends State<StartRouter> {
); );
}, },
onSkip: () async { onSkip: () async {
// Mark as onboarded and guest so next app start opens sign-up/sign-in // Mark as onboarded and guest so next app start goes to main app
await _setOnboarded(asGuest: true); await _setOnboarded(asGuest: true);
}, },
); );
screenKey = 'welcome'; screenKey = 'welcome';
} else { } else if (_userMode == 'guest') {
// User is onboarded and in guest mode, take them to main app
screen = const MainScreen(); screen = const MainScreen();
screenKey = 'main'; screenKey = 'main';
} else {
// User is onboarded but not in guest mode, show sign-in screen
screen = const SignInScreen();
screenKey = 'signin';
} }
return AnimatedSwitcher( return AnimatedSwitcher(
@@ -395,7 +396,7 @@ class WelcomeScreen extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'FixMate', I18n.t('app.name'),
style: Theme.of(context).textTheme.headlineSmall style: Theme.of(context).textTheme.headlineSmall
?.copyWith( ?.copyWith(
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
@@ -403,7 +404,7 @@ class WelcomeScreen extends StatelessWidget {
), ),
), ),
Text( Text(
'Civic Solutions', 'Civic Solutions', // TODO: Add to i18n as app.subtitle
style: Theme.of(context).textTheme.bodySmall style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: cs.onSurface.withOpacity(0.7)), ?.copyWith(color: cs.onSurface.withOpacity(0.7)),
), ),
@@ -445,7 +446,7 @@ class WelcomeScreen extends StatelessWidget {
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
Text( Text(
'Spot it. Snap it. Fix it.', I18n.t('welcome.title'),
style: Theme.of(context).textTheme.headlineSmall style: Theme.of(context).textTheme.headlineSmall
?.copyWith( ?.copyWith(
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
@@ -455,7 +456,7 @@ class WelcomeScreen extends StatelessWidget {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
'Report city issues in seconds with AI-powered detection. Help create safer, better communities together.', I18n.t('welcome.subtitle'),
style: Theme.of(context).textTheme.bodyLarge?.copyWith( style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: cs.onSurface.withOpacity(0.8), color: cs.onSurface.withOpacity(0.8),
height: 1.5, height: 1.5,
@@ -473,7 +474,7 @@ class WelcomeScreen extends StatelessWidget {
onPressed: onContinue, onPressed: onContinue,
icon: const Icon(Icons.arrow_forward, size: 20), icon: const Icon(Icons.arrow_forward, size: 20),
label: Text( label: Text(
'Continue as Guest', I18n.t('cta.continueGuest'),
style: const TextStyle(fontSize: 16), style: const TextStyle(fontSize: 16),
), ),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@@ -493,7 +494,7 @@ class WelcomeScreen extends StatelessWidget {
onPressed: onSignIn, onPressed: onSignIn,
icon: const Icon(Icons.login, size: 20), icon: const Icon(Icons.login, size: 20),
label: Text( label: Text(
'Sign In', I18n.t('cta.signIn'),
style: const TextStyle(fontSize: 16), style: const TextStyle(fontSize: 16),
), ),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
@@ -509,7 +510,7 @@ class WelcomeScreen extends StatelessWidget {
TextButton( TextButton(
onPressed: onSkip, onPressed: onSkip,
child: Text( child: Text(
'Skip for now', I18n.t('cta.skip'),
style: TextStyle( style: TextStyle(
color: cs.onSurface.withOpacity(0.7), color: cs.onSurface.withOpacity(0.7),
fontSize: 16, fontSize: 16,
@@ -646,7 +647,7 @@ class _OnboardingFlowState extends State<OnboardingFlow> {
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Text( Text(
'Welcome to FixMate', I18n.t('onboarding.header'),
style: Theme.of(context).textTheme.titleLarge?.copyWith( style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: cs.onSurface, color: cs.onSurface,
@@ -662,28 +663,25 @@ class _OnboardingFlowState extends State<OnboardingFlow> {
onPageChanged: (i) => setState(() => _index = i), onPageChanged: (i) => setState(() => _index = i),
children: [ children: [
_buildPage( _buildPage(
title: 'Fast Issue Reporting', title: I18n.t('onboarding.title1'),
subtitle: 'AI-Powered Detection', subtitle: I18n.t('onboarding.subtitle1'),
description: description: I18n.t('onboarding.body1'),
'Simply take a photo of any urban issue - our AI automatically identifies and categorizes the problem in seconds.',
icon: Icons.camera_alt, icon: Icons.camera_alt,
gradientStart: const Color(0xFF22C55E), gradientStart: const Color(0xFF22C55E),
gradientEnd: const Color(0xFF4ADE80), gradientEnd: const Color(0xFF4ADE80),
), ),
_buildPage( _buildPage(
title: 'Smart City Mapping', title: I18n.t('onboarding.title2'),
subtitle: 'Real-Time Visualization', subtitle: I18n.t('onboarding.subtitle2'),
description: description: I18n.t('onboarding.body2'),
'View all reported issues on an interactive map with intelligent clustering and filtering options.',
icon: Icons.map, icon: Icons.map,
gradientStart: const Color(0xFF2563EB), gradientStart: const Color(0xFF2563EB),
gradientEnd: const Color(0xFF3B82F6), gradientEnd: const Color(0xFF3B82F6),
), ),
_buildPage( _buildPage(
title: 'Track Progress', title: I18n.t('onboarding.title3'),
subtitle: 'Stay Informed', subtitle: I18n.t('onboarding.subtitle3'),
description: description: I18n.t('onboarding.body3'),
'Follow the status of your reports from submission to resolution. Help make your community better.',
icon: Icons.check_circle, icon: Icons.check_circle,
gradientStart: const Color(0xFFF97316), gradientStart: const Color(0xFFF97316),
gradientEnd: const Color(0xFFFB923C), gradientEnd: const Color(0xFFFB923C),
@@ -717,7 +715,7 @@ class _OnboardingFlowState extends State<OnboardingFlow> {
TextButton( TextButton(
onPressed: () => Navigator.pop(context, true), onPressed: () => Navigator.pop(context, true),
child: Text( child: Text(
'Skip', I18n.t('onboarding.skip'),
style: TextStyle( style: TextStyle(
color: cs.onSurface.withOpacity(0.7), color: cs.onSurface.withOpacity(0.7),
fontSize: 16, fontSize: 16,
@@ -739,7 +737,9 @@ class _OnboardingFlowState extends State<OnboardingFlow> {
), ),
), ),
child: Text( child: Text(
_index < 2 ? 'Next' : 'Get Started', _index < 2
? I18n.t('onboarding.next')
: I18n.t('onboarding.getStarted'),
style: const TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,

View File

@@ -679,7 +679,7 @@ class _MapScreenState extends State<MapScreen> {
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
'Issue Severity', I18n.t('label.severity'),
style: Theme.of(context).textTheme.bodySmall style: Theme.of(context).textTheme.bodySmall
?.copyWith( ?.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -691,11 +691,20 @@ class _MapScreenState extends State<MapScreen> {
], ],
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_enhancedLegendItem(Severity.high, 'High Priority'), _enhancedLegendItem(
Severity.high,
'${I18n.t('severity.high')} Priority',
),
const SizedBox(height: 8), const SizedBox(height: 8),
_enhancedLegendItem(Severity.medium, 'Medium Priority'), _enhancedLegendItem(
Severity.medium,
'${I18n.t('severity.medium')} Priority',
),
const SizedBox(height: 8), const SizedBox(height: 8),
_enhancedLegendItem(Severity.low, 'Low Priority'), _enhancedLegendItem(
Severity.low,
'${I18n.t('severity.low')} Priority',
),
], ],
), ),
), ),

View File

@@ -37,9 +37,11 @@ class _CaptureScreenState extends State<CaptureScreen> {
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of( ScaffoldMessenger.of(context).showSnackBar(
context, SnackBar(
).showSnackBar(SnackBar(content: Text('Error picking image: $e'))); content: Text(I18n.t('error.imagePick', {'0': e.toString()})),
),
);
} }
} finally { } finally {
if (mounted) { if (mounted) {
@@ -58,8 +60,10 @@ class _CaptureScreenState extends State<CaptureScreen> {
if (position == null) { if (position == null) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('Unable to get location. Please try again.'), content: const Text(
'Unable to get location. Please try again.',
), // TODO: Move to i18n
), ),
); );
} }
@@ -110,9 +114,11 @@ class _CaptureScreenState extends State<CaptureScreen> {
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of( ScaffoldMessenger.of(context).showSnackBar(
context, SnackBar(
).showSnackBar(SnackBar(content: Text('Error processing image: $e'))); content: Text(I18n.t('error.imageProcessing', {'0': e.toString()})),
),
);
} }
} }
} }
@@ -127,7 +133,7 @@ class _CaptureScreenState extends State<CaptureScreen> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
'Report Issue', I18n.t('capture.title'),
style: const TextStyle(fontWeight: FontWeight.w600), style: const TextStyle(fontWeight: FontWeight.w600),
), ),
elevation: 0, elevation: 0,
@@ -181,7 +187,7 @@ class _CaptureScreenState extends State<CaptureScreen> {
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
Text( Text(
'Report City Issues', I18n.t('capture.subtitle'),
style: Theme.of(context).textTheme.headlineSmall style: Theme.of(context).textTheme.headlineSmall
?.copyWith( ?.copyWith(
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
@@ -191,7 +197,7 @@ class _CaptureScreenState extends State<CaptureScreen> {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'Take a photo of any urban maintenance issue like potholes, broken streetlights, or damaged signage.', I18n.t('capture.description'),
style: Theme.of(context).textTheme.bodyLarge?.copyWith( style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: cs.onSurface.withOpacity(0.7), color: cs.onSurface.withOpacity(0.7),
height: 1.5, height: 1.5,
@@ -221,7 +227,7 @@ class _CaptureScreenState extends State<CaptureScreen> {
CircularProgressIndicator(), CircularProgressIndicator(),
SizedBox(height: 16), SizedBox(height: 16),
Text( Text(
'Processing image...', 'Processing image...', // TODO: Move to i18n
style: TextStyle(fontSize: 16), style: TextStyle(fontSize: 16),
), ),
], ],
@@ -249,7 +255,7 @@ class _CaptureScreenState extends State<CaptureScreen> {
onPressed: () => _pickImage(ImageSource.camera), onPressed: () => _pickImage(ImageSource.camera),
icon: const Icon(Icons.camera_alt, size: 24), icon: const Icon(Icons.camera_alt, size: 24),
label: const Text( label: const Text(
'Take Photo', 'Take Photo', // TODO: Move to i18n
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -274,7 +280,7 @@ class _CaptureScreenState extends State<CaptureScreen> {
onPressed: () => _pickImage(ImageSource.gallery), onPressed: () => _pickImage(ImageSource.gallery),
icon: const Icon(Icons.photo_library, size: 24), icon: const Icon(Icons.photo_library, size: 24),
label: const Text( label: const Text(
'Choose from Gallery', 'Choose from Gallery', // TODO: Move to i18n
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,

View File

@@ -66,9 +66,9 @@ class _ReviewScreenState extends State<ReviewScreen> {
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of( ScaffoldMessenger.of(context).showSnackBar(
context, SnackBar(content: Text(I18n.t('error.saving', {'0': e.toString()}))),
).showSnackBar(SnackBar(content: Text('Error saving report: $e'))); );
} }
} finally { } finally {
if (mounted) { if (mounted) {
@@ -86,7 +86,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
'Review & Submit', I18n.t('review.title'),
style: const TextStyle(fontWeight: FontWeight.w600), style: const TextStyle(fontWeight: FontWeight.w600),
), ),
elevation: 0, elevation: 0,
@@ -102,7 +102,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
child: CircularProgressIndicator(strokeWidth: 2), child: CircularProgressIndicator(strokeWidth: 2),
) )
: Text( : Text(
'Submit', I18n.t('review.submit'),
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -190,7 +190,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'AI Analysis Complete', I18n.t('review.aiAnalysis'),
style: Theme.of(context).textTheme.titleMedium style: Theme.of(context).textTheme.titleMedium
?.copyWith( ?.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -198,7 +198,13 @@ class _ReviewScreenState extends State<ReviewScreen> {
), ),
), ),
Text( Text(
'Smart detection with ${(widget.report.aiSuggestion.confidence * 100).round()}% confidence', I18n.t('review.aiConfidence', {
'0':
(widget.report.aiSuggestion.confidence *
100)
.round()
.toString(),
}),
style: Theme.of(context).textTheme.bodySmall style: Theme.of(context).textTheme.bodySmall
?.copyWith( ?.copyWith(
color: cs.onSurface.withOpacity(0.7), color: cs.onSurface.withOpacity(0.7),
@@ -300,7 +306,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
}); });
}, },
icon: const Icon(Icons.check_circle, size: 18), icon: const Icon(Icons.check_circle, size: 18),
label: const Text('Use AI Suggestion'), label: Text(I18n.t('review.useSuggestion')),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0EA5E9), backgroundColor: const Color(0xFF0EA5E9),
foregroundColor: Colors.white, foregroundColor: Colors.white,
@@ -316,7 +322,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
child: OutlinedButton.icon( child: OutlinedButton.icon(
onPressed: () {}, onPressed: () {},
icon: const Icon(Icons.edit, size: 18), icon: const Icon(Icons.edit, size: 18),
label: const Text('Edit Manually'), label: Text(I18n.t('review.editManually')),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
side: BorderSide( side: BorderSide(
color: const Color(0xFF0EA5E9), color: const Color(0xFF0EA5E9),
@@ -359,7 +365,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
Icon(Icons.category, color: cs.primary, size: 20), Icon(Icons.category, color: cs.primary, size: 20),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
'Issue Category', I18n.t('review.category'),
style: Theme.of(context).textTheme.titleMedium style: Theme.of(context).textTheme.titleMedium
?.copyWith( ?.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -443,7 +449,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
Icon(Icons.warning, color: cs.secondary, size: 20), Icon(Icons.warning, color: cs.secondary, size: 20),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
'Severity Level', I18n.t('review.severity'),
style: Theme.of(context).textTheme.titleMedium style: Theme.of(context).textTheme.titleMedium
?.copyWith( ?.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -539,7 +545,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
Icon(Icons.note, color: cs.primary, size: 20), Icon(Icons.note, color: cs.primary, size: 20),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
'Additional Notes', I18n.t('review.notes'),
style: Theme.of(context).textTheme.titleMedium style: Theme.of(context).textTheme.titleMedium
?.copyWith( ?.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -552,7 +558,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
TextField( TextField(
controller: _notesController, controller: _notesController,
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Add any additional details or context...', hintText: I18n.t('review.notesHint'),
hintStyle: TextStyle( hintStyle: TextStyle(
color: cs.onSurface.withOpacity(0.6), color: cs.onSurface.withOpacity(0.6),
), ),
@@ -602,7 +608,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
Icon(Icons.location_on, color: cs.secondary, size: 20), Icon(Icons.location_on, color: cs.secondary, size: 20),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
'Location Details', I18n.t('review.location'),
style: Theme.of(context).textTheme.titleMedium style: Theme.of(context).textTheme.titleMedium
?.copyWith( ?.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -630,7 +636,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
'Coordinates', I18n.t('review.coordinates'),
style: Theme.of(context).textTheme.bodySmall style: Theme.of(context).textTheme.bodySmall
?.copyWith( ?.copyWith(
color: cs.onSurface.withOpacity(0.7), color: cs.onSurface.withOpacity(0.7),
@@ -659,7 +665,10 @@ class _ReviewScreenState extends State<ReviewScreen> {
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
'Accuracy: ${widget.report.location.accuracy!.toStringAsFixed(1)}m', I18n.t('review.accuracy', {
'0': widget.report.location.accuracy!
.toStringAsFixed(1),
}),
style: Theme.of(context).textTheme.bodySmall style: Theme.of(context).textTheme.bodySmall
?.copyWith( ?.copyWith(
color: cs.onSurface.withOpacity(0.7), color: cs.onSurface.withOpacity(0.7),

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../l10n/i18n.dart'; import '../../l10n/i18n.dart';
import '../../l10n/locale_provider.dart'; import '../../l10n/locale_provider.dart';
import '../../services/storage.dart'; import '../../services/storage.dart';
@@ -42,8 +43,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
title: Text(I18n.t('confirm.clearData.title')), title: Text(I18n.t('confirm.clearData.title')),
content: Text(I18n.t('confirm.clearData.message')), content: Text(I18n.t('confirm.clearData.message')),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(I18n.t('btn.no'))), TextButton(
TextButton(onPressed: () => Navigator.pop(ctx, true), child: Text(I18n.t('btn.yes'))), onPressed: () => Navigator.pop(ctx, false),
child: Text(I18n.t('btn.no')),
),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: Text(I18n.t('btn.yes')),
),
], ],
), ),
); );
@@ -59,25 +66,59 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (ok) { if (ok) {
await _loadStats(); await _loadStats();
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(I18n.t('toast.storageCleared')))); ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(I18n.t('toast.storageCleared'))),
);
} }
} else { } else {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to clear data'))); ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(I18n.t('error.clearData'))));
} }
} }
} }
} }
Future<void> _logout() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: Text(I18n.t('confirm.logout.title')),
content: Text(I18n.t('confirm.logout.message')),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: Text(I18n.t('btn.no')),
),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: Text(I18n.t('btn.yes')),
),
],
),
);
if (confirmed == true) {
// Clear user preferences to reset to initial state
final prefs = await SharedPreferences.getInstance();
await prefs.remove('onboarded_v1');
await prefs.remove('user_mode');
if (mounted) {
// Navigate back to the start router (welcome screen)
Navigator.of(context).pushNamedAndRemoveUntil('/', (route) => false);
}
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final localeProvider = Provider.of<LocaleProvider>(context); final localeProvider = Provider.of<LocaleProvider>(context);
final isEnglish = localeProvider.isEnglish; final isEnglish = localeProvider.isEnglish;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(title: Text(I18n.t('nav.settings'))),
title: Text(I18n.t('nav.settings')),
),
body: ListView( body: ListView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
children: [ children: [
@@ -110,22 +151,48 @@ class _SettingsScreenState extends State<SettingsScreen> {
ListTile( ListTile(
title: Text(I18n.t('settings.diagnostics')), title: Text(I18n.t('settings.diagnostics')),
subtitle: _loadingStats subtitle: _loadingStats
? const Text('Loading...') ? const Text('Loading...') // TODO: Move to i18n
: Text('${I18n.t('toast.reportSaved')}: ${_stats?.reportCount ?? 0}${_stats?.formattedPhotoSize ?? "0 B"}'), : Text(
'${I18n.t('toast.reportSaved')}: ${_stats?.reportCount ?? 0}${_stats?.formattedPhotoSize ?? "0 B"}',
),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
ElevatedButton.icon( ElevatedButton.icon(
onPressed: _clearing ? null : () => _confirmAndClearAll(context), onPressed: _clearing ? null : () => _confirmAndClearAll(context),
icon: const Icon(Icons.delete_forever), icon: const Icon(Icons.delete_forever),
label: _clearing ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)) : Text(I18n.t('btn.clearAll')), label: _clearing
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text(I18n.t('btn.clearAll')),
style: ElevatedButton.styleFrom(backgroundColor: Colors.red), style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text('App', style: Theme.of(context).textTheme.titleMedium), Text(
I18n.t('settings.app'),
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8), const SizedBox(height: 8),
ListTile( ListTile(
title: Text(I18n.t('app.name')), title: Text(I18n.t('app.name')),
subtitle: Text('v1.0.0'), subtitle: Text(I18n.t('settings.version')),
),
const Divider(),
ListTile(
title: Text(I18n.t('settings.account')),
subtitle: Text(I18n.t('settings.account.guest')),
),
const SizedBox(height: 8),
ElevatedButton.icon(
onPressed: () => _logout(),
icon: const Icon(Icons.logout),
label: Text(I18n.t('btn.logout')),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
),
), ),
], ],
), ),

View File

@@ -380,7 +380,9 @@ class ReportCard extends StatelessWidget {
children: [ children: [
Icon(Icons.visibility), Icon(Icons.visibility),
SizedBox(width: 8), SizedBox(width: 8),
Text('View Details'), Text(
'View Details',
), // TODO: Move to i18n but need to handle dynamic text in popup menu
], ],
), ),
), ),
@@ -390,7 +392,9 @@ class ReportCard extends StatelessWidget {
children: [ children: [
Icon(Icons.update), Icon(Icons.update),
SizedBox(width: 8), SizedBox(width: 8),
Text('Update Status'), Text(
'Update Status',
), // TODO: Move to i18n but need to handle dynamic text in popup menu
], ],
), ),
), ),
@@ -400,7 +404,10 @@ class ReportCard extends StatelessWidget {
children: [ children: [
Icon(Icons.delete, color: Colors.red), Icon(Icons.delete, color: Colors.red),
SizedBox(width: 8), SizedBox(width: 8),
Text('Delete', style: TextStyle(color: Colors.red)), Text(
'Delete',
style: TextStyle(color: Colors.red),
), // TODO: Move to i18n but need to handle dynamic text in popup menu
], ],
), ),
), ),