feat(i18n): add capture prompt translations and locale init

- integrate flutter_localizations and delegates in MaterialApp
- use language-only supportedLocales; add resolution callback
- initialize I18n on app start and when switching language
- localize capture screen prompt via I18n.t
- schedule map centering via postFrame to avoid race conditions
- add flutter_localizations to pubspec
This commit is contained in:
2025-09-26 10:45:29 +08:00
parent 4496986d9b
commit ee9a9200b6
8 changed files with 45 additions and 8 deletions

View File

@@ -7,6 +7,7 @@
"btn.capture": "Capture", "btn.capture": "Capture",
"btn.gallery": "Gallery", "btn.gallery": "Gallery",
"btn.camera": "Camera", "btn.camera": "Camera",
"capture.prompt": "Take a photo of the issue",
"btn.next": "Next", "btn.next": "Next",
"btn.submit": "Submit", "btn.submit": "Submit",
"btn.save": "Save", "btn.save": "Save",

View File

@@ -7,6 +7,7 @@
"btn.capture": "Tangkap", "btn.capture": "Tangkap",
"btn.gallery": "Galeri", "btn.gallery": "Galeri",
"btn.camera": "Kamera", "btn.camera": "Kamera",
"capture.prompt": "Ambil gambar isu",
"btn.next": "Seterusnya", "btn.next": "Seterusnya",
"btn.submit": "Hantar", "btn.submit": "Hantar",
"btn.save": "Simpan", "btn.save": "Simpan",

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:flutter_localizations/flutter_localizations.dart';
import 'l10n/i18n.dart'; import 'l10n/i18n.dart';
import 'l10n/locale_provider.dart'; import 'l10n/locale_provider.dart';
import 'screens/report_flow/capture_screen.dart'; import 'screens/report_flow/capture_screen.dart';
@@ -22,10 +23,29 @@ class FixMateApp extends StatelessWidget {
darkTheme: AppThemes.dark(), darkTheme: AppThemes.dark(),
themeMode: ThemeMode.system, themeMode: ThemeMode.system,
locale: localeProvider.locale, locale: localeProvider.locale,
supportedLocales: const [ localizationsDelegates: const [
Locale('en', 'US'), GlobalMaterialLocalizations.delegate,
Locale('ms', 'MY'), GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
], ],
supportedLocales: const [
Locale('en'),
Locale('ms'),
],
localeResolutionCallback: (locale, supported) {
debugPrint('[i18n] localeResolution: device=$locale, supported=$supported');
if (locale == null) return supported.first;
for (final s in supported) {
if (s.languageCode == locale.languageCode) {
return s;
}
}
return supported.first;
},
builder: (context, child) {
debugPrint('[i18n] Building MaterialApp; locale=${localeProvider.locale}');
return child!;
},
home: const StartRouter(), home: const StartRouter(),
); );
}, },
@@ -153,6 +173,7 @@ class _StartRouterState extends State<StartRouter> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
debugPrint('[i18n] StartRouter: hasMaterial=${Localizations.of<MaterialLocalizations>(context, MaterialLocalizations) != null} locale=${Localizations.localeOf(context)}');
if (_loading) { if (_loading) {
return const Scaffold(body: Center(child: CircularProgressIndicator())); return const Scaffold(body: Center(child: CircularProgressIndicator()));
} }

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'i18n.dart';
/// Provider for managing app locale and language switching /// Provider for managing app locale and language switching
class LocaleProvider extends ChangeNotifier { class LocaleProvider extends ChangeNotifier {
@@ -20,6 +21,7 @@ class LocaleProvider extends ChangeNotifier {
_prefs = await SharedPreferences.getInstance(); _prefs = await SharedPreferences.getInstance();
final savedLanguage = _prefs.getString(_languageKey) ?? _defaultLanguage; final savedLanguage = _prefs.getString(_languageKey) ?? _defaultLanguage;
_locale = Locale(savedLanguage); _locale = Locale(savedLanguage);
await I18n.init(_locale);
notifyListeners(); notifyListeners();
} }
@@ -29,6 +31,7 @@ class LocaleProvider extends ChangeNotifier {
_locale = locale; _locale = locale;
await _prefs.setString(_languageKey, locale.languageCode); await _prefs.setString(_languageKey, locale.languageCode);
await I18n.init(locale);
notifyListeners(); notifyListeners();
} }

View File

@@ -64,9 +64,11 @@ class _MapScreenState extends State<MapScreen> {
_applyFilters(); _applyFilters();
// If we have filtered reports, fit; otherwise try device location // If we have filtered reports, fit; otherwise try device location
if (_filteredReports.isNotEmpty) { if (_filteredReports.isNotEmpty) {
debugPrint('[map] _refresh: filtered=${_filteredReports.length}; scheduling fitBounds postFrame');
WidgetsBinding.instance.addPostFrameCallback((_) => _fitToBounds()); WidgetsBinding.instance.addPostFrameCallback((_) => _fitToBounds());
} else { } else {
await _centerOnDeviceOrDefault(); debugPrint('[map] _refresh: filtered=0; scheduling centerOnDeviceOrDefault postFrame');
WidgetsBinding.instance.addPostFrameCallback((_) => _centerOnDeviceOrDefault());
} }
} }
@@ -74,11 +76,12 @@ class _MapScreenState extends State<MapScreen> {
try { try {
final pos = await LocationService.getBestAvailablePosition(); final pos = await LocationService.getBestAvailablePosition();
if (pos != null) { if (pos != null) {
debugPrint('[map] _centerOnDeviceOrDefault: moving to device location (${pos.latitude}, ${pos.longitude})');
_mapController.move(LatLng(pos.latitude, pos.longitude), _defaultZoom); _mapController.move(LatLng(pos.latitude, pos.longitude), _defaultZoom);
return; return;
} }
} catch (_) {} } catch (_) {}
// fallback debugPrint('[map] _centerOnDeviceOrDefault: moving to default center ($_defaultCenter) zoom=$_defaultZoom');
_mapController.move(_defaultCenter, _defaultZoom); _mapController.move(_defaultCenter, _defaultZoom);
} }

View File

@@ -116,6 +116,7 @@ class _CaptureScreenState extends State<CaptureScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
debugPrint('[i18n] CaptureScreen: locale=${I18n.currentLocale} prompt=${I18n.t('capture.prompt')}');
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(I18n.t('nav.report')), title: Text(I18n.t('nav.report')),
@@ -126,7 +127,7 @@ class _CaptureScreenState extends State<CaptureScreen> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
'Take a photo of the issue', I18n.t('capture.prompt'),
style: Theme.of(context).textTheme.headlineSmall, style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),

View File

@@ -166,6 +166,11 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.0" version: "5.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_map: flutter_map:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -340,10 +345,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: intl name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.19.0" version: "0.20.2"
latlong2: latlong2:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@@ -30,6 +30,8 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
flutter_localizations:
sdk: flutter
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
flutter_map: ^8.2.2 flutter_map: ^8.2.2
flutter_map_marker_cluster: ^8.2.2 flutter_map_marker_cluster: ^8.2.2