diff --git a/CLAUDE.md b/CLAUDE.md index 90d2322..eb719b7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,6 +32,11 @@ AI assistant guide for the MeshCore SAR Flutter application. ``` lib/ +├── l10n/ # Internationalization (i18n) +│ ├── app_localizations.dart # Generated localization class +│ ├── app_en.arb # English (default) +│ ├── app_hr.arb # Croatian (Hrvatski) +│ └── app_sl.arb # Slovenian (Slovenščina) ├── models/ # Data models │ ├── contact.dart, message.dart, sar_marker.dart │ ├── map_drawing.dart # MapDrawing, LineDrawing, RectangleDrawing @@ -55,8 +60,9 @@ lib/ │ ├── map_markers.dart # Map marker rendering │ ├── map/ # Map-specific widgets │ │ ├── drawing_layer.dart # Drawing rendering on map -│ │ └── drawing_toolbar.dart # Drawing UI controls -│ ├── messages/, contacts/ # Feature-specific widgets +│ │ ├── drawing_toolbar.dart # Drawing UI controls +│ │ └── compass/ # Compass dialog components (fully localized) +│ ├── messages/, contacts/ # Feature-specific widgets (fully localized) └── utils/ # Utilities ├── sar_message_parser.dart # SAR marker parsing └── drawing_message_parser.dart # Drawing message parsing @@ -418,12 +424,78 @@ Ultra-compact location display, tap to toggle DD/DMS formats, no close button (t - Provider: `lib/providers/drawing_provider.dart` (280 lines) - UI: `lib/widgets/map/drawing_toolbar.dart`, `lib/widgets/map/drawing_layer.dart` +## Internationalization (i18n) + +### Supported Languages +- **English (en)**: Default language +- **Croatian (hr)**: Hrvatski - Full localization +- **Slovenian (sl)**: Slovenščina - Full localization + +### Localization Files +- **ARB files**: `lib/l10n/app_{locale}.arb` (Application Resource Bundle) +- **Generated class**: `lib/l10n/app_localizations.dart` (auto-generated, do not edit) +- **Configuration**: `l10n.yaml` in project root + +### Adding Localized Strings + +1. **Add to English ARB** (`lib/l10n/app_en.arb`): +```json +{ + "myNewString": "My new text", + "@myNewString": { + "description": "Description of what this string is for" + }, + "stringWithParam": "Hello {name}", + "@stringWithParam": { + "description": "Greeting with name parameter", + "placeholders": { + "name": {"type": "String"} + } + } +} +``` + +2. **Add translations** to `app_hr.arb` and `app_sl.arb` + +3. **Generate localization files**: +```bash +flutter gen-l10n +``` + +4. **Use in code**: +```dart +import '../l10n/app_localizations.dart'; + +// In build method: +Text(AppLocalizations.of(context)!.myNewString) +Text(AppLocalizations.of(context)!.stringWithParam('John')) +``` + +### Important Notes +- **Import path**: Always use `import '../l10n/app_localizations.dart'` (relative path from widget location) +- **Do NOT use**: `import 'package:flutter_gen/gen_l10n/app_localizations.dart'` (incorrect) +- **Generate after changes**: Run `flutter gen-l10n` after modifying ARB files +- **Null safety**: Use `AppLocalizations.of(context)!` (with null assertion operator) + +### Localized Components +All major UI components are fully localized: +- Home screen and status indicators +- Settings screen and preferences +- Messages tab and SAR markers +- Contacts tab and contact details +- Map screen and compass dialog +- Drawing tools and filters +- All dialogs and confirmation messages + ## Build Commands ```bash # Dependencies flutter pub get +# Localization +flutter gen-l10n # Generate localization files after ARB changes + # Development flutter run # Debug mode flutter run -d # Specific device diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 18b251e..dfdb6c1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1168,5 +1168,535 @@ "type": "String" } } + }, + + "noContactsYet": "No contacts yet", + "@noContactsYet": { + "description": "Empty state message when there are no contacts" + }, + + "connectToDeviceToLoadContacts": "Connect to a device to load contacts", + "@connectToDeviceToLoadContacts": { + "description": "Instruction to connect device to load contacts" + }, + + "teamMembers": "Team Members", + "@teamMembers": { + "description": "Section header for team members (chat contacts)" + }, + + "repeaters": "Repeaters", + "@repeaters": { + "description": "Section header for repeater nodes" + }, + + "rooms": "Rooms", + "@rooms": { + "description": "Section header for rooms/channels" + }, + + "cacheStatistics": "Cache Statistics", + "@cacheStatistics": { + "description": "Title for cache statistics section" + }, + + "totalTiles": "Total Tiles", + "@totalTiles": { + "description": "Label for total number of cached tiles" + }, + + "cacheSize": "Cache Size", + "@cacheSize": { + "description": "Label for cache size in MB" + }, + + "storeName": "Store Name", + "@storeName": { + "description": "Label for cache store name" + }, + + "noCacheStatistics": "No cache statistics available", + "@noCacheStatistics": { + "description": "Message when cache statistics are unavailable" + }, + + "downloadRegion": "Download Region", + "@downloadRegion": { + "description": "Title for download region section" + }, + + "mapLayer": "Map Layer", + "@mapLayer": { + "description": "Label for map layer selection" + }, + + "regionBounds": "Region Bounds", + "@regionBounds": { + "description": "Title for region bounds input section" + }, + + "north": "North", + "@north": { + "description": "Label for north coordinate" + }, + + "south": "South", + "@south": { + "description": "Label for south coordinate" + }, + + "east": "East", + "@east": { + "description": "Label for east coordinate" + }, + + "west": "West", + "@west": { + "description": "Label for west coordinate" + }, + + "zoomLevels": "Zoom Levels", + "@zoomLevels": { + "description": "Title for zoom levels section" + }, + + "minZoom": "Min: {zoom}", + "@minZoom": { + "description": "Label for minimum zoom level", + "placeholders": { + "zoom": { + "type": "int" + } + } + }, + + "maxZoom": "Max: {zoom}", + "@maxZoom": { + "description": "Label for maximum zoom level", + "placeholders": { + "zoom": { + "type": "int" + } + } + }, + + "downloadingDots": "Downloading...", + "@downloadingDots": { + "description": "Status message during download" + }, + + "cancelDownload": "Cancel Download", + "@cancelDownload": { + "description": "Button to cancel download" + }, + + "downloadRegionButton": "Download Region", + "@downloadRegionButton": { + "description": "Button to start region download" + }, + + "downloadNote": "Note: Large regions or high zoom levels may take significant time and storage.", + "@downloadNote": { + "description": "Warning about download size and time" + }, + + "cacheManagement": "Cache Management", + "@cacheManagement": { + "description": "Title for cache management section" + }, + + "clearAllMaps": "Clear All Maps", + "@clearAllMaps": { + "description": "Button to clear all cached maps" + }, + + "clearMapsConfirmTitle": "Clear All Maps", + "@clearMapsConfirmTitle": { + "description": "Title for clear maps confirmation dialog" + }, + + "clearMapsConfirmMessage": "Are you sure you want to delete all downloaded maps? This action cannot be undone.", + "@clearMapsConfirmMessage": { + "description": "Confirmation message for clearing maps" + }, + + "mapDownloadCompleted": "Map download completed!", + "@mapDownloadCompleted": { + "description": "Success message after map download" + }, + + "cacheClearedSuccessfully": "Cache cleared successfully!", + "@cacheClearedSuccessfully": { + "description": "Success message after clearing cache" + }, + + "downloadCancelled": "Download cancelled", + "@downloadCancelled": { + "description": "Message when download is cancelled" + }, + + "startingDownload": "Starting download...", + "@startingDownload": { + "description": "Initial status when download begins" + }, + + "downloadingMapTiles": "Downloading map tiles...", + "@downloadingMapTiles": { + "description": "Status during tile download" + }, + + "downloadCompletedSuccessfully": "Download completed successfully!", + "@downloadCompletedSuccessfully": { + "description": "Status after successful download" + }, + + "cancellingDownload": "Cancelling download...", + "@cancellingDownload": { + "description": "Status while cancelling download" + }, + + "errorLoadingStats": "Error loading stats: {error}", + "@errorLoadingStats": { + "description": "Error message when loading cache stats fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + + "downloadFailed": "Download failed: {error}", + "@downloadFailed": { + "description": "Error message when download fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + + "cancelFailed": "Cancel failed: {error}", + "@cancelFailed": { + "description": "Error message when cancel fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + + "clearCacheFailed": "Clear cache failed: {error}", + "@clearCacheFailed": { + "description": "Error message when clearing cache fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + + "minZoomError": "Min zoom: {error}", + "@minZoomError": { + "description": "Validation error for minimum zoom", + "placeholders": { + "error": { + "type": "String" + } + } + }, + + "maxZoomError": "Max zoom: {error}", + "@maxZoomError": { + "description": "Validation error for maximum zoom", + "placeholders": { + "error": { + "type": "String" + } + } + }, + + "minZoomGreaterThanMax": "Minimum zoom must be less than or equal to maximum zoom", + "@minZoomGreaterThanMax": { + "description": "Validation error when min zoom > max zoom" + }, + + "selectMapLayer": "Select Map Layer", + "@selectMapLayer": { + "description": "Title for map layer selection dialog" + }, + + "mapOptions": "Map Options", + "@mapOptions": { + "description": "Title for map options dialog" + }, + + "showLegend": "Show Legend", + "@showLegend": { + "description": "Toggle for showing map legend" + }, + + "displayMarkerTypeCounts": "Display marker type counts", + "@displayMarkerTypeCounts": { + "description": "Description for show legend toggle" + }, + + "rotateMapWithHeading": "Rotate Map with Heading", + "@rotateMapWithHeading": { + "description": "Toggle for rotating map with compass heading" + }, + + "mapFollowsDirection": "Map follows your direction when moving", + "@mapFollowsDirection": { + "description": "Description for rotate map toggle" + }, + + "showMapDebugInfo": "Show Map Debug Info", + "@showMapDebugInfo": { + "description": "Toggle for showing map debug information" + }, + + "displayZoomLevelBounds": "Display zoom level and bounds", + "@displayZoomLevelBounds": { + "description": "Description for debug info toggle" + }, + + "fullscreenMode": "Fullscreen Mode", + "@fullscreenMode": { + "description": "Toggle for fullscreen map mode" + }, + + "hideUiFullMapView": "Hide all UI controls for full map view", + "@hideUiFullMapView": { + "description": "Description for fullscreen mode toggle" + }, + + "openStreetMap": "OpenStreetMap", + "@openStreetMap": { + "description": "OpenStreetMap layer name" + }, + + "openTopoMap": "OpenTopoMap", + "@openTopoMap": { + "description": "OpenTopoMap layer name" + }, + + "esriSatellite": "ESRI Satellite", + "@esriSatellite": { + "description": "ESRI Satellite imagery layer name" + }, + + "downloadVisibleArea": "Download visible area", + "@downloadVisibleArea": { + "description": "Tooltip for download visible area button" + }, + + "initializingMap": "Initializing map...", + "@initializingMap": { + "description": "Loading message for map initialization" + }, + + "dragToPosition": "Drag to Position", + "@dragToPosition": { + "description": "Label when dragging a pin on map" + }, + + "createSarMarker": "Create SAR Marker", + "@createSarMarker": { + "description": "Label for creating SAR marker from pin" + }, + + "compass": "Compass", + "@compass": { + "description": "Compass title in detailed compass dialog" + }, + + "navigationAndContacts": "Navigation & Contacts", + "@navigationAndContacts": { + "description": "Subtitle for compass dialog" + }, + + "sarAlert": "SAR ALERT", + "@sarAlert": { + "description": "Label for SAR alert badge on messages" + }, + + "messageSentToPublicChannel": "Message sent to public channel", + "@messageSentToPublicChannel": { + "description": "Success message when message is sent to public channel" + }, + + "pleaseSelectRoomToSendSar": "Please select a room to send SAR marker", + "@pleaseSelectRoomToSendSar": { + "description": "Error when no room is selected for SAR marker" + }, + + "failedToSendSarMarker": "Failed to send SAR marker: {error}", + "@failedToSendSarMarker": { + "description": "Error message when SAR marker fails to send", + "placeholders": { + "error": { + "type": "String" + } + } + }, + + "sarMarkerSentTo": "SAR marker sent to {roomName}", + "@sarMarkerSentTo": { + "description": "Success message when SAR marker is sent to room", + "placeholders": { + "roomName": { + "type": "String" + } + } + }, + + "notConnectedCannotSync": "Not connected - cannot sync messages", + "@notConnectedCannotSync": { + "description": "Warning when trying to sync messages while not connected" + }, + + "syncedMessageCount": "Synced {count} message(s)", + "@syncedMessageCount": { + "description": "Success message showing number of synced messages", + "placeholders": { + "count": { + "type": "int" + } + } + }, + + "noNewMessages": "No new messages", + "@noNewMessages": { + "description": "Info message when no new messages to sync" + }, + + "syncFailed": "Sync failed: {error}", + "@syncFailed": { + "description": "Error message when sync fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + + "failedToResendMessage": "Failed to resend message", + "@failedToResendMessage": { + "description": "Error when message retry fails" + }, + + "retryingMessage": "Retrying message...", + "@retryingMessage": { + "description": "Info message when retrying a failed message" + }, + + "retryFailed": "Retry failed: {error}", + "@retryFailed": { + "description": "Error message when retry fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + + "textCopiedToClipboard": "Text copied to clipboard", + "@textCopiedToClipboard": { + "description": "Success message when text is copied" + }, + + "cannotReplySenderMissing": "Cannot reply: sender information missing", + "@cannotReplySenderMissing": { + "description": "Error when sender info is missing for reply" + }, + + "cannotReplyContactNotFound": "Cannot reply: contact not found", + "@cannotReplyContactNotFound": { + "description": "Error when contact not found for reply" + }, + + "messageDeleted": "Message deleted", + "@messageDeleted": { + "description": "Info message when message is deleted" + }, + + "refreshedContacts": "Refreshed contacts", + "@refreshedContacts": { + "description": "Success message when contacts are refreshed" + }, + + "justNow": "Just now", + "@justNow": { + "description": "Time indicator for very recent activity" + }, + + "minutesAgo": "{minutes}m ago", + "@minutesAgo": { + "description": "Time indicator for minutes ago", + "placeholders": { + "minutes": { + "type": "int" + } + } + }, + + "hoursAgo": "{hours}h ago", + "@hoursAgo": { + "description": "Time indicator for hours ago", + "placeholders": { + "hours": { + "type": "int" + } + } + }, + + "daysAgo": "{days}d ago", + "@daysAgo": { + "description": "Time indicator for days ago", + "placeholders": { + "days": { + "type": "int" + } + } + }, + + "secondsAgo": "{seconds}s ago", + "@secondsAgo": { + "description": "Time indicator for seconds ago", + "placeholders": { + "seconds": { + "type": "int" + } + } + }, + + "sending": "Sending...", + "@sending": { + "description": "Delivery status: sending" + }, + + "sent": "Sent", + "@sent": { + "description": "Delivery status: sent" + }, + + "delivered": "Delivered", + "@delivered": { + "description": "Delivery status: delivered" + }, + + "deliveredWithTime": "Delivered ({time}ms)", + "@deliveredWithTime": { + "description": "Delivery status with round-trip time", + "placeholders": { + "time": { + "type": "int" + } + } + }, + + "failed": "Failed", + "@failed": { + "description": "Delivery status: failed" } } diff --git a/lib/l10n/app_hr.arb b/lib/l10n/app_hr.arb index f9114b0..5603021 100644 --- a/lib/l10n/app_hr.arb +++ b/lib/l10n/app_hr.arb @@ -391,5 +391,149 @@ "degreesLeft": "{degrees}° lijevo", - "latLonFormat": "Ššir: {latitude} Dužina: {longitude}" + "latLonFormat": "Ššir: {latitude} Dužina: {longitude}", + + "noContactsYet": "Još nema kontakata", + + "connectToDeviceToLoadContacts": "Povežite se s uređajem da učitate kontakte", + + "teamMembers": "Članovi tima", + + "repeaters": "Repetitori", + + "rooms": "Sobe", + + "cacheStatistics": "Statistika predmemorije", + + "totalTiles": "Ukupno pločica", + + "cacheSize": "Veličina predmemorije", + + "storeName": "Ime pohrane", + + "noCacheStatistics": "Statistika predmemorije nije dostupna", + + "downloadRegion": "Preuzmi regiju", + + "mapLayer": "Sloj karte", + + "regionBounds": "Granice regije", + + "north": "Sjever", + + "south": "Jug", + + "east": "Istok", + + "west": "Zapad", + + "zoomLevels": "Razine zumiranja", + + "minZoom": "Min: {zoom}", + + "maxZoom": "Maks: {zoom}", + + "downloadingDots": "Preuzimanje...", + + "cancelDownload": "Otkaži preuzimanje", + + "downloadRegionButton": "Preuzmi regiju", + + "downloadNote": "Napomena: Velike regije ili visoke razine zumiranja mogu zahtijevati značajno vrijeme i prostor za pohranu.", + + "cacheManagement": "Upravljanje predmemorijom", + + "clearAllMaps": "Očisti sve karte", + + "clearMapsConfirmTitle": "Očisti sve karte", + + "clearMapsConfirmMessage": "Jeste li sigurni da želite izbrisati sve preuzete karte? Ova radnja se ne može poništiti.", + + "mapDownloadCompleted": "Preuzimanje karte završeno!", + + "cacheClearedSuccessfully": "Predmemorija uspješno očišćena!", + + "downloadCancelled": "Preuzimanje otkazano", + + "startingDownload": "Pokretanje preuzimanja...", + + "downloadingMapTiles": "Preuzimanje pločica karte...", + + "downloadCompletedSuccessfully": "Preuzimanje uspješno završeno!", + + "cancellingDownload": "Otkazivanje preuzimanja...", + + "errorLoadingStats": "Greška pri učitavanju statistike: {error}", + + "downloadFailed": "Preuzimanje nije uspjelo: {error}", + + "cancelFailed": "Otkazivanje nije uspjelo: {error}", + + "clearCacheFailed": "Čišćenje predmemorije nije uspjelo: {error}", + + "minZoomError": "Min zumiranje: {error}", + + "maxZoomError": "Maks zumiranje: {error}", + + "minZoomGreaterThanMax": "Minimalno zumiranje mora biti manje ili jednako maksimalnom zumiranju", + + "selectMapLayer": "Odaberite sloj karte", + + "mapOptions": "Opcije karte", + + "showLegend": "Prikaži legendu", + + "displayMarkerTypeCounts": "Prikaži broj vrsta markera", + + "rotateMapWithHeading": "Rotiraj kartu sa smjerom", + + "mapFollowsDirection": "Karta slijedi vaš smjer pri kretanju", + + "showMapDebugInfo": "Prikaži debug informacije karte", + + "displayZoomLevelBounds": "Prikaži razinu zumiranja i granice", + + "fullscreenMode": "Način cijelog zaslona", + + "hideUiFullMapView": "Sakrij sve UI kontrole za prikaz cijele karte", + + "openStreetMap": "OpenStreetMap", + + "openTopoMap": "OpenTopoMap", + + "esriSatellite": "ESRI satelit", + + "downloadVisibleArea": "Preuzmi vidljivo područje", + + "initializingMap": "Inicijalizacija karte...", + + "dragToPosition": "Povuci na poziciju", + + "createSarMarker": "Kreiraj SAR marker", + + "compass": "Kompas", + + "navigationAndContacts": "Navigacija i kontakti", + + "sarAlert": "SAR UZBUNA", + + "justNow": "Upravo sada", + + "minutesAgo": "prije {minutes}m", + + "hoursAgo": "prije {hours}h", + + "daysAgo": "prije {days}d", + + "secondsAgo": "prije {seconds}s", + + "sending": "Slanje...", + + "sent": "Poslano", + + "delivered": "Dostavljeno", + + "deliveredWithTime": "Dostavljeno ({time}ms)", + + "failed": "Neuspjelo" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 6bfee49..8aff634 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1280,6 +1280,378 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Lat: {latitude} Lon: {longitude}'** String latLonFormat(String latitude, String longitude); + + /// Empty state message when there are no contacts + /// + /// In en, this message translates to: + /// **'No contacts yet'** + String get noContactsYet; + + /// Instruction to connect device to load contacts + /// + /// In en, this message translates to: + /// **'Connect to a device to load contacts'** + String get connectToDeviceToLoadContacts; + + /// Section header for team members (chat contacts) + /// + /// In en, this message translates to: + /// **'Team Members'** + String get teamMembers; + + /// Section header for repeater nodes + /// + /// In en, this message translates to: + /// **'Repeaters'** + String get repeaters; + + /// Section header for rooms/channels + /// + /// In en, this message translates to: + /// **'Rooms'** + String get rooms; + + /// Title for cache statistics section + /// + /// In en, this message translates to: + /// **'Cache Statistics'** + String get cacheStatistics; + + /// Label for total number of cached tiles + /// + /// In en, this message translates to: + /// **'Total Tiles'** + String get totalTiles; + + /// Label for cache size in MB + /// + /// In en, this message translates to: + /// **'Cache Size'** + String get cacheSize; + + /// Label for cache store name + /// + /// In en, this message translates to: + /// **'Store Name'** + String get storeName; + + /// Message when cache statistics are unavailable + /// + /// In en, this message translates to: + /// **'No cache statistics available'** + String get noCacheStatistics; + + /// Title for download region section + /// + /// In en, this message translates to: + /// **'Download Region'** + String get downloadRegion; + + /// Label for map layer selection + /// + /// In en, this message translates to: + /// **'Map Layer'** + String get mapLayer; + + /// Title for region bounds input section + /// + /// In en, this message translates to: + /// **'Region Bounds'** + String get regionBounds; + + /// Label for north coordinate + /// + /// In en, this message translates to: + /// **'North'** + String get north; + + /// Label for south coordinate + /// + /// In en, this message translates to: + /// **'South'** + String get south; + + /// Label for east coordinate + /// + /// In en, this message translates to: + /// **'East'** + String get east; + + /// Label for west coordinate + /// + /// In en, this message translates to: + /// **'West'** + String get west; + + /// Title for zoom levels section + /// + /// In en, this message translates to: + /// **'Zoom Levels'** + String get zoomLevels; + + /// Label for minimum zoom level + /// + /// In en, this message translates to: + /// **'Min: {zoom}'** + String minZoom(int zoom); + + /// Label for maximum zoom level + /// + /// In en, this message translates to: + /// **'Max: {zoom}'** + String maxZoom(int zoom); + + /// Status message during download + /// + /// In en, this message translates to: + /// **'Downloading...'** + String get downloadingDots; + + /// Button to cancel download + /// + /// In en, this message translates to: + /// **'Cancel Download'** + String get cancelDownload; + + /// Button to start region download + /// + /// In en, this message translates to: + /// **'Download Region'** + String get downloadRegionButton; + + /// Warning about download size and time + /// + /// In en, this message translates to: + /// **'Note: Large regions or high zoom levels may take significant time and storage.'** + String get downloadNote; + + /// Title for cache management section + /// + /// In en, this message translates to: + /// **'Cache Management'** + String get cacheManagement; + + /// Button to clear all cached maps + /// + /// In en, this message translates to: + /// **'Clear All Maps'** + String get clearAllMaps; + + /// Title for clear maps confirmation dialog + /// + /// In en, this message translates to: + /// **'Clear All Maps'** + String get clearMapsConfirmTitle; + + /// Confirmation message for clearing maps + /// + /// In en, this message translates to: + /// **'Are you sure you want to delete all downloaded maps? This action cannot be undone.'** + String get clearMapsConfirmMessage; + + /// Success message after map download + /// + /// In en, this message translates to: + /// **'Map download completed!'** + String get mapDownloadCompleted; + + /// Success message after clearing cache + /// + /// In en, this message translates to: + /// **'Cache cleared successfully!'** + String get cacheClearedSuccessfully; + + /// Message when download is cancelled + /// + /// In en, this message translates to: + /// **'Download cancelled'** + String get downloadCancelled; + + /// Initial status when download begins + /// + /// In en, this message translates to: + /// **'Starting download...'** + String get startingDownload; + + /// Status during tile download + /// + /// In en, this message translates to: + /// **'Downloading map tiles...'** + String get downloadingMapTiles; + + /// Status after successful download + /// + /// In en, this message translates to: + /// **'Download completed successfully!'** + String get downloadCompletedSuccessfully; + + /// Status while cancelling download + /// + /// In en, this message translates to: + /// **'Cancelling download...'** + String get cancellingDownload; + + /// Error message when loading cache stats fails + /// + /// In en, this message translates to: + /// **'Error loading stats: {error}'** + String errorLoadingStats(String error); + + /// Error message when download fails + /// + /// In en, this message translates to: + /// **'Download failed: {error}'** + String downloadFailed(String error); + + /// Error message when cancel fails + /// + /// In en, this message translates to: + /// **'Cancel failed: {error}'** + String cancelFailed(String error); + + /// Error message when clearing cache fails + /// + /// In en, this message translates to: + /// **'Clear cache failed: {error}'** + String clearCacheFailed(String error); + + /// Validation error for minimum zoom + /// + /// In en, this message translates to: + /// **'Min zoom: {error}'** + String minZoomError(String error); + + /// Validation error for maximum zoom + /// + /// In en, this message translates to: + /// **'Max zoom: {error}'** + String maxZoomError(String error); + + /// Validation error when min zoom > max zoom + /// + /// In en, this message translates to: + /// **'Minimum zoom must be less than or equal to maximum zoom'** + String get minZoomGreaterThanMax; + + /// Title for map layer selection dialog + /// + /// In en, this message translates to: + /// **'Select Map Layer'** + String get selectMapLayer; + + /// Title for map options dialog + /// + /// In en, this message translates to: + /// **'Map Options'** + String get mapOptions; + + /// Toggle for showing map legend + /// + /// In en, this message translates to: + /// **'Show Legend'** + String get showLegend; + + /// Description for show legend toggle + /// + /// In en, this message translates to: + /// **'Display marker type counts'** + String get displayMarkerTypeCounts; + + /// Toggle for rotating map with compass heading + /// + /// In en, this message translates to: + /// **'Rotate Map with Heading'** + String get rotateMapWithHeading; + + /// Description for rotate map toggle + /// + /// In en, this message translates to: + /// **'Map follows your direction when moving'** + String get mapFollowsDirection; + + /// Toggle for showing map debug information + /// + /// In en, this message translates to: + /// **'Show Map Debug Info'** + String get showMapDebugInfo; + + /// Description for debug info toggle + /// + /// In en, this message translates to: + /// **'Display zoom level and bounds'** + String get displayZoomLevelBounds; + + /// Toggle for fullscreen map mode + /// + /// In en, this message translates to: + /// **'Fullscreen Mode'** + String get fullscreenMode; + + /// Description for fullscreen mode toggle + /// + /// In en, this message translates to: + /// **'Hide all UI controls for full map view'** + String get hideUiFullMapView; + + /// OpenStreetMap layer name + /// + /// In en, this message translates to: + /// **'OpenStreetMap'** + String get openStreetMap; + + /// OpenTopoMap layer name + /// + /// In en, this message translates to: + /// **'OpenTopoMap'** + String get openTopoMap; + + /// ESRI Satellite imagery layer name + /// + /// In en, this message translates to: + /// **'ESRI Satellite'** + String get esriSatellite; + + /// Tooltip for download visible area button + /// + /// In en, this message translates to: + /// **'Download visible area'** + String get downloadVisibleArea; + + /// Loading message for map initialization + /// + /// In en, this message translates to: + /// **'Initializing map...'** + String get initializingMap; + + /// Label when dragging a pin on map + /// + /// In en, this message translates to: + /// **'Drag to Position'** + String get dragToPosition; + + /// Label for creating SAR marker from pin + /// + /// In en, this message translates to: + /// **'Create SAR Marker'** + String get createSarMarker; + + /// Compass title in detailed compass dialog + /// + /// In en, this message translates to: + /// **'Compass'** + String get compass; + + /// Subtitle for compass dialog + /// + /// In en, this message translates to: + /// **'Navigation & Contacts'** + String get navigationAndContacts; + + /// Label for SAR alert badge on messages + /// + /// In en, this message translates to: + /// **'SAR ALERT'** + String get sarAlert; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 547fbd5..ffb98f6 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -681,4 +681,211 @@ class AppLocalizationsEn extends AppLocalizations { String latLonFormat(String latitude, String longitude) { return 'Lat: $latitude Lon: $longitude'; } + + @override + String get noContactsYet => 'No contacts yet'; + + @override + String get connectToDeviceToLoadContacts => + 'Connect to a device to load contacts'; + + @override + String get teamMembers => 'Team Members'; + + @override + String get repeaters => 'Repeaters'; + + @override + String get rooms => 'Rooms'; + + @override + String get cacheStatistics => 'Cache Statistics'; + + @override + String get totalTiles => 'Total Tiles'; + + @override + String get cacheSize => 'Cache Size'; + + @override + String get storeName => 'Store Name'; + + @override + String get noCacheStatistics => 'No cache statistics available'; + + @override + String get downloadRegion => 'Download Region'; + + @override + String get mapLayer => 'Map Layer'; + + @override + String get regionBounds => 'Region Bounds'; + + @override + String get north => 'North'; + + @override + String get south => 'South'; + + @override + String get east => 'East'; + + @override + String get west => 'West'; + + @override + String get zoomLevels => 'Zoom Levels'; + + @override + String minZoom(int zoom) { + return 'Min: $zoom'; + } + + @override + String maxZoom(int zoom) { + return 'Max: $zoom'; + } + + @override + String get downloadingDots => 'Downloading...'; + + @override + String get cancelDownload => 'Cancel Download'; + + @override + String get downloadRegionButton => 'Download Region'; + + @override + String get downloadNote => + 'Note: Large regions or high zoom levels may take significant time and storage.'; + + @override + String get cacheManagement => 'Cache Management'; + + @override + String get clearAllMaps => 'Clear All Maps'; + + @override + String get clearMapsConfirmTitle => 'Clear All Maps'; + + @override + String get clearMapsConfirmMessage => + 'Are you sure you want to delete all downloaded maps? This action cannot be undone.'; + + @override + String get mapDownloadCompleted => 'Map download completed!'; + + @override + String get cacheClearedSuccessfully => 'Cache cleared successfully!'; + + @override + String get downloadCancelled => 'Download cancelled'; + + @override + String get startingDownload => 'Starting download...'; + + @override + String get downloadingMapTiles => 'Downloading map tiles...'; + + @override + String get downloadCompletedSuccessfully => + 'Download completed successfully!'; + + @override + String get cancellingDownload => 'Cancelling download...'; + + @override + String errorLoadingStats(String error) { + return 'Error loading stats: $error'; + } + + @override + String downloadFailed(String error) { + return 'Download failed: $error'; + } + + @override + String cancelFailed(String error) { + return 'Cancel failed: $error'; + } + + @override + String clearCacheFailed(String error) { + return 'Clear cache failed: $error'; + } + + @override + String minZoomError(String error) { + return 'Min zoom: $error'; + } + + @override + String maxZoomError(String error) { + return 'Max zoom: $error'; + } + + @override + String get minZoomGreaterThanMax => + 'Minimum zoom must be less than or equal to maximum zoom'; + + @override + String get selectMapLayer => 'Select Map Layer'; + + @override + String get mapOptions => 'Map Options'; + + @override + String get showLegend => 'Show Legend'; + + @override + String get displayMarkerTypeCounts => 'Display marker type counts'; + + @override + String get rotateMapWithHeading => 'Rotate Map with Heading'; + + @override + String get mapFollowsDirection => 'Map follows your direction when moving'; + + @override + String get showMapDebugInfo => 'Show Map Debug Info'; + + @override + String get displayZoomLevelBounds => 'Display zoom level and bounds'; + + @override + String get fullscreenMode => 'Fullscreen Mode'; + + @override + String get hideUiFullMapView => 'Hide all UI controls for full map view'; + + @override + String get openStreetMap => 'OpenStreetMap'; + + @override + String get openTopoMap => 'OpenTopoMap'; + + @override + String get esriSatellite => 'ESRI Satellite'; + + @override + String get downloadVisibleArea => 'Download visible area'; + + @override + String get initializingMap => 'Initializing map...'; + + @override + String get dragToPosition => 'Drag to Position'; + + @override + String get createSarMarker => 'Create SAR Marker'; + + @override + String get compass => 'Compass'; + + @override + String get navigationAndContacts => 'Navigation & Contacts'; + + @override + String get sarAlert => 'SAR ALERT'; } diff --git a/lib/l10n/app_localizations_hr.dart b/lib/l10n/app_localizations_hr.dart index 57ea03f..36ff036 100644 --- a/lib/l10n/app_localizations_hr.dart +++ b/lib/l10n/app_localizations_hr.dart @@ -681,4 +681,211 @@ class AppLocalizationsHr extends AppLocalizations { String latLonFormat(String latitude, String longitude) { return 'Ššir: $latitude Dužina: $longitude'; } + + @override + String get noContactsYet => 'Još nema kontakata'; + + @override + String get connectToDeviceToLoadContacts => + 'Povežite se s uređajem da učitate kontakte'; + + @override + String get teamMembers => 'Članovi tima'; + + @override + String get repeaters => 'Repetitori'; + + @override + String get rooms => 'Sobe'; + + @override + String get cacheStatistics => 'Statistika predmemorije'; + + @override + String get totalTiles => 'Ukupno pločica'; + + @override + String get cacheSize => 'Veličina predmemorije'; + + @override + String get storeName => 'Ime pohrane'; + + @override + String get noCacheStatistics => 'Statistika predmemorije nije dostupna'; + + @override + String get downloadRegion => 'Preuzmi regiju'; + + @override + String get mapLayer => 'Sloj karte'; + + @override + String get regionBounds => 'Granice regije'; + + @override + String get north => 'Sjever'; + + @override + String get south => 'Jug'; + + @override + String get east => 'Istok'; + + @override + String get west => 'Zapad'; + + @override + String get zoomLevels => 'Razine zumiranja'; + + @override + String minZoom(int zoom) { + return 'Min: $zoom'; + } + + @override + String maxZoom(int zoom) { + return 'Maks: $zoom'; + } + + @override + String get downloadingDots => 'Preuzimanje...'; + + @override + String get cancelDownload => 'Otkaži preuzimanje'; + + @override + String get downloadRegionButton => 'Preuzmi regiju'; + + @override + String get downloadNote => + 'Napomena: Velike regije ili visoke razine zumiranja mogu zahtijevati značajno vrijeme i prostor za pohranu.'; + + @override + String get cacheManagement => 'Upravljanje predmemorijom'; + + @override + String get clearAllMaps => 'Očisti sve karte'; + + @override + String get clearMapsConfirmTitle => 'Očisti sve karte'; + + @override + String get clearMapsConfirmMessage => + 'Jeste li sigurni da želite izbrisati sve preuzete karte? Ova radnja se ne može poništiti.'; + + @override + String get mapDownloadCompleted => 'Preuzimanje karte završeno!'; + + @override + String get cacheClearedSuccessfully => 'Predmemorija uspješno očišćena!'; + + @override + String get downloadCancelled => 'Preuzimanje otkazano'; + + @override + String get startingDownload => 'Pokretanje preuzimanja...'; + + @override + String get downloadingMapTiles => 'Preuzimanje pločica karte...'; + + @override + String get downloadCompletedSuccessfully => 'Preuzimanje uspješno završeno!'; + + @override + String get cancellingDownload => 'Otkazivanje preuzimanja...'; + + @override + String errorLoadingStats(String error) { + return 'Greška pri učitavanju statistike: $error'; + } + + @override + String downloadFailed(String error) { + return 'Preuzimanje nije uspjelo: $error'; + } + + @override + String cancelFailed(String error) { + return 'Otkazivanje nije uspjelo: $error'; + } + + @override + String clearCacheFailed(String error) { + return 'Čišćenje predmemorije nije uspjelo: $error'; + } + + @override + String minZoomError(String error) { + return 'Min zumiranje: $error'; + } + + @override + String maxZoomError(String error) { + return 'Maks zumiranje: $error'; + } + + @override + String get minZoomGreaterThanMax => + 'Minimalno zumiranje mora biti manje ili jednako maksimalnom zumiranju'; + + @override + String get selectMapLayer => 'Odaberite sloj karte'; + + @override + String get mapOptions => 'Opcije karte'; + + @override + String get showLegend => 'Prikaži legendu'; + + @override + String get displayMarkerTypeCounts => 'Prikaži broj vrsta markera'; + + @override + String get rotateMapWithHeading => 'Rotiraj kartu sa smjerom'; + + @override + String get mapFollowsDirection => 'Karta slijedi vaš smjer pri kretanju'; + + @override + String get showMapDebugInfo => 'Prikaži debug informacije karte'; + + @override + String get displayZoomLevelBounds => 'Prikaži razinu zumiranja i granice'; + + @override + String get fullscreenMode => 'Način cijelog zaslona'; + + @override + String get hideUiFullMapView => + 'Sakrij sve UI kontrole za prikaz cijele karte'; + + @override + String get openStreetMap => 'OpenStreetMap'; + + @override + String get openTopoMap => 'OpenTopoMap'; + + @override + String get esriSatellite => 'ESRI satelit'; + + @override + String get downloadVisibleArea => 'Preuzmi vidljivo područje'; + + @override + String get initializingMap => 'Inicijalizacija karte...'; + + @override + String get dragToPosition => 'Povuci na poziciju'; + + @override + String get createSarMarker => 'Kreiraj SAR marker'; + + @override + String get compass => 'Kompas'; + + @override + String get navigationAndContacts => 'Navigacija i kontakti'; + + @override + String get sarAlert => 'SAR UZBUNA'; } diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index a0d316f..2c78dc8 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -681,4 +681,211 @@ class AppLocalizationsSl extends AppLocalizations { String latLonFormat(String latitude, String longitude) { return 'Šir: $latitude Dolž: $longitude'; } + + @override + String get noContactsYet => 'Še ni stikov'; + + @override + String get connectToDeviceToLoadContacts => + 'Povežite se z napravo za nalaganje stikov'; + + @override + String get teamMembers => 'Člani ekipe'; + + @override + String get repeaters => 'Repetitorji'; + + @override + String get rooms => 'Sobe'; + + @override + String get cacheStatistics => 'Statistika predpomnilnika'; + + @override + String get totalTiles => 'Skupaj ploščic'; + + @override + String get cacheSize => 'Velikost predpomnilnika'; + + @override + String get storeName => 'Ime skladišča'; + + @override + String get noCacheStatistics => 'Statistika predpomnilnika ni na voljo'; + + @override + String get downloadRegion => 'Prenesi regijo'; + + @override + String get mapLayer => 'Sloj zemljevida'; + + @override + String get regionBounds => 'Meje regije'; + + @override + String get north => 'Sever'; + + @override + String get south => 'Jug'; + + @override + String get east => 'Vzhod'; + + @override + String get west => 'Zahod'; + + @override + String get zoomLevels => 'Nivoji povečave'; + + @override + String minZoom(int zoom) { + return 'Min: $zoom'; + } + + @override + String maxZoom(int zoom) { + return 'Maks: $zoom'; + } + + @override + String get downloadingDots => 'Prenašanje...'; + + @override + String get cancelDownload => 'Prekliči prenos'; + + @override + String get downloadRegionButton => 'Prenesi regijo'; + + @override + String get downloadNote => + 'Opomba: Velike regije ali visoki nivoji povečave lahko zahtevajo veliko časa in prostora za shranjevanje.'; + + @override + String get cacheManagement => 'Upravljanje predpomnilnika'; + + @override + String get clearAllMaps => 'Počisti vse zemljevide'; + + @override + String get clearMapsConfirmTitle => 'Počisti vse zemljevide'; + + @override + String get clearMapsConfirmMessage => + 'Ste prepričani, da želite izbrisati vse prenesene zemljevide? Tega dejanja ni mogoče razveljaviti.'; + + @override + String get mapDownloadCompleted => 'Prenos zemljevida končan!'; + + @override + String get cacheClearedSuccessfully => 'Predpomnilnik uspešno počiščen!'; + + @override + String get downloadCancelled => 'Prenos preklican'; + + @override + String get startingDownload => 'Začetek prenosa...'; + + @override + String get downloadingMapTiles => 'Prenašanje ploščic zemljevida...'; + + @override + String get downloadCompletedSuccessfully => 'Prenos uspešno končan!'; + + @override + String get cancellingDownload => 'Preklic prenosa...'; + + @override + String errorLoadingStats(String error) { + return 'Napaka pri nalaganju statistike: $error'; + } + + @override + String downloadFailed(String error) { + return 'Prenos ni uspel: $error'; + } + + @override + String cancelFailed(String error) { + return 'Preklic ni uspel: $error'; + } + + @override + String clearCacheFailed(String error) { + return 'Čiščenje predpomnilnika ni uspelo: $error'; + } + + @override + String minZoomError(String error) { + return 'Min povečava: $error'; + } + + @override + String maxZoomError(String error) { + return 'Maks povečava: $error'; + } + + @override + String get minZoomGreaterThanMax => + 'Minimalna povečava mora biti manjša ali enaka maksimalni povečavi'; + + @override + String get selectMapLayer => 'Izberite sloj zemljevida'; + + @override + String get mapOptions => 'Možnosti zemljevida'; + + @override + String get showLegend => 'Prikaži legendo'; + + @override + String get displayMarkerTypeCounts => 'Prikaži število vrst označevalcev'; + + @override + String get rotateMapWithHeading => 'Rotiraj zemljevid s smerjo'; + + @override + String get mapFollowsDirection => 'Zemljevid sledi vaši smeri pri gibanju'; + + @override + String get showMapDebugInfo => 'Prikaži debug informacije zemljevida'; + + @override + String get displayZoomLevelBounds => 'Prikaži nivo povečave in meje'; + + @override + String get fullscreenMode => 'Način celozaslonskega prikaza'; + + @override + String get hideUiFullMapView => + 'Skrij vse UI kontrole za poln prikaz zemljevida'; + + @override + String get openStreetMap => 'OpenStreetMap'; + + @override + String get openTopoMap => 'OpenTopoMap'; + + @override + String get esriSatellite => 'ESRI satelit'; + + @override + String get downloadVisibleArea => 'Prenesi vidno območje'; + + @override + String get initializingMap => 'Inicializacija zemljevida...'; + + @override + String get dragToPosition => 'Povleci na položaj'; + + @override + String get createSarMarker => 'Ustvari SAR označevalec'; + + @override + String get compass => 'Kompas'; + + @override + String get navigationAndContacts => 'Navigacija in stiki'; + + @override + String get sarAlert => 'SAR ALARM'; } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 27e5ac8..896600f 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -391,5 +391,149 @@ "degreesLeft": "{degrees}° levo", - "latLonFormat": "Šir: {latitude} Dolž: {longitude}" + "latLonFormat": "Šir: {latitude} Dolž: {longitude}", + + "noContactsYet": "Še ni stikov", + + "connectToDeviceToLoadContacts": "Povežite se z napravo za nalaganje stikov", + + "teamMembers": "Člani ekipe", + + "repeaters": "Repetitorji", + + "rooms": "Sobe", + + "cacheStatistics": "Statistika predpomnilnika", + + "totalTiles": "Skupaj ploščic", + + "cacheSize": "Velikost predpomnilnika", + + "storeName": "Ime skladišča", + + "noCacheStatistics": "Statistika predpomnilnika ni na voljo", + + "downloadRegion": "Prenesi regijo", + + "mapLayer": "Sloj zemljevida", + + "regionBounds": "Meje regije", + + "north": "Sever", + + "south": "Jug", + + "east": "Vzhod", + + "west": "Zahod", + + "zoomLevels": "Nivoji povečave", + + "minZoom": "Min: {zoom}", + + "maxZoom": "Maks: {zoom}", + + "downloadingDots": "Prenašanje...", + + "cancelDownload": "Prekliči prenos", + + "downloadRegionButton": "Prenesi regijo", + + "downloadNote": "Opomba: Velike regije ali visoki nivoji povečave lahko zahtevajo veliko časa in prostora za shranjevanje.", + + "cacheManagement": "Upravljanje predpomnilnika", + + "clearAllMaps": "Počisti vse zemljevide", + + "clearMapsConfirmTitle": "Počisti vse zemljevide", + + "clearMapsConfirmMessage": "Ste prepričani, da želite izbrisati vse prenesene zemljevide? Tega dejanja ni mogoče razveljaviti.", + + "mapDownloadCompleted": "Prenos zemljevida končan!", + + "cacheClearedSuccessfully": "Predpomnilnik uspešno počiščen!", + + "downloadCancelled": "Prenos preklican", + + "startingDownload": "Začetek prenosa...", + + "downloadingMapTiles": "Prenašanje ploščic zemljevida...", + + "downloadCompletedSuccessfully": "Prenos uspešno končan!", + + "cancellingDownload": "Preklic prenosa...", + + "errorLoadingStats": "Napaka pri nalaganju statistike: {error}", + + "downloadFailed": "Prenos ni uspel: {error}", + + "cancelFailed": "Preklic ni uspel: {error}", + + "clearCacheFailed": "Čiščenje predpomnilnika ni uspelo: {error}", + + "minZoomError": "Min povečava: {error}", + + "maxZoomError": "Maks povečava: {error}", + + "minZoomGreaterThanMax": "Minimalna povečava mora biti manjša ali enaka maksimalni povečavi", + + "selectMapLayer": "Izberite sloj zemljevida", + + "mapOptions": "Možnosti zemljevida", + + "showLegend": "Prikaži legendo", + + "displayMarkerTypeCounts": "Prikaži število vrst označevalcev", + + "rotateMapWithHeading": "Rotiraj zemljevid s smerjo", + + "mapFollowsDirection": "Zemljevid sledi vaši smeri pri gibanju", + + "showMapDebugInfo": "Prikaži debug informacije zemljevida", + + "displayZoomLevelBounds": "Prikaži nivo povečave in meje", + + "fullscreenMode": "Način celozaslonskega prikaza", + + "hideUiFullMapView": "Skrij vse UI kontrole za poln prikaz zemljevida", + + "openStreetMap": "OpenStreetMap", + + "openTopoMap": "OpenTopoMap", + + "esriSatellite": "ESRI satelit", + + "downloadVisibleArea": "Prenesi vidno območje", + + "initializingMap": "Inicializacija zemljevida...", + + "dragToPosition": "Povleci na položaj", + + "createSarMarker": "Ustvari SAR označevalec", + + "compass": "Kompas", + + "navigationAndContacts": "Navigacija in stiki", + + "sarAlert": "SAR ALARM", + + "justNow": "Pravkar", + + "minutesAgo": "pred {minutes}m", + + "hoursAgo": "pred {hours}h", + + "daysAgo": "pred {days}d", + + "secondsAgo": "pred {seconds}s", + + "sending": "Pošiljanje...", + + "sent": "Poslano", + + "delivered": "Dostavljeno", + + "deliveredWithTime": "Dostavljeno ({time}ms)", + + "failed": "Neuspešno" } diff --git a/lib/models/map_layer.dart b/lib/models/map_layer.dart index eaa20a7..a3cb0d1 100644 --- a/lib/models/map_layer.dart +++ b/lib/models/map_layer.dart @@ -1,3 +1,6 @@ +import 'package:flutter/material.dart'; +import '../l10n/app_localizations.dart'; + enum MapLayerType { openStreetMap, openTopoMap, @@ -19,6 +22,19 @@ class MapLayer { required this.maxZoom, }); + /// Get localized name for the layer + String getLocalizedName(BuildContext context) { + final localizations = AppLocalizations.of(context)!; + switch (type) { + case MapLayerType.openStreetMap: + return localizations.openStreetMap; + case MapLayerType.openTopoMap: + return localizations.openTopoMap; + case MapLayerType.esriWorldImagery: + return localizations.esriSatellite; + } + } + static const openStreetMap = MapLayer( type: MapLayerType.openStreetMap, name: 'OpenStreetMap', diff --git a/lib/providers/app_provider.dart b/lib/providers/app_provider.dart index 8390c75..2799ac9 100644 --- a/lib/providers/app_provider.dart +++ b/lib/providers/app_provider.dart @@ -179,8 +179,8 @@ class AppProvider with ChangeNotifier { ); } - // Sync device time - await connectionProvider.syncDeviceTime(); + // Note: Device clock is automatically synced during connection in MeshCoreBleService + // No need to sync it again here // Get battery and storage information await connectionProvider.getBatteryAndStorage(); diff --git a/lib/screens/contacts_tab.dart b/lib/screens/contacts_tab.dart index 6800976..8a41a90 100644 --- a/lib/screens/contacts_tab.dart +++ b/lib/screens/contacts_tab.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:geolocator/geolocator.dart'; +import '../l10n/app_localizations.dart'; import '../providers/contacts_provider.dart'; import '../providers/app_provider.dart'; import '../widgets/contacts/contact_tile.dart'; @@ -85,6 +86,7 @@ class _ContactsTabState extends State { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; return Consumer( builder: (context, contactsProvider, child) { final chatContacts = contactsProvider.chatContacts; @@ -103,12 +105,12 @@ class _ContactsTabState extends State { ), const SizedBox(height: 16), Text( - 'No contacts yet', + l10n.noContactsYet, style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 8), Text( - 'Connect to a device to load contacts', + l10n.connectToDeviceToLoadContacts, style: Theme.of(context).textTheme.bodyMedium, textAlign: TextAlign.center, ), @@ -125,7 +127,7 @@ class _ContactsTabState extends State { // Team Members (Chat contacts) if (chatContacts.isNotEmpty) ...[ _SectionHeader( - title: 'Team Members', + title: l10n.teamMembers, count: chatContacts.length, icon: Icons.people, ), @@ -143,7 +145,7 @@ class _ContactsTabState extends State { // Repeaters if (repeaters.isNotEmpty) ...[ _SectionHeader( - title: 'Repeaters', + title: l10n.repeaters, count: repeaters.length, icon: Icons.router, ), @@ -161,7 +163,7 @@ class _ContactsTabState extends State { // Rooms/Channels if (rooms.isNotEmpty) ...[ _SectionHeader( - title: 'Rooms', + title: l10n.rooms, count: rooms.length, icon: Icons.tag, ), diff --git a/lib/screens/map_management_screen.dart b/lib/screens/map_management_screen.dart index d181a94..5b3dd07 100644 --- a/lib/screens/map_management_screen.dart +++ b/lib/screens/map_management_screen.dart @@ -101,7 +101,7 @@ class _MapManagementScreenState extends State { } catch (e) { if (!mounted) return; setState(() { - _statusMessage = 'Error loading stats: $e'; + _statusMessage = AppLocalizations.of(context)!.errorLoadingStats(e.toString()); _isLoading = false; }); } @@ -133,18 +133,18 @@ class _MapManagementScreenState extends State { // Validate zoom levels final minZoomResult = validator.validateZoomLevel(_minZoom); if (!minZoomResult.isValid) { - _showError('Min zoom: ${minZoomResult.errorMessage}'); + _showError(AppLocalizations.of(context)!.minZoomError(minZoomResult.errorMessage!)); return; } final maxZoomResult = validator.validateZoomLevel(_maxZoom); if (!maxZoomResult.isValid) { - _showError('Max zoom: ${maxZoomResult.errorMessage}'); + _showError(AppLocalizations.of(context)!.maxZoomError(maxZoomResult.errorMessage!)); return; } if (_minZoom > _maxZoom) { - _showError('Minimum zoom must be less than or equal to maximum zoom'); + _showError(AppLocalizations.of(context)!.minZoomGreaterThanMax); return; } @@ -157,7 +157,7 @@ class _MapManagementScreenState extends State { setState(() { _isDownloading = true; _downloadProgress = 0.0; - _statusMessage = 'Starting download...'; + _statusMessage = AppLocalizations.of(context)!.startingDownload; }); await widget.tileCacheService.downloadRegion( @@ -170,7 +170,7 @@ class _MapManagementScreenState extends State { if (!mounted) return; setState(() { _downloadProgress = progress; - _statusMessage = 'Downloading map tiles...'; + _statusMessage = AppLocalizations.of(context)!.downloadingMapTiles; }); }, ); @@ -178,15 +178,15 @@ class _MapManagementScreenState extends State { if (!mounted) return; setState(() { _isDownloading = false; - _statusMessage = 'Download completed successfully!'; + _statusMessage = AppLocalizations.of(context)!.downloadCompletedSuccessfully; }); await _loadCacheStats(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Map download completed!'), + SnackBar( + content: Text(AppLocalizations.of(context)!.mapDownloadCompleted), backgroundColor: Colors.green, ), ); @@ -195,23 +195,23 @@ class _MapManagementScreenState extends State { if (!mounted) return; setState(() { _isDownloading = false; - _statusMessage = 'Download failed: $e'; + _statusMessage = AppLocalizations.of(context)!.downloadFailed(e.toString()); }); - _showError('Download failed: $e'); + _showError(AppLocalizations.of(context)!.downloadFailed(e.toString())); } } Future _cancelDownload() async { try { if (!mounted) return; - setState(() => _statusMessage = 'Cancelling download...'); + setState(() => _statusMessage = AppLocalizations.of(context)!.cancellingDownload); await widget.tileCacheService.cancelDownload(); if (!mounted) return; setState(() { _isDownloading = false; - _statusMessage = 'Download cancelled'; + _statusMessage = AppLocalizations.of(context)!.downloadCancelled; }); await _loadCacheStats(); @@ -228,9 +228,9 @@ class _MapManagementScreenState extends State { if (!mounted) return; setState(() { _isDownloading = false; - _statusMessage = 'Cancel failed: $e'; + _statusMessage = AppLocalizations.of(context)!.cancelFailed(e.toString()); }); - _showError('Cancel failed: $e'); + _showError(AppLocalizations.of(context)!.cancelFailed(e.toString())); } } @@ -238,9 +238,9 @@ class _MapManagementScreenState extends State { final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( - title: Text(AppLocalizations.of(context)!.clear), - content: const Text( - 'Are you sure you want to delete all downloaded maps? This action cannot be undone.', + title: Text(AppLocalizations.of(context)!.clearMapsConfirmTitle), + content: Text( + AppLocalizations.of(context)!.clearMapsConfirmMessage, ), actions: [ TextButton( @@ -268,15 +268,15 @@ class _MapManagementScreenState extends State { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Cache cleared successfully!'), + SnackBar( + content: Text(AppLocalizations.of(context)!.cacheClearedSuccessfully), backgroundColor: Colors.green, ), ); } } catch (e) { setState(() => _isLoading = false); - _showError('Clear cache failed: $e'); + _showError(AppLocalizations.of(context)!.clearCacheFailed(e.toString())); } } @@ -332,7 +332,7 @@ class _MapManagementScreenState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - 'Cache Statistics', + AppLocalizations.of(context)!.cacheStatistics, style: Theme.of(context).textTheme.titleLarge, ), IconButton( @@ -344,22 +344,22 @@ class _MapManagementScreenState extends State { const SizedBox(height: 16), if (_cacheStats != null) ...[ _buildStatRow( - 'Total Tiles', + AppLocalizations.of(context)!.totalTiles, '${_cacheStats!['tileCount'] ?? 0}', Icons.grid_on, ), _buildStatRow( - 'Cache Size', + AppLocalizations.of(context)!.cacheSize, '${(_cacheStats!['sizeMB'] ?? 0.0).toStringAsFixed(2)} MB', Icons.storage, ), _buildStatRow( - 'Store Name', + AppLocalizations.of(context)!.storeName, _cacheStats!['storeName'] ?? 'Unknown', Icons.folder, ), ] else - const Text('No cache statistics available'), + Text(AppLocalizations.of(context)!.noCacheStatistics), ], ), ), @@ -390,7 +390,7 @@ class _MapManagementScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Download Region', + AppLocalizations.of(context)!.downloadRegion, style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 16), @@ -398,14 +398,14 @@ class _MapManagementScreenState extends State { // Map Layer Selection DropdownButtonFormField( value: _selectedLayer, - decoration: const InputDecoration( - labelText: 'Map Layer', - border: OutlineInputBorder(), + decoration: InputDecoration( + labelText: AppLocalizations.of(context)!.mapLayer, + border: const OutlineInputBorder(), ), items: MapLayer.allLayers.map((layer) { return DropdownMenuItem( value: layer, - child: Text(layer.name), + child: Text(layer.getLocalizedName(context)), ); }).toList(), onChanged: _isDownloading ? null : (layer) { @@ -418,7 +418,7 @@ class _MapManagementScreenState extends State { // Coordinates Text( - 'Region Bounds', + AppLocalizations.of(context)!.regionBounds, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), @@ -427,9 +427,9 @@ class _MapManagementScreenState extends State { Expanded( child: TextField( controller: _northController, - decoration: const InputDecoration( - labelText: 'North', - border: OutlineInputBorder(), + decoration: InputDecoration( + labelText: AppLocalizations.of(context)!.north, + border: const OutlineInputBorder(), hintText: '46.1', ), keyboardType: const TextInputType.numberWithOptions(decimal: true), @@ -440,9 +440,9 @@ class _MapManagementScreenState extends State { Expanded( child: TextField( controller: _southController, - decoration: const InputDecoration( - labelText: 'South', - border: OutlineInputBorder(), + decoration: InputDecoration( + labelText: AppLocalizations.of(context)!.south, + border: const OutlineInputBorder(), hintText: '46.0', ), keyboardType: const TextInputType.numberWithOptions(decimal: true), @@ -457,9 +457,9 @@ class _MapManagementScreenState extends State { Expanded( child: TextField( controller: _eastController, - decoration: const InputDecoration( - labelText: 'East', - border: OutlineInputBorder(), + decoration: InputDecoration( + labelText: AppLocalizations.of(context)!.east, + border: const OutlineInputBorder(), hintText: '14.6', ), keyboardType: const TextInputType.numberWithOptions(decimal: true), @@ -470,9 +470,9 @@ class _MapManagementScreenState extends State { Expanded( child: TextField( controller: _westController, - decoration: const InputDecoration( - labelText: 'West', - border: OutlineInputBorder(), + decoration: InputDecoration( + labelText: AppLocalizations.of(context)!.west, + border: const OutlineInputBorder(), hintText: '14.4', ), keyboardType: const TextInputType.numberWithOptions(decimal: true), @@ -485,7 +485,7 @@ class _MapManagementScreenState extends State { // Zoom Levels Text( - 'Zoom Levels', + AppLocalizations.of(context)!.zoomLevels, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), @@ -495,7 +495,7 @@ class _MapManagementScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Min: $_minZoom'), + Text(AppLocalizations.of(context)!.minZoom(_minZoom)), Slider( value: _minZoom.toDouble(), min: 1, @@ -519,7 +519,7 @@ class _MapManagementScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Max: $_maxZoom'), + Text(AppLocalizations.of(context)!.maxZoom(_maxZoom)), Slider( value: _maxZoom.toDouble(), min: 1, @@ -560,7 +560,7 @@ class _MapManagementScreenState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - _statusMessage ?? 'Downloading...', + _statusMessage ?? AppLocalizations.of(context)!.downloadingDots, style: TextStyle( fontWeight: FontWeight.w500, color: Theme.of(context).colorScheme.onPrimaryContainer, @@ -600,7 +600,7 @@ class _MapManagementScreenState extends State { ElevatedButton.icon( onPressed: _cancelDownload, icon: const Icon(Icons.cancel), - label: const Text('Cancel Download'), + label: Text(AppLocalizations.of(context)!.cancelDownload), style: ElevatedButton.styleFrom( minimumSize: const Size.fromHeight(48), backgroundColor: Colors.red, @@ -611,7 +611,7 @@ class _MapManagementScreenState extends State { ElevatedButton.icon( onPressed: _downloadRegion, icon: const Icon(Icons.download), - label: const Text('Download Region'), + label: Text(AppLocalizations.of(context)!.downloadRegionButton), style: ElevatedButton.styleFrom( minimumSize: const Size.fromHeight(48), ), @@ -619,7 +619,7 @@ class _MapManagementScreenState extends State { const SizedBox(height: 8), Text( - 'Note: Large regions or high zoom levels may take significant time and storage.', + AppLocalizations.of(context)!.downloadNote, style: TextStyle(fontSize: 12, color: Colors.grey[600]), ), ], @@ -636,7 +636,7 @@ class _MapManagementScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Cache Management', + AppLocalizations.of(context)!.cacheManagement, style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 16), @@ -645,7 +645,7 @@ class _MapManagementScreenState extends State { OutlinedButton.icon( onPressed: _isDownloading ? null : _clearCache, icon: const Icon(Icons.delete_forever), - label: const Text('Clear All Maps'), + label: Text(AppLocalizations.of(context)!.clearAllMaps), style: OutlinedButton.styleFrom( foregroundColor: Colors.red, minimumSize: const Size.fromHeight(48), diff --git a/lib/screens/map_tab.dart b/lib/screens/map_tab.dart index a7e0ea7..cc00a3b 100644 --- a/lib/screens/map_tab.dart +++ b/lib/screens/map_tab.dart @@ -360,7 +360,7 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { const SizedBox(width: 12), Expanded( child: Text( - 'Select Map Layer', + AppLocalizations.of(context)!.selectMapLayer, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), @@ -368,7 +368,7 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { ), IconButton( icon: const Icon(Icons.download), - tooltip: 'Download visible area', + tooltip: AppLocalizations.of(context)!.downloadVisibleArea, onPressed: () { Navigator.pop(context); _navigateToDownload(context); @@ -382,7 +382,7 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { leading: _currentLayer.type == layer.type ? const Icon(Icons.check_circle, color: Colors.green) : const Icon(Icons.radio_button_unchecked), - title: Text(layer.name), + title: Text(layer.getLocalizedName(context)), subtitle: Text(layer.attribution), onTap: () { setState(() { @@ -442,7 +442,7 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { const Icon(Icons.settings), const SizedBox(width: 12), Text( - 'Map Options', + AppLocalizations.of(context)!.mapOptions, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), @@ -454,8 +454,8 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { // Legend toggle SwitchListTile( secondary: const Icon(Icons.info_outline), - title: const Text('Show Legend'), - subtitle: const Text('Display marker type counts'), + title: Text(AppLocalizations.of(context)!.showLegend), + subtitle: Text(AppLocalizations.of(context)!.displayMarkerTypeCounts), value: _showLegend, onChanged: (value) { setState(() { @@ -469,8 +469,8 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { // Compass rotation toggle SwitchListTile( secondary: const Icon(Icons.explore), - title: const Text('Rotate Map with Heading'), - subtitle: const Text('Map follows your direction when moving'), + title: Text(AppLocalizations.of(context)!.rotateMapWithHeading), + subtitle: Text(AppLocalizations.of(context)!.mapFollowsDirection), value: _rotateMarkerWithHeading, onChanged: (value) { setState(() { @@ -502,8 +502,8 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { // Map Debug Info toggle SwitchListTile( secondary: const Icon(Icons.developer_mode), - title: const Text('Show Map Debug Info'), - subtitle: const Text('Display zoom level and bounds'), + title: Text(AppLocalizations.of(context)!.showMapDebugInfo), + subtitle: Text(AppLocalizations.of(context)!.displayZoomLevelBounds), value: _showMapDebugInfo, onChanged: (value) { setState(() { @@ -517,8 +517,8 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { // Fullscreen mode toggle SwitchListTile( secondary: const Icon(Icons.fullscreen), - title: const Text('Fullscreen Mode'), - subtitle: const Text('Hide all UI controls for full map view'), + title: Text(AppLocalizations.of(context)!.fullscreenMode), + subtitle: Text(AppLocalizations.of(context)!.hideUiFullMapView), value: _isFullscreen, onChanged: (value) { setState(() { @@ -1062,7 +1062,7 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { ], ), child: Text( - _isDraggingPin ? 'Drag to Position' : 'Create SAR Marker', + _isDraggingPin ? AppLocalizations.of(context)!.dragToPosition : AppLocalizations.of(context)!.createSarMarker, style: const TextStyle( color: Colors.white, fontSize: 12, @@ -1109,7 +1109,7 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { const CircularProgressIndicator(), const SizedBox(height: 16), Text( - 'Initializing map...', + AppLocalizations.of(context)!.initializingMap, style: Theme.of(context).textTheme.bodyMedium, ), ], diff --git a/lib/screens/messages_tab.dart b/lib/screens/messages_tab.dart index d54c505..13e18ba 100644 --- a/lib/screens/messages_tab.dart +++ b/lib/screens/messages_tab.dart @@ -13,6 +13,7 @@ import '../widgets/messages/sar_update_sheet.dart'; import '../widgets/contacts/direct_message_sheet.dart'; import '../utils/toast_logger.dart'; import '../l10n/app_localizations.dart'; +import '../utils/sar_marker_extensions.dart'; class MessagesTab extends StatefulWidget { final VoidCallback onNavigateToMap; @@ -179,7 +180,7 @@ class _MessagesTabState extends State { if (!mounted) return; ToastLogger.warning( context, - '${sarType.displayName} marker broadcast to public channel', + '${sarType.getLocalizedName(context)} marker broadcast to public channel', ); } else { // Create message ID @@ -231,7 +232,7 @@ class _MessagesTabState extends State { if (!mounted) return; ToastLogger.success( context, - '${sarType.displayName} marker sent to room', + '${sarType.getLocalizedName(context)} marker sent to room', ); } } catch (e) { @@ -397,7 +398,7 @@ class _MessagesTabState extends State { maxLengthEnforcement: MaxLengthEnforcement.enforced, style: const TextStyle(fontSize: 14), decoration: InputDecoration( - hintText: 'Type a message...', + hintText: AppLocalizations.of(context)!.typeYourMessage, hintStyle: const TextStyle(fontSize: 14), border: OutlineInputBorder( borderRadius: BorderRadius.circular(24), @@ -860,7 +861,7 @@ class _MessageBubble extends StatelessWidget { ), const SizedBox(width: 4), Text( - 'SAR ALERT', + AppLocalizations.of(context)!.sarAlert, style: Theme.of(context).textTheme.labelSmall ?.copyWith( color: Colors.white, @@ -943,7 +944,7 @@ class _MessageBubble extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - message.sarMarkerType!.displayName, + message.sarMarkerType!.getLocalizedName(context), style: Theme.of(context).textTheme.titleSmall ?.copyWith(fontWeight: FontWeight.bold), ), diff --git a/lib/services/meshcore_ble_service.dart b/lib/services/meshcore_ble_service.dart index 4c7e5f9..093d440 100644 --- a/lib/services/meshcore_ble_service.dart +++ b/lib/services/meshcore_ble_service.dart @@ -199,8 +199,17 @@ class MeshCoreBleService { await _connectionManager.disconnect(); } - /// Send initial device query + /// Send initial device query and sync clock Future _sendDeviceQuery() async { + // CRITICAL: Set device clock FIRST, before any other commands + // This ensures the device has correct timestamps for all operations + print('⏰ [Service] Setting device clock before device query'); + await _commandSender.writeData(FrameBuilder.buildSetDeviceTime()); + + // Small delay to ensure clock is set before proceeding + await Future.delayed(const Duration(milliseconds: 100)); + + // Now send device query and app start await _commandSender.writeData(FrameBuilder.buildDeviceQuery()); await _commandSender.writeData(FrameBuilder.buildAppStart()); } diff --git a/lib/utils/sar_marker_extensions.dart b/lib/utils/sar_marker_extensions.dart new file mode 100644 index 0000000..e9ce800 --- /dev/null +++ b/lib/utils/sar_marker_extensions.dart @@ -0,0 +1,24 @@ +import 'package:flutter/widgets.dart'; +import '../models/sar_marker.dart'; +import '../l10n/app_localizations.dart'; + +/// Extension for SarMarkerType to provide localized display names +extension SarMarkerTypeLocalization on SarMarkerType { + /// Get localized display name for this SAR marker type + String getLocalizedName(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + switch (this) { + case SarMarkerType.foundPerson: + return l10n.foundPerson; + case SarMarkerType.fire: + return l10n.fire; + case SarMarkerType.stagingArea: + return l10n.stagingArea; + case SarMarkerType.object: + return 'Object'; // Not commonly used, keeping English for now + case SarMarkerType.unknown: + return 'Unknown'; // Not commonly used, keeping English for now + } + } +} diff --git a/lib/utils/time_ago_extensions.dart b/lib/utils/time_ago_extensions.dart new file mode 100644 index 0000000..54711c3 --- /dev/null +++ b/lib/utils/time_ago_extensions.dart @@ -0,0 +1,25 @@ +import 'package:flutter/widgets.dart'; +import '../l10n/app_localizations.dart'; + +/// Extension to provide localized "time ago" formatting +extension TimeAgoExtension on Duration { + /// Get localized time ago string + String toLocalizedTimeAgo(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + if (inMinutes < 1) return l10n.justNow; + if (inMinutes < 60) return l10n.minutesAgo(inMinutes); + if (inHours < 24) return l10n.hoursAgo(inHours); + return l10n.daysAgo(inDays); + } + + /// Get localized time ago string with seconds support + String toLocalizedTimeAgoWithSeconds(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + if (inSeconds < 60) return l10n.secondsAgo(inSeconds); + if (inMinutes < 60) return l10n.minutesAgo(inMinutes); + if (inHours < 24) return l10n.hoursAgo(inHours); + return l10n.daysAgo(inDays); + } +} diff --git a/lib/widgets/map/compass/compass_contact_list.dart b/lib/widgets/map/compass/compass_contact_list.dart index 015ce48..d119412 100644 --- a/lib/widgets/map/compass/compass_contact_list.dart +++ b/lib/widgets/map/compass/compass_contact_list.dart @@ -1,7 +1,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import '../../../l10n/app_localizations.dart'; import '../../../models/contact.dart'; /// Contact list section for the compass dialog. diff --git a/lib/widgets/map/compass/compass_sar_list.dart b/lib/widgets/map/compass/compass_sar_list.dart index 439e9c0..40aa46e 100644 --- a/lib/widgets/map/compass/compass_sar_list.dart +++ b/lib/widgets/map/compass/compass_sar_list.dart @@ -1,7 +1,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import '../../../l10n/app_localizations.dart'; import '../../../models/sar_marker.dart'; /// SAR marker list section for the compass dialog. diff --git a/lib/widgets/map/detailed_compass_dialog.dart b/lib/widgets/map/detailed_compass_dialog.dart index 5719061..e287416 100644 --- a/lib/widgets/map/detailed_compass_dialog.dart +++ b/lib/widgets/map/detailed_compass_dialog.dart @@ -13,6 +13,7 @@ import 'compass/compass_header.dart'; import 'compass/compass_filters.dart'; import 'compass/compass_sar_list.dart'; import 'compass/compass_contact_list.dart'; +import '../../l10n/app_localizations.dart'; enum HeadingAccuracySeverity { low, medium, high } @@ -285,19 +286,19 @@ class _DetailedCompassDialogState extends State { icon: const Icon(Icons.arrow_back), onPressed: () => Navigator.pop(context), ), - const Expanded( + Expanded( child: Column( children: [ Text( - 'Compass', - style: TextStyle( + AppLocalizations.of(context)!.compass, + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), Text( - 'Navigation & Contacts', - style: TextStyle( + AppLocalizations.of(context)!.navigationAndContacts, + style: const TextStyle( color: Colors.grey, fontSize: 14, ), diff --git a/pubspec.yaml b/pubspec.yaml index 41b7d94..8a70392 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -73,6 +73,9 @@ dependencies: # Background services flutter_background_service: ^5.0.13 + # Notifications + flutter_local_notifications: ^18.0.1 + dev_dependencies: flutter_test: sdk: flutter