diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 1ec2433..18b251e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -677,5 +677,496 @@ "typeYourMessage": "Type your message...", "@typeYourMessage": { "description": "Placeholder text for message input field" + }, + + "quickLocationMarker": "Quick location marker", + "@quickLocationMarker": { + "description": "Subtitle for SAR marker sheet header" + }, + + "markerType": "Marker Type", + "@markerType": { + "description": "Label for marker type selection section" + }, + + "sendTo": "Send To", + "@sendTo": { + "description": "Label for destination selection section" + }, + + "noDestinationsAvailable": "No destinations available.", + "@noDestinationsAvailable": { + "description": "Warning when no rooms or channels exist" + }, + + "selectDestination": "Select destination...", + "@selectDestination": { + "description": "Placeholder for destination dropdown" + }, + + "ephemeralBroadcastInfo": "Ephemeral: Broadcast over-the-air only. Not stored - nodes must be online.", + "@ephemeralBroadcastInfo": { + "description": "Information about ephemeral channel broadcasts" + }, + + "persistentRoomInfo": "Persistent: Stored immutably in room. Synced automatically and preserved offline.", + "@persistentRoomInfo": { + "description": "Information about persistent room storage" + }, + + "location": "Location", + "@location": { + "description": "Label for location section" + }, + + "fromMap": "From Map", + "@fromMap": { + "description": "Badge showing location is from map tap" + }, + + "gettingLocation": "Getting location...", + "@gettingLocation": { + "description": "Loading message while fetching GPS location" + }, + + "locationError": "Location Error", + "@locationError": { + "description": "Title for location error messages" + }, + + "retry": "Retry", + "@retry": { + "description": "Retry button label" + }, + + "refreshLocation": "Refresh location", + "@refreshLocation": { + "description": "Tooltip for refresh location button" + }, + + "accuracyMeters": "Accuracy: ±{accuracy}m", + "@accuracyMeters": { + "description": "Display of GPS accuracy in meters", + "placeholders": { + "accuracy": { + "type": "int" + } + } + }, + + "notesOptional": "Notes (optional)", + "@notesOptional": { + "description": "Label for optional notes field" + }, + + "addAdditionalInformation": "Add additional information...", + "@addAdditionalInformation": { + "description": "Placeholder for notes field" + }, + + "lowAccuracyWarning": "Location accuracy is ±{accuracy}m. This may not be accurate enough for SAR operations.\n\nContinue anyway?", + "@lowAccuracyWarning": { + "description": "Warning dialog content for low GPS accuracy", + "placeholders": { + "accuracy": { + "type": "int" + } + } + }, + + "loginToRoom": "Login to Room", + "@loginToRoom": { + "description": "Title for room login dialog" + }, + + "enterPasswordInfo": "Enter the password to access this room. The password will be saved for future use.", + "@enterPasswordInfo": { + "description": "Information about room password" + }, + + "password": "Password", + "@password": { + "description": "Password field label" + }, + + "enterRoomPassword": "Enter room password", + "@enterRoomPassword": { + "description": "Password field hint" + }, + + "loggingInDots": "Logging in...", + "@loggingInDots": { + "description": "Button text while logging in" + }, + + "login": "Login", + "@login": { + "description": "Login button label" + }, + + "failedToAddRoom": "Failed to add room to device: {error}\n\nThe room may not have advertised yet.\nTry waiting for the room to broadcast.", + "@failedToAddRoom": { + "description": "Error message when adding room fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + + "direct": "Direct", + "@direct": { + "description": "Direct routing indicator" + }, + + "flood": "Flood", + "@flood": { + "description": "Flood routing indicator" + }, + + "admin": "Admin", + "@admin": { + "description": "Admin badge label" + }, + + "loggedIn": "Logged In", + "@loggedIn": { + "description": "Logged in status badge" + }, + + "noGpsData": "No GPS data", + "@noGpsData": { + "description": "Message when GPS data is not available" + }, + + "distance": "Distance", + "@distance": { + "description": "Distance label" + }, + + "pingingDirect": "Pinging {name} (direct via path)...", + "@pingingDirect": { + "description": "Status message for direct ping", + "placeholders": { + "name": { + "type": "String" + } + } + }, + + "pingingFlood": "Pinging {name} (flooding - no path)...", + "@pingingFlood": { + "description": "Status message for flood ping", + "placeholders": { + "name": { + "type": "String" + } + } + }, + + "directPingTimeout": "Direct ping timeout - retrying {name} with flooding...", + "@directPingTimeout": { + "description": "Warning when direct ping times out", + "placeholders": { + "name": { + "type": "String" + } + } + }, + + "pingSuccessful": "Ping successful to {name}{fallback}", + "@pingSuccessful": { + "description": "Success message for ping", + "placeholders": { + "name": { + "type": "String" + }, + "fallback": { + "type": "String" + } + } + }, + + "viaFloodingFallback": " (via flooding fallback)", + "@viaFloodingFallback": { + "description": "Suffix for ping success with fallback" + }, + + "pingFailed": "Ping failed to {name} - no response received", + "@pingFailed": { + "description": "Error message when ping fails", + "placeholders": { + "name": { + "type": "String" + } + } + }, + + "deleteContactConfirmation": "Are you sure you want to delete \"{name}\"?\n\nThis will remove the contact from both the app and the companion radio device.", + "@deleteContactConfirmation": { + "description": "Confirmation message for deleting contact", + "placeholders": { + "name": { + "type": "String" + } + } + }, + + "removingContact": "Removing {name}...", + "@removingContact": { + "description": "Status message while removing contact", + "placeholders": { + "name": { + "type": "String" + } + } + }, + + "contactRemoved": "Contact \"{name}\" removed", + "@contactRemoved": { + "description": "Success message after removing contact", + "placeholders": { + "name": { + "type": "String" + } + } + }, + + "failedToRemoveContact": "Failed to remove contact: {error}", + "@failedToRemoveContact": { + "description": "Error message when contact removal fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + + "type": "Type", + "@type": { + "description": "Contact type label" + }, + + "publicKey": "Public Key", + "@publicKey": { + "description": "Public key label" + }, + + "lastSeen": "Last Seen", + "@lastSeen": { + "description": "Last seen label" + }, + + "roomStatus": "Room Status", + "@roomStatus": { + "description": "Room status section header" + }, + + "loginStatus": "Login Status", + "@loginStatus": { + "description": "Login status label" + }, + + "notLoggedIn": "Not Logged In", + "@notLoggedIn": { + "description": "Not logged in status" + }, + + "adminAccess": "Admin Access", + "@adminAccess": { + "description": "Admin access label" + }, + + "yes": "Yes", + "@yes": { + "description": "Yes answer" + }, + + "no": "No", + "@no": { + "description": "No answer" + }, + + "permissions": "Permissions", + "@permissions": { + "description": "Permissions label" + }, + + "passwordSaved": "Password Saved", + "@passwordSaved": { + "description": "Password saved label" + }, + + "locationColon": "Location:", + "@locationColon": { + "description": "Location section header" + }, + + "telemetry": "Telemetry", + "@telemetry": { + "description": "Telemetry section header" + }, + + "requestingTelemetry": "Requesting telemetry from {name}...", + "@requestingTelemetry": { + "description": "Status message while requesting telemetry", + "placeholders": { + "name": { + "type": "String" + } + } + }, + + "voltage": "Voltage", + "@voltage": { + "description": "Voltage label" + }, + + "battery": "Battery", + "@battery": { + "description": "Battery label" + }, + + "temperature": "Temperature", + "@temperature": { + "description": "Temperature label" + }, + + "humidity": "Humidity", + "@humidity": { + "description": "Humidity label" + }, + + "pressure": "Pressure", + "@pressure": { + "description": "Pressure label" + }, + + "gpsTelemetry": "GPS (Telemetry)", + "@gpsTelemetry": { + "description": "GPS from telemetry label" + }, + + "updated": "Updated", + "@updated": { + "description": "Updated timestamp label" + }, + + "pathResetInfo": "Path reset for {name}. Next message will find a new route.", + "@pathResetInfo": { + "description": "Info message after path reset", + "placeholders": { + "name": { + "type": "String" + } + } + }, + + "reLoginToRoom": "Re-Login to Room", + "@reLoginToRoom": { + "description": "Button to re-login to room" + }, + + "heading": "Heading", + "@heading": { + "description": "Compass heading label" + }, + + "elevation": "Elevation", + "@elevation": { + "description": "Elevation/altitude label" + }, + + "accuracy": "Accuracy", + "@accuracy": { + "description": "GPS accuracy label" + }, + + "filterMarkers": "Filter Markers", + "@filterMarkers": { + "description": "Title for filter markers dialog" + }, + + "filterMarkersTooltip": "Filter markers", + "@filterMarkersTooltip": { + "description": "Tooltip for filter button" + }, + + "contactsFilter": "Contacts", + "@contactsFilter": { + "description": "Filter option for contacts" + }, + + "sarMarkers": "SAR Markers", + "@sarMarkers": { + "description": "SAR markers section header" + }, + + "foundPerson": "Found Person", + "@foundPerson": { + "description": "Found person SAR marker type" + }, + + "fire": "Fire", + "@fire": { + "description": "Fire SAR marker type" + }, + + "stagingArea": "Staging Area", + "@stagingArea": { + "description": "Staging area SAR marker type" + }, + + "showAll": "Show All", + "@showAll": { + "description": "Button to show all filters" + }, + + "nearbyContacts": "Nearby Contacts", + "@nearbyContacts": { + "description": "Title for nearby contacts list in compass" + }, + + "locationUnavailable": "Location unavailable", + "@locationUnavailable": { + "description": "Message when GPS location is unavailable" + }, + + "ahead": "ahead", + "@ahead": { + "description": "Relative bearing direction - ahead" + }, + + "degreesRight": "{degrees}° right", + "@degreesRight": { + "description": "Relative bearing direction - right", + "placeholders": { + "degrees": { + "type": "int" + } + } + }, + + "degreesLeft": "{degrees}° left", + "@degreesLeft": { + "description": "Relative bearing direction - left", + "placeholders": { + "degrees": { + "type": "int" + } + } + }, + + "latLonFormat": "Lat: {latitude} Lon: {longitude}", + "@latLonFormat": { + "description": "Latitude and longitude display format", + "placeholders": { + "latitude": { + "type": "String" + }, + "longitude": { + "type": "String" + } + } } } diff --git a/lib/l10n/app_hr.arb b/lib/l10n/app_hr.arb index d822238..f9114b0 100644 --- a/lib/l10n/app_hr.arb +++ b/lib/l10n/app_hr.arb @@ -231,5 +231,165 @@ "directMessageInfo": "Ova poruka će biti poslana izravno {contactName}. Također će se prikazati u glavnom feedu poruka.", - "typeYourMessage": "Upišite svoju poruku..." + "typeYourMessage": "Upišite svoju poruku...", + + "quickLocationMarker": "Brzi označitelj lokacije", + + "markerType": "Vrsta markera", + + "sendTo": "Pošalji na", + + "noDestinationsAvailable": "Nema dostupnih odredišta.", + + "selectDestination": "Odaberite odredište...", + + "ephemeralBroadcastInfo": "Privremeno: Samo emitiranje. Nije pohranjeno - čvorovi moraju biti online.", + + "persistentRoomInfo": "Trajno: Nepromjenjivo pohranjeno u sobi. Automatski sinkronizirano i očuvano offline.", + + "location": "Lokacija", + + "fromMap": "S karte", + + "gettingLocation": "Dohvaćanje lokacije...", + + "locationError": "Greška lokacije", + + "retry": "Pokušaj ponovno", + + "refreshLocation": "Osvježi lokaciju", + + "accuracyMeters": "Točnost: ±{accuracy}m", + + "notesOptional": "Napomene (opcionalno)", + + "addAdditionalInformation": "Dodajte dodatne informacije...", + + "lowAccuracyWarning": "Točnost lokacije je ±{accuracy}m. Ovo možda nije dovoljno precizno za SAR operacije.\n\nNastaviti svejedno?", + + "loginToRoom": "Prijava u sobu", + + "enterPasswordInfo": "Unesite lozinku za pristup ovoj sobi. Lozinka će biti spremljena za buduću upotrebu.", + + "password": "Lozinka", + + "enterRoomPassword": "Unesite lozinku sobe", + + "loggingInDots": "Prijavljivanje...", + + "login": "Prijava", + + "failedToAddRoom": "Neuspjelo dodavanje sobe na uređaj: {error}\n\nSoba možda još nije oglašena.\nPokušajte pričekati da soba emitira.", + + "direct": "Izravno", + + "flood": "Poplava", + + "admin": "Administrator", + + "loggedIn": "Prijavljen", + + "noGpsData": "Nema GPS podataka", + + "distance": "Udaljenost", + + "pingingDirect": "Pingiranje {name} (izravno putem puta)...", + + "pingingFlood": "Pingiranje {name} (poplava - nema puta)...", + + "directPingTimeout": "Istek izravnog pinga - ponovni pokušaj {name} s poplavom...", + + "pingSuccessful": "Ping uspješan prema {name}{fallback}", + + "viaFloodingFallback": " (putem rezervnog plavljenja)", + + "pingFailed": "Ping neuspješan prema {name} - nije primljen odgovor", + + "deleteContactConfirmation": "Jeste li sigurni da želite izbrisati \"{name}\"?\n\nOvo će ukloniti kontakt iz aplikacije i pratećeg radio uređaja.", + + "removingContact": "Uklanjanje {name}...", + + "contactRemoved": "Kontakt \"{name}\" uklonjen", + + "failedToRemoveContact": "Neuspjelo uklanjanje kontakta: {error}", + + "type": "Vrsta", + + "publicKey": "Javni ključ", + + "lastSeen": "Zadnje viđen", + + "roomStatus": "Status sobe", + + "loginStatus": "Status prijave", + + "notLoggedIn": "Nije prijavljen", + + "adminAccess": "Administratorski pristup", + + "yes": "Da", + + "no": "Ne", + + "permissions": "Dopuštenja", + + "passwordSaved": "Lozinka spremljena", + + "locationColon": "Lokacija:", + + "telemetry": "Telemetrija", + + "requestingTelemetry": "Zahtijevanje telemetrije od {name}...", + + "voltage": "Napon", + + "battery": "Baterija", + + "temperature": "Temperatura", + + "humidity": "Vlažnost", + + "pressure": "Tlak", + + "gpsTelemetry": "GPS (Telemetrija)", + + "updated": "Ažurirano", + + "pathResetInfo": "Put resetiran za {name}. Sljedeća poruka će pronaći novu rutu.", + + "reLoginToRoom": "Ponovna prijava u sobu", + + "heading": "Smjer", + + "elevation": "Nadmorska visina", + + "accuracy": "Točnost", + + "filterMarkers": "Filtriraj markere", + + "filterMarkersTooltip": "Filtriraj markere", + + "contactsFilter": "Kontakti", + + "sarMarkers": "SAR markeri", + + "foundPerson": "Pronađena osoba", + + "fire": "Požar", + + "stagingArea": "Zbirno mjesto", + + "showAll": "Prikaži sve", + + "nearbyContacts": "Obližnji kontakti", + + "locationUnavailable": "Lokacija nije dostupna", + + "ahead": "naprijed", + + "degreesRight": "{degrees}° desno", + + "degreesLeft": "{degrees}° lijevo", + + "latLonFormat": "Ššir: {latitude} Dužina: {longitude}" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 350b7b5..6bfee49 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -800,6 +800,486 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Type your message...'** String get typeYourMessage; + + /// Subtitle for SAR marker sheet header + /// + /// In en, this message translates to: + /// **'Quick location marker'** + String get quickLocationMarker; + + /// Label for marker type selection section + /// + /// In en, this message translates to: + /// **'Marker Type'** + String get markerType; + + /// Label for destination selection section + /// + /// In en, this message translates to: + /// **'Send To'** + String get sendTo; + + /// Warning when no rooms or channels exist + /// + /// In en, this message translates to: + /// **'No destinations available.'** + String get noDestinationsAvailable; + + /// Placeholder for destination dropdown + /// + /// In en, this message translates to: + /// **'Select destination...'** + String get selectDestination; + + /// Information about ephemeral channel broadcasts + /// + /// In en, this message translates to: + /// **'Ephemeral: Broadcast over-the-air only. Not stored - nodes must be online.'** + String get ephemeralBroadcastInfo; + + /// Information about persistent room storage + /// + /// In en, this message translates to: + /// **'Persistent: Stored immutably in room. Synced automatically and preserved offline.'** + String get persistentRoomInfo; + + /// Label for location section + /// + /// In en, this message translates to: + /// **'Location'** + String get location; + + /// Badge showing location is from map tap + /// + /// In en, this message translates to: + /// **'From Map'** + String get fromMap; + + /// Loading message while fetching GPS location + /// + /// In en, this message translates to: + /// **'Getting location...'** + String get gettingLocation; + + /// Title for location error messages + /// + /// In en, this message translates to: + /// **'Location Error'** + String get locationError; + + /// Retry button label + /// + /// In en, this message translates to: + /// **'Retry'** + String get retry; + + /// Tooltip for refresh location button + /// + /// In en, this message translates to: + /// **'Refresh location'** + String get refreshLocation; + + /// Display of GPS accuracy in meters + /// + /// In en, this message translates to: + /// **'Accuracy: ±{accuracy}m'** + String accuracyMeters(int accuracy); + + /// Label for optional notes field + /// + /// In en, this message translates to: + /// **'Notes (optional)'** + String get notesOptional; + + /// Placeholder for notes field + /// + /// In en, this message translates to: + /// **'Add additional information...'** + String get addAdditionalInformation; + + /// Warning dialog content for low GPS accuracy + /// + /// In en, this message translates to: + /// **'Location accuracy is ±{accuracy}m. This may not be accurate enough for SAR operations.\n\nContinue anyway?'** + String lowAccuracyWarning(int accuracy); + + /// Title for room login dialog + /// + /// In en, this message translates to: + /// **'Login to Room'** + String get loginToRoom; + + /// Information about room password + /// + /// In en, this message translates to: + /// **'Enter the password to access this room. The password will be saved for future use.'** + String get enterPasswordInfo; + + /// Password field label + /// + /// In en, this message translates to: + /// **'Password'** + String get password; + + /// Password field hint + /// + /// In en, this message translates to: + /// **'Enter room password'** + String get enterRoomPassword; + + /// Button text while logging in + /// + /// In en, this message translates to: + /// **'Logging in...'** + String get loggingInDots; + + /// Login button label + /// + /// In en, this message translates to: + /// **'Login'** + String get login; + + /// Error message when adding room fails + /// + /// In en, this message translates to: + /// **'Failed to add room to device: {error}\n\nThe room may not have advertised yet.\nTry waiting for the room to broadcast.'** + String failedToAddRoom(String error); + + /// Direct routing indicator + /// + /// In en, this message translates to: + /// **'Direct'** + String get direct; + + /// Flood routing indicator + /// + /// In en, this message translates to: + /// **'Flood'** + String get flood; + + /// Admin badge label + /// + /// In en, this message translates to: + /// **'Admin'** + String get admin; + + /// Logged in status badge + /// + /// In en, this message translates to: + /// **'Logged In'** + String get loggedIn; + + /// Message when GPS data is not available + /// + /// In en, this message translates to: + /// **'No GPS data'** + String get noGpsData; + + /// Distance label + /// + /// In en, this message translates to: + /// **'Distance'** + String get distance; + + /// Status message for direct ping + /// + /// In en, this message translates to: + /// **'Pinging {name} (direct via path)...'** + String pingingDirect(String name); + + /// Status message for flood ping + /// + /// In en, this message translates to: + /// **'Pinging {name} (flooding - no path)...'** + String pingingFlood(String name); + + /// Warning when direct ping times out + /// + /// In en, this message translates to: + /// **'Direct ping timeout - retrying {name} with flooding...'** + String directPingTimeout(String name); + + /// Success message for ping + /// + /// In en, this message translates to: + /// **'Ping successful to {name}{fallback}'** + String pingSuccessful(String name, String fallback); + + /// Suffix for ping success with fallback + /// + /// In en, this message translates to: + /// **' (via flooding fallback)'** + String get viaFloodingFallback; + + /// Error message when ping fails + /// + /// In en, this message translates to: + /// **'Ping failed to {name} - no response received'** + String pingFailed(String name); + + /// Confirmation message for deleting contact + /// + /// In en, this message translates to: + /// **'Are you sure you want to delete \"{name}\"?\n\nThis will remove the contact from both the app and the companion radio device.'** + String deleteContactConfirmation(String name); + + /// Status message while removing contact + /// + /// In en, this message translates to: + /// **'Removing {name}...'** + String removingContact(String name); + + /// Success message after removing contact + /// + /// In en, this message translates to: + /// **'Contact \"{name}\" removed'** + String contactRemoved(String name); + + /// Error message when contact removal fails + /// + /// In en, this message translates to: + /// **'Failed to remove contact: {error}'** + String failedToRemoveContact(String error); + + /// Contact type label + /// + /// In en, this message translates to: + /// **'Type'** + String get type; + + /// Public key label + /// + /// In en, this message translates to: + /// **'Public Key'** + String get publicKey; + + /// Last seen label + /// + /// In en, this message translates to: + /// **'Last Seen'** + String get lastSeen; + + /// Room status section header + /// + /// In en, this message translates to: + /// **'Room Status'** + String get roomStatus; + + /// Login status label + /// + /// In en, this message translates to: + /// **'Login Status'** + String get loginStatus; + + /// Not logged in status + /// + /// In en, this message translates to: + /// **'Not Logged In'** + String get notLoggedIn; + + /// Admin access label + /// + /// In en, this message translates to: + /// **'Admin Access'** + String get adminAccess; + + /// Yes answer + /// + /// In en, this message translates to: + /// **'Yes'** + String get yes; + + /// No answer + /// + /// In en, this message translates to: + /// **'No'** + String get no; + + /// Permissions label + /// + /// In en, this message translates to: + /// **'Permissions'** + String get permissions; + + /// Password saved label + /// + /// In en, this message translates to: + /// **'Password Saved'** + String get passwordSaved; + + /// Location section header + /// + /// In en, this message translates to: + /// **'Location:'** + String get locationColon; + + /// Telemetry section header + /// + /// In en, this message translates to: + /// **'Telemetry'** + String get telemetry; + + /// Status message while requesting telemetry + /// + /// In en, this message translates to: + /// **'Requesting telemetry from {name}...'** + String requestingTelemetry(String name); + + /// Voltage label + /// + /// In en, this message translates to: + /// **'Voltage'** + String get voltage; + + /// Battery label + /// + /// In en, this message translates to: + /// **'Battery'** + String get battery; + + /// Temperature label + /// + /// In en, this message translates to: + /// **'Temperature'** + String get temperature; + + /// Humidity label + /// + /// In en, this message translates to: + /// **'Humidity'** + String get humidity; + + /// Pressure label + /// + /// In en, this message translates to: + /// **'Pressure'** + String get pressure; + + /// GPS from telemetry label + /// + /// In en, this message translates to: + /// **'GPS (Telemetry)'** + String get gpsTelemetry; + + /// Updated timestamp label + /// + /// In en, this message translates to: + /// **'Updated'** + String get updated; + + /// Info message after path reset + /// + /// In en, this message translates to: + /// **'Path reset for {name}. Next message will find a new route.'** + String pathResetInfo(String name); + + /// Button to re-login to room + /// + /// In en, this message translates to: + /// **'Re-Login to Room'** + String get reLoginToRoom; + + /// Compass heading label + /// + /// In en, this message translates to: + /// **'Heading'** + String get heading; + + /// Elevation/altitude label + /// + /// In en, this message translates to: + /// **'Elevation'** + String get elevation; + + /// GPS accuracy label + /// + /// In en, this message translates to: + /// **'Accuracy'** + String get accuracy; + + /// Title for filter markers dialog + /// + /// In en, this message translates to: + /// **'Filter Markers'** + String get filterMarkers; + + /// Tooltip for filter button + /// + /// In en, this message translates to: + /// **'Filter markers'** + String get filterMarkersTooltip; + + /// Filter option for contacts + /// + /// In en, this message translates to: + /// **'Contacts'** + String get contactsFilter; + + /// SAR markers section header + /// + /// In en, this message translates to: + /// **'SAR Markers'** + String get sarMarkers; + + /// Found person SAR marker type + /// + /// In en, this message translates to: + /// **'Found Person'** + String get foundPerson; + + /// Fire SAR marker type + /// + /// In en, this message translates to: + /// **'Fire'** + String get fire; + + /// Staging area SAR marker type + /// + /// In en, this message translates to: + /// **'Staging Area'** + String get stagingArea; + + /// Button to show all filters + /// + /// In en, this message translates to: + /// **'Show All'** + String get showAll; + + /// Title for nearby contacts list in compass + /// + /// In en, this message translates to: + /// **'Nearby Contacts'** + String get nearbyContacts; + + /// Message when GPS location is unavailable + /// + /// In en, this message translates to: + /// **'Location unavailable'** + String get locationUnavailable; + + /// Relative bearing direction - ahead + /// + /// In en, this message translates to: + /// **'ahead'** + String get ahead; + + /// Relative bearing direction - right + /// + /// In en, this message translates to: + /// **'{degrees}° right'** + String degreesRight(int degrees); + + /// Relative bearing direction - left + /// + /// In en, this message translates to: + /// **'{degrees}° left'** + String degreesLeft(int degrees); + + /// Latitude and longitude display format + /// + /// In en, this message translates to: + /// **'Lat: {latitude} Lon: {longitude}'** + String latLonFormat(String latitude, String longitude); } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 9fd8396..547fbd5 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -404,4 +404,281 @@ class AppLocalizationsEn extends AppLocalizations { @override String get typeYourMessage => 'Type your message...'; + + @override + String get quickLocationMarker => 'Quick location marker'; + + @override + String get markerType => 'Marker Type'; + + @override + String get sendTo => 'Send To'; + + @override + String get noDestinationsAvailable => 'No destinations available.'; + + @override + String get selectDestination => 'Select destination...'; + + @override + String get ephemeralBroadcastInfo => + 'Ephemeral: Broadcast over-the-air only. Not stored - nodes must be online.'; + + @override + String get persistentRoomInfo => + 'Persistent: Stored immutably in room. Synced automatically and preserved offline.'; + + @override + String get location => 'Location'; + + @override + String get fromMap => 'From Map'; + + @override + String get gettingLocation => 'Getting location...'; + + @override + String get locationError => 'Location Error'; + + @override + String get retry => 'Retry'; + + @override + String get refreshLocation => 'Refresh location'; + + @override + String accuracyMeters(int accuracy) { + return 'Accuracy: ±${accuracy}m'; + } + + @override + String get notesOptional => 'Notes (optional)'; + + @override + String get addAdditionalInformation => 'Add additional information...'; + + @override + String lowAccuracyWarning(int accuracy) { + return 'Location accuracy is ±${accuracy}m. This may not be accurate enough for SAR operations.\n\nContinue anyway?'; + } + + @override + String get loginToRoom => 'Login to Room'; + + @override + String get enterPasswordInfo => + 'Enter the password to access this room. The password will be saved for future use.'; + + @override + String get password => 'Password'; + + @override + String get enterRoomPassword => 'Enter room password'; + + @override + String get loggingInDots => 'Logging in...'; + + @override + String get login => 'Login'; + + @override + String failedToAddRoom(String error) { + return 'Failed to add room to device: $error\n\nThe room may not have advertised yet.\nTry waiting for the room to broadcast.'; + } + + @override + String get direct => 'Direct'; + + @override + String get flood => 'Flood'; + + @override + String get admin => 'Admin'; + + @override + String get loggedIn => 'Logged In'; + + @override + String get noGpsData => 'No GPS data'; + + @override + String get distance => 'Distance'; + + @override + String pingingDirect(String name) { + return 'Pinging $name (direct via path)...'; + } + + @override + String pingingFlood(String name) { + return 'Pinging $name (flooding - no path)...'; + } + + @override + String directPingTimeout(String name) { + return 'Direct ping timeout - retrying $name with flooding...'; + } + + @override + String pingSuccessful(String name, String fallback) { + return 'Ping successful to $name$fallback'; + } + + @override + String get viaFloodingFallback => ' (via flooding fallback)'; + + @override + String pingFailed(String name) { + return 'Ping failed to $name - no response received'; + } + + @override + String deleteContactConfirmation(String name) { + return 'Are you sure you want to delete \"$name\"?\n\nThis will remove the contact from both the app and the companion radio device.'; + } + + @override + String removingContact(String name) { + return 'Removing $name...'; + } + + @override + String contactRemoved(String name) { + return 'Contact \"$name\" removed'; + } + + @override + String failedToRemoveContact(String error) { + return 'Failed to remove contact: $error'; + } + + @override + String get type => 'Type'; + + @override + String get publicKey => 'Public Key'; + + @override + String get lastSeen => 'Last Seen'; + + @override + String get roomStatus => 'Room Status'; + + @override + String get loginStatus => 'Login Status'; + + @override + String get notLoggedIn => 'Not Logged In'; + + @override + String get adminAccess => 'Admin Access'; + + @override + String get yes => 'Yes'; + + @override + String get no => 'No'; + + @override + String get permissions => 'Permissions'; + + @override + String get passwordSaved => 'Password Saved'; + + @override + String get locationColon => 'Location:'; + + @override + String get telemetry => 'Telemetry'; + + @override + String requestingTelemetry(String name) { + return 'Requesting telemetry from $name...'; + } + + @override + String get voltage => 'Voltage'; + + @override + String get battery => 'Battery'; + + @override + String get temperature => 'Temperature'; + + @override + String get humidity => 'Humidity'; + + @override + String get pressure => 'Pressure'; + + @override + String get gpsTelemetry => 'GPS (Telemetry)'; + + @override + String get updated => 'Updated'; + + @override + String pathResetInfo(String name) { + return 'Path reset for $name. Next message will find a new route.'; + } + + @override + String get reLoginToRoom => 'Re-Login to Room'; + + @override + String get heading => 'Heading'; + + @override + String get elevation => 'Elevation'; + + @override + String get accuracy => 'Accuracy'; + + @override + String get filterMarkers => 'Filter Markers'; + + @override + String get filterMarkersTooltip => 'Filter markers'; + + @override + String get contactsFilter => 'Contacts'; + + @override + String get sarMarkers => 'SAR Markers'; + + @override + String get foundPerson => 'Found Person'; + + @override + String get fire => 'Fire'; + + @override + String get stagingArea => 'Staging Area'; + + @override + String get showAll => 'Show All'; + + @override + String get nearbyContacts => 'Nearby Contacts'; + + @override + String get locationUnavailable => 'Location unavailable'; + + @override + String get ahead => 'ahead'; + + @override + String degreesRight(int degrees) { + return '$degrees° right'; + } + + @override + String degreesLeft(int degrees) { + return '$degrees° left'; + } + + @override + String latLonFormat(String latitude, String longitude) { + return 'Lat: $latitude Lon: $longitude'; + } } diff --git a/lib/l10n/app_localizations_hr.dart b/lib/l10n/app_localizations_hr.dart index 3746a15..57ea03f 100644 --- a/lib/l10n/app_localizations_hr.dart +++ b/lib/l10n/app_localizations_hr.dart @@ -404,4 +404,281 @@ class AppLocalizationsHr extends AppLocalizations { @override String get typeYourMessage => 'Upišite svoju poruku...'; + + @override + String get quickLocationMarker => 'Brzi označitelj lokacije'; + + @override + String get markerType => 'Vrsta markera'; + + @override + String get sendTo => 'Pošalji na'; + + @override + String get noDestinationsAvailable => 'Nema dostupnih odredišta.'; + + @override + String get selectDestination => 'Odaberite odredište...'; + + @override + String get ephemeralBroadcastInfo => + 'Privremeno: Samo emitiranje. Nije pohranjeno - čvorovi moraju biti online.'; + + @override + String get persistentRoomInfo => + 'Trajno: Nepromjenjivo pohranjeno u sobi. Automatski sinkronizirano i očuvano offline.'; + + @override + String get location => 'Lokacija'; + + @override + String get fromMap => 'S karte'; + + @override + String get gettingLocation => 'Dohvaćanje lokacije...'; + + @override + String get locationError => 'Greška lokacije'; + + @override + String get retry => 'Pokušaj ponovno'; + + @override + String get refreshLocation => 'Osvježi lokaciju'; + + @override + String accuracyMeters(int accuracy) { + return 'Točnost: ±${accuracy}m'; + } + + @override + String get notesOptional => 'Napomene (opcionalno)'; + + @override + String get addAdditionalInformation => 'Dodajte dodatne informacije...'; + + @override + String lowAccuracyWarning(int accuracy) { + return 'Točnost lokacije je ±${accuracy}m. Ovo možda nije dovoljno precizno za SAR operacije.\n\nNastaviti svejedno?'; + } + + @override + String get loginToRoom => 'Prijava u sobu'; + + @override + String get enterPasswordInfo => + 'Unesite lozinku za pristup ovoj sobi. Lozinka će biti spremljena za buduću upotrebu.'; + + @override + String get password => 'Lozinka'; + + @override + String get enterRoomPassword => 'Unesite lozinku sobe'; + + @override + String get loggingInDots => 'Prijavljivanje...'; + + @override + String get login => 'Prijava'; + + @override + String failedToAddRoom(String error) { + return 'Neuspjelo dodavanje sobe na uređaj: $error\n\nSoba možda još nije oglašena.\nPokušajte pričekati da soba emitira.'; + } + + @override + String get direct => 'Izravno'; + + @override + String get flood => 'Poplava'; + + @override + String get admin => 'Administrator'; + + @override + String get loggedIn => 'Prijavljen'; + + @override + String get noGpsData => 'Nema GPS podataka'; + + @override + String get distance => 'Udaljenost'; + + @override + String pingingDirect(String name) { + return 'Pingiranje $name (izravno putem puta)...'; + } + + @override + String pingingFlood(String name) { + return 'Pingiranje $name (poplava - nema puta)...'; + } + + @override + String directPingTimeout(String name) { + return 'Istek izravnog pinga - ponovni pokušaj $name s poplavom...'; + } + + @override + String pingSuccessful(String name, String fallback) { + return 'Ping uspješan prema $name$fallback'; + } + + @override + String get viaFloodingFallback => ' (putem rezervnog plavljenja)'; + + @override + String pingFailed(String name) { + return 'Ping neuspješan prema $name - nije primljen odgovor'; + } + + @override + String deleteContactConfirmation(String name) { + return 'Jeste li sigurni da želite izbrisati \"$name\"?\n\nOvo će ukloniti kontakt iz aplikacije i pratećeg radio uređaja.'; + } + + @override + String removingContact(String name) { + return 'Uklanjanje $name...'; + } + + @override + String contactRemoved(String name) { + return 'Kontakt \"$name\" uklonjen'; + } + + @override + String failedToRemoveContact(String error) { + return 'Neuspjelo uklanjanje kontakta: $error'; + } + + @override + String get type => 'Vrsta'; + + @override + String get publicKey => 'Javni ključ'; + + @override + String get lastSeen => 'Zadnje viđen'; + + @override + String get roomStatus => 'Status sobe'; + + @override + String get loginStatus => 'Status prijave'; + + @override + String get notLoggedIn => 'Nije prijavljen'; + + @override + String get adminAccess => 'Administratorski pristup'; + + @override + String get yes => 'Da'; + + @override + String get no => 'Ne'; + + @override + String get permissions => 'Dopuštenja'; + + @override + String get passwordSaved => 'Lozinka spremljena'; + + @override + String get locationColon => 'Lokacija:'; + + @override + String get telemetry => 'Telemetrija'; + + @override + String requestingTelemetry(String name) { + return 'Zahtijevanje telemetrije od $name...'; + } + + @override + String get voltage => 'Napon'; + + @override + String get battery => 'Baterija'; + + @override + String get temperature => 'Temperatura'; + + @override + String get humidity => 'Vlažnost'; + + @override + String get pressure => 'Tlak'; + + @override + String get gpsTelemetry => 'GPS (Telemetrija)'; + + @override + String get updated => 'Ažurirano'; + + @override + String pathResetInfo(String name) { + return 'Put resetiran za $name. Sljedeća poruka će pronaći novu rutu.'; + } + + @override + String get reLoginToRoom => 'Ponovna prijava u sobu'; + + @override + String get heading => 'Smjer'; + + @override + String get elevation => 'Nadmorska visina'; + + @override + String get accuracy => 'Točnost'; + + @override + String get filterMarkers => 'Filtriraj markere'; + + @override + String get filterMarkersTooltip => 'Filtriraj markere'; + + @override + String get contactsFilter => 'Kontakti'; + + @override + String get sarMarkers => 'SAR markeri'; + + @override + String get foundPerson => 'Pronađena osoba'; + + @override + String get fire => 'Požar'; + + @override + String get stagingArea => 'Zbirno mjesto'; + + @override + String get showAll => 'Prikaži sve'; + + @override + String get nearbyContacts => 'Obližnji kontakti'; + + @override + String get locationUnavailable => 'Lokacija nije dostupna'; + + @override + String get ahead => 'naprijed'; + + @override + String degreesRight(int degrees) { + return '$degrees° desno'; + } + + @override + String degreesLeft(int degrees) { + return '$degrees° lijevo'; + } + + @override + String latLonFormat(String latitude, String longitude) { + return 'Ššir: $latitude Dužina: $longitude'; + } } diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 844fd3f..a0d316f 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -404,4 +404,281 @@ class AppLocalizationsSl extends AppLocalizations { @override String get typeYourMessage => 'Vnesite svoje sporočilo...'; + + @override + String get quickLocationMarker => 'Hitri označevalec lokacije'; + + @override + String get markerType => 'Vrsta označevalca'; + + @override + String get sendTo => 'Pošlji na'; + + @override + String get noDestinationsAvailable => 'Ni dostopnih ciljev.'; + + @override + String get selectDestination => 'Izberite cilj...'; + + @override + String get ephemeralBroadcastInfo => + 'Začasno: Samo oddajanje. Ni shranjeno - vozlišča morajo biti povezana.'; + + @override + String get persistentRoomInfo => + 'Trajno: Nespremenljivo shranjeno v sobi. Samodejno sinhronizirano in ohranjeno brez povezave.'; + + @override + String get location => 'Lokacija'; + + @override + String get fromMap => 'Z zemljevida'; + + @override + String get gettingLocation => 'Pridobivanje lokacije...'; + + @override + String get locationError => 'Napaka lokacije'; + + @override + String get retry => 'Poskusi znova'; + + @override + String get refreshLocation => 'Osveži lokacijo'; + + @override + String accuracyMeters(int accuracy) { + return 'Natančnost: ±${accuracy}m'; + } + + @override + String get notesOptional => 'Opombe (neobvezno)'; + + @override + String get addAdditionalInformation => 'Dodajte dodatne informacije...'; + + @override + String lowAccuracyWarning(int accuracy) { + return 'Natančnost lokacije je ±${accuracy}m. To morda ni dovolj natančno za SAR operacije.\n\nVseeno nadaljuj?'; + } + + @override + String get loginToRoom => 'Prijava v sobo'; + + @override + String get enterPasswordInfo => + 'Vnesite geslo za dostop do te sobe. Geslo bo shranjeno za prihodnjo uporabo.'; + + @override + String get password => 'Geslo'; + + @override + String get enterRoomPassword => 'Vnesite geslo sobe'; + + @override + String get loggingInDots => 'Prijavljanje...'; + + @override + String get login => 'Prijava'; + + @override + String failedToAddRoom(String error) { + return 'Dodajanje sobe v napravo ni uspelo: $error\n\nSoba morda še ni oglašena.\nPoskusite počakati, da soba odda.'; + } + + @override + String get direct => 'Neposredno'; + + @override + String get flood => 'Poplava'; + + @override + String get admin => 'Administrator'; + + @override + String get loggedIn => 'Prijavljen'; + + @override + String get noGpsData => 'Ni GPS podatkov'; + + @override + String get distance => 'Razdalja'; + + @override + String pingingDirect(String name) { + return 'Pinganje $name (neposredno preko poti)...'; + } + + @override + String pingingFlood(String name) { + return 'Pinganje $name (poplava - brez poti)...'; + } + + @override + String directPingTimeout(String name) { + return 'Časovna omejitev neposrednega pinga - ponovni poskus $name s poplavo...'; + } + + @override + String pingSuccessful(String name, String fallback) { + return 'Ping uspešen do $name$fallback'; + } + + @override + String get viaFloodingFallback => ' (preko rezervnega poplavljanja)'; + + @override + String pingFailed(String name) { + return 'Ping neuspešen do $name - odgovor ni prejet'; + } + + @override + String deleteContactConfirmation(String name) { + return 'Ste prepričani, da želite izbrisati \"$name\"?\n\nTo bo odstranilo stik iz aplikacije in spremljevalne radijske naprave.'; + } + + @override + String removingContact(String name) { + return 'Odstranjevanje $name...'; + } + + @override + String contactRemoved(String name) { + return 'Stik \"$name\" odstranjen'; + } + + @override + String failedToRemoveContact(String error) { + return 'Odstranjevanje stika ni uspelo: $error'; + } + + @override + String get type => 'Vrsta'; + + @override + String get publicKey => 'Javni ključ'; + + @override + String get lastSeen => 'Nazadnje viden'; + + @override + String get roomStatus => 'Status sobe'; + + @override + String get loginStatus => 'Status prijave'; + + @override + String get notLoggedIn => 'Ni prijavljen'; + + @override + String get adminAccess => 'Administratorski dostop'; + + @override + String get yes => 'Da'; + + @override + String get no => 'Ne'; + + @override + String get permissions => 'Dovoljenja'; + + @override + String get passwordSaved => 'Geslo shranjeno'; + + @override + String get locationColon => 'Lokacija:'; + + @override + String get telemetry => 'Telemetrija'; + + @override + String requestingTelemetry(String name) { + return 'Zahtevanje telemetrije od $name...'; + } + + @override + String get voltage => 'Napetost'; + + @override + String get battery => 'Baterija'; + + @override + String get temperature => 'Temperatura'; + + @override + String get humidity => 'Vlažnost'; + + @override + String get pressure => 'Tlak'; + + @override + String get gpsTelemetry => 'GPS (Telemetrija)'; + + @override + String get updated => 'Posodobljeno'; + + @override + String pathResetInfo(String name) { + return 'Pot ponastavljena za $name. Naslednje sporočilo bo našlo novo pot.'; + } + + @override + String get reLoginToRoom => 'Ponovna prijava v sobo'; + + @override + String get heading => 'Smer'; + + @override + String get elevation => 'Nadmorska višina'; + + @override + String get accuracy => 'Natančnost'; + + @override + String get filterMarkers => 'Filtriraj označevalce'; + + @override + String get filterMarkersTooltip => 'Filtriraj označevalce'; + + @override + String get contactsFilter => 'Stiki'; + + @override + String get sarMarkers => 'SAR označevalci'; + + @override + String get foundPerson => 'Najdena oseba'; + + @override + String get fire => 'Ogenj'; + + @override + String get stagingArea => 'Zbirališče'; + + @override + String get showAll => 'Prikaži vse'; + + @override + String get nearbyContacts => 'Bližnji stiki'; + + @override + String get locationUnavailable => 'Lokacija ni na voljo'; + + @override + String get ahead => 'naravnost'; + + @override + String degreesRight(int degrees) { + return '$degrees° desno'; + } + + @override + String degreesLeft(int degrees) { + return '$degrees° levo'; + } + + @override + String latLonFormat(String latitude, String longitude) { + return 'Šir: $latitude Dolž: $longitude'; + } } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 533ea12..27e5ac8 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -231,5 +231,165 @@ "directMessageInfo": "To sporočilo bo poslano neposredno {contactName}. Prikazalo se bo tudi v glavnem viru sporočil.", - "typeYourMessage": "Vnesite svoje sporočilo..." + "typeYourMessage": "Vnesite svoje sporočilo...", + + "quickLocationMarker": "Hitri označevalec lokacije", + + "markerType": "Vrsta označevalca", + + "sendTo": "Pošlji na", + + "noDestinationsAvailable": "Ni dostopnih ciljev.", + + "selectDestination": "Izberite cilj...", + + "ephemeralBroadcastInfo": "Začasno: Samo oddajanje. Ni shranjeno - vozlišča morajo biti povezana.", + + "persistentRoomInfo": "Trajno: Nespremenljivo shranjeno v sobi. Samodejno sinhronizirano in ohranjeno brez povezave.", + + "location": "Lokacija", + + "fromMap": "Z zemljevida", + + "gettingLocation": "Pridobivanje lokacije...", + + "locationError": "Napaka lokacije", + + "retry": "Poskusi znova", + + "refreshLocation": "Osveži lokacijo", + + "accuracyMeters": "Natančnost: ±{accuracy}m", + + "notesOptional": "Opombe (neobvezno)", + + "addAdditionalInformation": "Dodajte dodatne informacije...", + + "lowAccuracyWarning": "Natančnost lokacije je ±{accuracy}m. To morda ni dovolj natančno za SAR operacije.\n\nVseeno nadaljuj?", + + "loginToRoom": "Prijava v sobo", + + "enterPasswordInfo": "Vnesite geslo za dostop do te sobe. Geslo bo shranjeno za prihodnjo uporabo.", + + "password": "Geslo", + + "enterRoomPassword": "Vnesite geslo sobe", + + "loggingInDots": "Prijavljanje...", + + "login": "Prijava", + + "failedToAddRoom": "Dodajanje sobe v napravo ni uspelo: {error}\n\nSoba morda še ni oglašena.\nPoskusite počakati, da soba odda.", + + "direct": "Neposredno", + + "flood": "Poplava", + + "admin": "Administrator", + + "loggedIn": "Prijavljen", + + "noGpsData": "Ni GPS podatkov", + + "distance": "Razdalja", + + "pingingDirect": "Pinganje {name} (neposredno preko poti)...", + + "pingingFlood": "Pinganje {name} (poplava - brez poti)...", + + "directPingTimeout": "Časovna omejitev neposrednega pinga - ponovni poskus {name} s poplavo...", + + "pingSuccessful": "Ping uspešen do {name}{fallback}", + + "viaFloodingFallback": " (preko rezervnega poplavljanja)", + + "pingFailed": "Ping neuspešen do {name} - odgovor ni prejet", + + "deleteContactConfirmation": "Ste prepričani, da želite izbrisati \"{name}\"?\n\nTo bo odstranilo stik iz aplikacije in spremljevalne radijske naprave.", + + "removingContact": "Odstranjevanje {name}...", + + "contactRemoved": "Stik \"{name}\" odstranjen", + + "failedToRemoveContact": "Odstranjevanje stika ni uspelo: {error}", + + "type": "Vrsta", + + "publicKey": "Javni ključ", + + "lastSeen": "Nazadnje viden", + + "roomStatus": "Status sobe", + + "loginStatus": "Status prijave", + + "notLoggedIn": "Ni prijavljen", + + "adminAccess": "Administratorski dostop", + + "yes": "Da", + + "no": "Ne", + + "permissions": "Dovoljenja", + + "passwordSaved": "Geslo shranjeno", + + "locationColon": "Lokacija:", + + "telemetry": "Telemetrija", + + "requestingTelemetry": "Zahtevanje telemetrije od {name}...", + + "voltage": "Napetost", + + "battery": "Baterija", + + "temperature": "Temperatura", + + "humidity": "Vlažnost", + + "pressure": "Tlak", + + "gpsTelemetry": "GPS (Telemetrija)", + + "updated": "Posodobljeno", + + "pathResetInfo": "Pot ponastavljena za {name}. Naslednje sporočilo bo našlo novo pot.", + + "reLoginToRoom": "Ponovna prijava v sobo", + + "heading": "Smer", + + "elevation": "Nadmorska višina", + + "accuracy": "Natančnost", + + "filterMarkers": "Filtriraj označevalce", + + "filterMarkersTooltip": "Filtriraj označevalce", + + "contactsFilter": "Stiki", + + "sarMarkers": "SAR označevalci", + + "foundPerson": "Najdena oseba", + + "fire": "Ogenj", + + "stagingArea": "Zbirališče", + + "showAll": "Prikaži vse", + + "nearbyContacts": "Bližnji stiki", + + "locationUnavailable": "Lokacija ni na voljo", + + "ahead": "naravnost", + + "degreesRight": "{degrees}° desno", + + "degreesLeft": "{degrees}° levo", + + "latLonFormat": "Šir: {latitude} Dolž: {longitude}" } diff --git a/lib/providers/connection_provider.dart b/lib/providers/connection_provider.dart index 81daccd..5b7212f 100644 --- a/lib/providers/connection_provider.dart +++ b/lib/providers/connection_provider.dart @@ -76,22 +76,27 @@ class ConnectionProvider with ChangeNotifier { // Helper instances final RoomLoginManager _roomLoginManager = RoomLoginManager(); - final MessageDeliveryTracker _messageDeliveryTracker = MessageDeliveryTracker(); + final MessageDeliveryTracker _messageDeliveryTracker = + MessageDeliveryTracker(); final PingTracker _pingTracker = PingTracker(); // Expose room login states - Map get roomLoginStates => _roomLoginManager.roomLoginStates; + Map get roomLoginStates => + _roomLoginManager.roomLoginStates; // Callbacks for other providers Function(Contact)? onContactReceived; Function(List)? onContactsComplete; Function(Message)? onMessageReceived; Function(Uint8List publicKey, Uint8List lppData)? onTelemetryReceived; - Function(Uint8List publicKeyPrefix, int tag, Uint8List responseData)? onBinaryResponse; + Function(Uint8List publicKeyPrefix, int tag, Uint8List responseData)? + onBinaryResponse; Function(Uint8List publicKey)? onPathUpdated; - Function(Uint8List publicKeyPrefix, int permissions, bool isAdmin, int tag)? onLoginSuccess; + Function(Uint8List publicKeyPrefix, int permissions, bool isAdmin, int tag)? + onLoginSuccess; Function(Uint8List publicKeyPrefix)? onLoginFail; - Function(String messageId, int expectedAckTag, int suggestedTimeoutMs)? onMessageSent; + Function(String messageId, int expectedAckTag, int suggestedTimeoutMs)? + onMessageSent; Function(int ackCode, int roundTripTimeMs)? onMessageDelivered; Function(Uint8List publicKeyPrefix, Uint8List statusData)? onStatusResponse; @@ -106,11 +111,13 @@ class ConnectionProvider with ChangeNotifier { connectionState: isConnected ? ConnectionState.connected : (_bleService.isReconnecting - ? ConnectionState.connecting - : ConnectionState.disconnected), + ? ConnectionState.connecting + : ConnectionState.disconnected), lastUpdate: DateTime.now(), ); - print(' Updated deviceInfo.connectionState: ${_deviceInfo.connectionState}'); + print( + ' Updated deviceInfo.connectionState: ${_deviceInfo.connectionState}', + ); print(' Updated deviceInfo.isConnected: ${_deviceInfo.isConnected}'); print(' isReconnecting: ${_bleService.isReconnecting}'); notifyListeners(); @@ -137,7 +144,9 @@ class ConnectionProvider with ChangeNotifier { connectionState: ConnectionState.error, ); } else { - print(' Keeping connection state as connected (ignoring data parsing error)'); + print( + ' Keeping connection state as connected (ignoring data parsing error)', + ); } notifyListeners(); @@ -165,7 +174,9 @@ class ConnectionProvider with ChangeNotifier { _bleService.onBinaryResponse = (publicKeyPrefix, tag, responseData) { print('📥 [Provider] Binary response received'); - print(' Public key prefix: ${publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}'); + print( + ' Public key prefix: ${publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}', + ); print(' Tag: $tag'); print(' Response data: ${responseData.length} bytes'); // Mark ping as successful if this was a ping request @@ -180,15 +191,20 @@ class ConnectionProvider with ChangeNotifier { }; _bleService.onMessageWaiting = () { - print('📥 [Provider] PUSH_CODE_MSG_WAITING received - auto-fetching messages via event'); + print( + '📥 [Provider] PUSH_CODE_MSG_WAITING received - auto-fetching messages via event', + ); // Automatically fetch messages when push notification received // This is the CORRECT way to receive messages - room server pushes them syncAllMessages(); }; - _bleService.onLoginSuccess = (publicKeyPrefix, permissions, isAdmin, tag) async { + _bleService + .onLoginSuccess = (publicKeyPrefix, permissions, isAdmin, tag) async { print('📥 [Provider] Login successful to room'); - print(' Public key prefix: ${publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}'); + print( + ' Public key prefix: ${publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}', + ); print(' Permissions: $permissions, Admin: $isAdmin, Tag: $tag'); // Update room login state via helper @@ -205,12 +221,12 @@ class ConnectionProvider with ChangeNotifier { _bleService.onLoginFail = (publicKeyPrefix) { print('📥 [Provider] Login failed to room'); - print(' Public key prefix: ${publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}'); + print( + ' Public key prefix: ${publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}', + ); // Update room login state to logged out via helper - _roomLoginManager.handleLoginFail( - publicKeyPrefix: publicKeyPrefix, - ); + _roomLoginManager.handleLoginFail(publicKeyPrefix: publicKeyPrefix); notifyListeners(); onLoginFail?.call(publicKeyPrefix); @@ -218,22 +234,33 @@ class ConnectionProvider with ChangeNotifier { _bleService.onAdvertReceived = (publicKey) { print('📥 [Provider] Advert received from node'); - print(' Public key: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...'); - print(' Note: Waiting for PUSH_CODE_NEW_ADVERT (0x8A) with full contact details'); + print( + ' Public key: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...', + ); + print( + ' Note: Waiting for PUSH_CODE_NEW_ADVERT (0x8A) with full contact details', + ); // The companion radio will automatically send PUSH_CODE_NEW_ADVERT if manual_add_contacts=0 // which will trigger onContactReceived callback and add/update the contact }; _bleService.onPathUpdated = (publicKey) { print('📥 [Provider] Path updated for contact'); - print(' Public key: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...'); - print(' Note: Mesh network discovered a new/better routing path to this contact'); + print( + ' Public key: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...', + ); + print( + ' Note: Mesh network discovered a new/better routing path to this contact', + ); // Forward the callback to ContactsProvider to trigger contact sync onPathUpdated?.call(publicKey); }; - _bleService.onMessageSent = (expectedAckTag, suggestedTimeoutMs, isFloodMode) { - print('📥 [Provider] Message sent - ACK tag: $expectedAckTag, timeout: ${suggestedTimeoutMs}ms'); + _bleService + .onMessageSent = (expectedAckTag, suggestedTimeoutMs, isFloodMode) { + print( + '📥 [Provider] Message sent - ACK tag: $expectedAckTag, timeout: ${suggestedTimeoutMs}ms', + ); // Pop the first pending message ID from the queue (FIFO) via helper final messageId = _messageDeliveryTracker.popPendingMessageId(); @@ -247,18 +274,24 @@ class ConnectionProvider with ChangeNotifier { // Notify callback with message ID onMessageSent?.call(messageId, expectedAckTag, suggestedTimeoutMs); } else { - print('⚠️ [Provider] SENT response received but no pending message IDs'); + print( + '⚠️ [Provider] SENT response received but no pending message IDs', + ); } }; _bleService.onMessageDelivered = (ackCode, roundTripTimeMs) { - print('📥 [Provider] Message delivered - ACK code: $ackCode, RTT: ${roundTripTimeMs}ms'); + print( + '📥 [Provider] Message delivered - ACK code: $ackCode, RTT: ${roundTripTimeMs}ms', + ); onMessageDelivered?.call(ackCode, roundTripTimeMs); }; _bleService.onStatusResponse = (publicKeyPrefix, statusData) { print('📥 [Provider] Status response received from node'); - print(' Public key prefix: ${publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}'); + print( + ' Public key prefix: ${publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}', + ); print(' Status data: ${statusData.length} bytes'); // Forward the callback to whoever needs it (e.g., ContactsProvider) onStatusResponse?.call(publicKeyPrefix, statusData); @@ -289,9 +322,15 @@ class ConnectionProvider with ChangeNotifier { _bleService.onSelfInfoReceived = (selfInfo) { print('📥 [Provider] Received SelfInfo:'); - print(' TX Power: ${selfInfo['txPower']} / ${selfInfo['maxTxPower']} dBm'); - print(' Radio: freq=${selfInfo['radioFreq']}, bw=${selfInfo['radioBw']}, sf=${selfInfo['radioSf']}, cr=${selfInfo['radioCr']}'); - print(' Position: ${selfInfo['advLat'] / 1000000.0}, ${selfInfo['advLon'] / 1000000.0}'); + print( + ' TX Power: ${selfInfo['txPower']} / ${selfInfo['maxTxPower']} dBm', + ); + print( + ' Radio: freq=${selfInfo['radioFreq']}, bw=${selfInfo['radioBw']}, sf=${selfInfo['radioSf']}, cr=${selfInfo['radioCr']}', + ); + print( + ' Position: ${selfInfo['advLat'] / 1000000.0}, ${selfInfo['advLon'] / 1000000.0}', + ); print(' Self Name: ${selfInfo['selfName']}'); _deviceInfo = _deviceInfo.copyWith( @@ -316,7 +355,9 @@ class ConnectionProvider with ChangeNotifier { _bleService.onBatteryAndStorage = (millivolts, usedKb, totalKb) { print('📥 [Provider] Received BatteryAndStorage:'); - print(' Battery: ${millivolts}mV (${(millivolts / 1000.0).toStringAsFixed(2)}V)'); + print( + ' Battery: ${millivolts}mV (${(millivolts / 1000.0).toStringAsFixed(2)}V)', + ); if (usedKb != null) { print(' Storage Used: ${usedKb}KB'); } @@ -362,7 +403,6 @@ class ConnectionProvider with ChangeNotifier { }; _bleService.onRssiUpdate = (rssi) { - print('📡 [Provider] RSSI updated: $rssi dBm'); _deviceInfo = _deviceInfo.copyWith( signalRssi: rssi, lastUpdate: DateTime.now(), @@ -381,25 +421,34 @@ class ConnectionProvider with ChangeNotifier { print('✅ [Provider] Scan state initialized, notifying listeners'); try { - await for (final scanResult - in _bleService.scanForDevices(timeout: const Duration(seconds: 10))) { + await for (final scanResult in _bleService.scanForDevices( + timeout: const Duration(seconds: 10), + )) { print('📱 [Provider] Scan result received from scan stream'); final device = scanResult.device; final rssi = scanResult.rssi; if (!_scannedDevices.any((d) => d.device.remoteId == device.remoteId)) { _scannedDevices.add(ScannedDevice(device: device, rssi: rssi)); - print('✅ [Provider] Added device to list: ${device.platformName} (RSSI: $rssi dBm), total: ${_scannedDevices.length}'); + print( + '✅ [Provider] Added device to list: ${device.platformName} (RSSI: $rssi dBm), total: ${_scannedDevices.length}', + ); notifyListeners(); } else { // Update RSSI if device already exists - final index = _scannedDevices.indexWhere((d) => d.device.remoteId == device.remoteId); + final index = _scannedDevices.indexWhere( + (d) => d.device.remoteId == device.remoteId, + ); if (index != -1 && _scannedDevices[index].rssi != rssi) { _scannedDevices[index] = ScannedDevice(device: device, rssi: rssi); - print(' 🔄 [Provider] Updated RSSI for ${device.platformName}: $rssi dBm'); + print( + ' 🔄 [Provider] Updated RSSI for ${device.platformName}: $rssi dBm', + ); notifyListeners(); } else { - print(' ⏭️ [Provider] Device already in list with same RSSI, skipping'); + print( + ' ⏭️ [Provider] Device already in list with same RSSI, skipping', + ); } } } @@ -426,7 +475,9 @@ class ConnectionProvider with ChangeNotifier { _deviceInfo = _deviceInfo.copyWith( deviceId: device.remoteId.toString(), - deviceName: device.platformName.isNotEmpty ? device.platformName : 'Unknown', + deviceName: device.platformName.isNotEmpty + ? device.platformName + : 'Unknown', connectionState: ConnectionState.connecting, ); _error = null; @@ -457,10 +508,9 @@ class ConnectionProvider with ChangeNotifier { await _bleService.disconnect(); - _deviceInfo = DeviceInfo( - connectionState: ConnectionState.disconnected, - ); - _roomLoginManager.clearRoomLoginStates(); // Clear login states on disconnect + _deviceInfo = DeviceInfo(connectionState: ConnectionState.disconnected); + _roomLoginManager + .clearRoomLoginStates(); // Clear login states on disconnect _pingTracker.clearAll(); // Clear pending pings on disconnect notifyListeners(); } @@ -579,10 +629,7 @@ class ConnectionProvider with ChangeNotifier { } try { - await _bleService.sendChannelMessage( - channelIdx: channelIdx, - text: text, - ); + await _bleService.sendChannelMessage(channelIdx: channelIdx, text: text); // Channel messages are ephemeral (not persisted) - mark as "sent" immediately // They don't have ACK/TAG mechanism like direct messages @@ -622,7 +669,10 @@ class ConnectionProvider with ChangeNotifier { /// deprecated and should continue to be used for telemetry requests. /// /// [zeroHop] - if true, only direct connection (no mesh forwarding) - Future requestTelemetry(Uint8List contactPublicKey, {bool zeroHop = false}) async { + Future requestTelemetry( + Uint8List contactPublicKey, { + bool zeroHop = false, + }) async { if (!_bleService.isConnected) { _error = 'Not connected to device'; notifyListeners(); @@ -684,7 +734,9 @@ class ConnectionProvider with ChangeNotifier { // First attempt timed out - retry with flooding if first was direct if (firstAttemptDirect) { - print('⚠️ [Provider] Ping timeout on direct attempt, retrying with flooding...'); + print( + '⚠️ [Provider] Ping timeout on direct attempt, retrying with flooding...', + ); onRetryWithFlooding?.call(); // Track the retry @@ -708,11 +760,7 @@ class ConnectionProvider with ChangeNotifier { } // First attempt was already flooding and it timed out - return PingResult( - success: false, - usedFlooding: true, - timedOut: true, - ); + return PingResult(success: false, usedFlooding: true, timedOut: true); } catch (e) { _error = 'Failed to ping contact: $e'; notifyListeners(); @@ -1003,14 +1051,19 @@ class ConnectionProvider with ChangeNotifier { // Keep syncing until we get NoMoreMessages response // The device will send ContactMsgRecv or ChannelMsgRecv responses // until it sends NoMoreMessages - for (int i = 0; i < 100; i++) { // Safety limit + for (int i = 0; i < 100; i++) { + // Safety limit // Check flag BEFORE sending (not after) if (_noMoreMessages) { - print('✅ [Provider] Message sync complete - NoMoreMessages flag set after $count requests'); + print( + '✅ [Provider] Message sync complete - NoMoreMessages flag set after $count requests', + ); break; } - print('📤 [Provider] Sync iteration ${i + 1}: Sending CMD_SYNC_NEXT_MESSAGE'); + print( + '📤 [Provider] Sync iteration ${i + 1}: Sending CMD_SYNC_NEXT_MESSAGE', + ); await _bleService.syncNextMessage(); count++; @@ -1022,10 +1075,14 @@ class ConnectionProvider with ChangeNotifier { } if (!_noMoreMessages && count >= 100) { - print('⚠️ [Provider] Message sync stopped - reached safety limit of 100 requests without NoMoreMessages'); + print( + '⚠️ [Provider] Message sync stopped - reached safety limit of 100 requests without NoMoreMessages', + ); } - print('🏁 [Provider] Message sync finished: sent $count sync requests, _noMoreMessages=$_noMoreMessages'); + print( + '🏁 [Provider] Message sync finished: sent $count sync requests, _noMoreMessages=$_noMoreMessages', + ); return count; } catch (e) { print('❌ [Provider] Failed to sync messages: $e'); diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 9dea31f..49533f6 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -78,7 +78,10 @@ class _HomeScreenState extends State if (!connectionProvider.deviceInfo.isConnected) { if (context.mounted) { - ToastLogger.error(context, AppLocalizations.of(context)!.deviceNotConnected); + ToastLogger.error( + context, + AppLocalizations.of(context)!.deviceNotConnected, + ); } return; } @@ -102,7 +105,10 @@ class _HomeScreenState extends State permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { if (context.mounted) { - ToastLogger.error(context, AppLocalizations.of(context)!.locationPermissionDenied); + ToastLogger.error( + context, + AppLocalizations.of(context)!.locationPermissionDenied, + ); } return; } @@ -130,7 +136,10 @@ class _HomeScreenState extends State } catch (e) { print('❌ Failed to get GPS position: $e'); if (context.mounted) { - ToastLogger.error(context, AppLocalizations.of(context)!.failedToGetGpsLocation); + ToastLogger.error( + context, + AppLocalizations.of(context)!.failedToGetGpsLocation, + ); } return; } @@ -159,7 +168,10 @@ class _HomeScreenState extends State } catch (e) { print('❌ Failed to advertise device: $e'); if (context.mounted) { - ToastLogger.error(context, AppLocalizations.of(context)!.failedToAdvertise(e.toString())); + ToastLogger.error( + context, + AppLocalizations.of(context)!.failedToAdvertise(e.toString()), + ); } } } @@ -307,7 +319,9 @@ class _HomeScreenState extends State connectionProvider.startScan(); }, icon: const Icon(Icons.refresh), - label: Text(AppLocalizations.of(context)!.scanAgain), + label: Text( + AppLocalizations.of(context)!.scanAgain, + ), ), ], ), @@ -564,7 +578,10 @@ class _HomeScreenState extends State ), text: AppLocalizations.of(context)!.contacts, ), - Tab(icon: const Icon(Icons.map), text: AppLocalizations.of(context)!.map), + Tab( + icon: const Icon(Icons.map), + text: AppLocalizations.of(context)!.map, + ), ], ), ); @@ -579,10 +596,6 @@ class _HomeScreenState extends State final deviceInfo = provider.deviceInfo; final isConnected = deviceInfo.isConnected; - print( - '🎨 [UI] Building status bar - isConnected: $isConnected, state: ${deviceInfo.connectionState}', - ); - if (!isConnected) { // Disconnected state: show connect button return Row( @@ -590,7 +603,10 @@ class _HomeScreenState extends State Expanded( child: Text( AppLocalizations.of(context)!.appTitle, - style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), ), ), ElevatedButton.icon( @@ -654,8 +670,12 @@ class _HomeScreenState extends State mainAxisSize: MainAxisSize.min, children: [ Text( - deviceInfo.selfName ?? AppLocalizations.of(context)!.appTitle, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + deviceInfo.selfName ?? + AppLocalizations.of(context)!.appTitle, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), overflow: TextOverflow.ellipsis, ), Row( @@ -674,7 +694,9 @@ class _HomeScreenState extends State '${deviceInfo.signalRssi}', style: TextStyle( fontSize: 11, - color: _getSignalColor(deviceInfo.signalRssi!), + color: _getSignalColor( + deviceInfo.signalRssi!, + ), ), ), ], @@ -682,7 +704,9 @@ class _HomeScreenState extends State const SizedBox(width: 8), Icon( _getBatteryIcon(deviceInfo.batteryPercent!), - color: _getBatteryColor(deviceInfo.batteryPercent!), + color: _getBatteryColor( + deviceInfo.batteryPercent!, + ), size: 13, ), const SizedBox(width: 3), @@ -690,7 +714,9 @@ class _HomeScreenState extends State '${deviceInfo.batteryPercent!.round()}%', style: TextStyle( fontSize: 11, - color: _getBatteryColor(deviceInfo.batteryPercent!), + color: _getBatteryColor( + deviceInfo.batteryPercent!, + ), ), ), ], @@ -811,7 +837,9 @@ class _HomeScreenState extends State ), ) else - const SizedBox(width: 52), // Placeholder to maintain layout balance + const SizedBox( + width: 52, + ), // Placeholder to maintain layout balance ], ); }, @@ -842,7 +870,8 @@ class _HomeScreenState extends State Expanded( child: Text( isConnected - ? deviceInfo.displayName ?? AppLocalizations.of(context)!.connect + ? deviceInfo.displayName ?? + AppLocalizations.of(context)!.connect : AppLocalizations.of(context)!.deviceNotConnected, style: Theme.of(context).textTheme.bodyMedium, ), diff --git a/lib/services/ble/ble_connection_manager.dart b/lib/services/ble/ble_connection_manager.dart index 3a76f95..57909ff 100644 --- a/lib/services/ble/ble_connection_manager.dart +++ b/lib/services/ble/ble_connection_manager.dart @@ -5,7 +5,8 @@ import '../meshcore_constants.dart'; /// Callback types for connection events typedef OnConnectionStateCallback = void Function(bool isConnected); typedef OnErrorCallback = void Function(String error); -typedef OnReconnectionAttemptCallback = void Function(int attemptNumber, int maxAttempts); +typedef OnReconnectionAttemptCallback = + void Function(int attemptNumber, int maxAttempts); typedef OnRssiUpdateCallback = void Function(int rssi); /// Manages BLE connection lifecycle with automatic reconnection @@ -30,13 +31,13 @@ class BleConnectionManager { // Pattern: Fast retries first (for temporary issues), then slower retries (for extended disconnections) static const int _maxReconnectionAttempts = 30; static const List _reconnectionDelaysMs = [ - 2000, // 2s - immediate retry - 3000, // 3s - quick retry - 5000, // 5s - fast retry - 10000, // 10s - moderate retry - 15000, // 15s - longer retry - 30000, // 30s - extended retry - 30000, // 30s - keep trying every 30s after this + 2000, // 2s - immediate retry + 3000, // 3s - quick retry + 5000, // 5s - fast retry + 10000, // 10s - moderate retry + 15000, // 15s - longer retry + 30000, // 30s - extended retry + 30000, // 30s - keep trying every 30s after this ]; // Total: ~15 minutes of reconnection attempts // Callbacks @@ -55,7 +56,9 @@ class BleConnectionManager { BluetoothCharacteristic? get txCharacteristic => _txCharacteristic; /// Scan for MeshCore devices - Stream scanForDevices({Duration timeout = const Duration(seconds: 10)}) async* { + Stream scanForDevices({ + Duration timeout = const Duration(seconds: 10), + }) async* { try { print('🔍 [BLE] Starting scan for MeshCore devices...'); print(' Service UUID: ${MeshCoreConstants.bleServiceUuid}'); @@ -69,14 +72,19 @@ class BleConnectionManager { int deviceCount = 0; await for (final scanResult in FlutterBluePlus.scanResults) { - print('📡 [BLE] Scan results batch received: ${scanResult.length} results'); + print( + '📡 [BLE] Scan results batch received: ${scanResult.length} results', + ); for (final result in scanResult) { - print(' Device: ${result.device.platformName} (${result.device.remoteId})'); + print( + ' Device: ${result.device.platformName} (${result.device.remoteId})', + ); print(' RSSI: ${result.rssi}'); print(' Service UUIDs: ${result.advertisementData.serviceUuids}'); - if (result.advertisementData.serviceUuids - .contains(Guid(MeshCoreConstants.bleServiceUuid))) { + if (result.advertisementData.serviceUuids.contains( + Guid(MeshCoreConstants.bleServiceUuid), + )) { deviceCount++; print(' ✅ MeshCore device found! Total: $deviceCount'); yield result; @@ -95,7 +103,9 @@ class BleConnectionManager { /// Connect to a MeshCore device Future connect(BluetoothDevice device) async { try { - print('🔵 [BLE] Starting connection to device: ${device.platformName} (${device.remoteId})'); + print( + '🔵 [BLE] Starting connection to device: ${device.platformName} (${device.remoteId})', + ); _device = device; // Connect to device @@ -121,7 +131,9 @@ class BleConnectionManager { } // Find MeshCore service - print('🔵 [BLE] Looking for MeshCore service: ${MeshCoreConstants.bleServiceUuid}'); + print( + '🔵 [BLE] Looking for MeshCore service: ${MeshCoreConstants.bleServiceUuid}', + ); BluetoothService? meshCoreService; for (final service in services) { if (service.uuid.toString().toLowerCase() == @@ -169,7 +181,8 @@ class BleConnectionManager { print('✅ [BLE] Notifications enabled'); _isConnected = true; - _reconnectionAttempt = 0; // Reset reconnection counter on successful connection + _reconnectionAttempt = + 0; // Reset reconnection counter on successful connection print('🔵 [BLE] Notifying connection state change: connected'); onConnectionStateChanged?.call(true); @@ -213,7 +226,9 @@ class BleConnectionManager { /// Setup connection monitoring for automatic reconnection void _setupConnectionMonitoring() { - print('🔵 [BLE] Setting up connection monitoring for device: ${_device?.platformName}'); + print( + '🔵 [BLE] Setting up connection monitoring for device: ${_device?.platformName}', + ); // Cancel any existing subscription _connectionStateSubscription?.cancel(); @@ -251,21 +266,32 @@ class BleConnectionManager { _isReconnecting = true; _reconnectionAttempt++; - print('🔄 [BLE] Reconnection attempt $_reconnectionAttempt of $_maxReconnectionAttempts'); + print( + '🔄 [BLE] Reconnection attempt $_reconnectionAttempt of $_maxReconnectionAttempts', + ); onReconnectionAttempt?.call(_reconnectionAttempt, _maxReconnectionAttempts); if (_reconnectionAttempt > _maxReconnectionAttempts) { - print('❌ [BLE] Max reconnection attempts reached after ~15 minutes. Giving up.'); + print( + '❌ [BLE] Max reconnection attempts reached after ~15 minutes. Giving up.', + ); _isReconnecting = false; - onError?.call('Connection lost. Unable to reconnect after 15 minutes ($_maxReconnectionAttempts attempts).'); + onError?.call( + 'Connection lost. Unable to reconnect after 15 minutes ($_maxReconnectionAttempts attempts).', + ); return; } // Calculate delay with exponential backoff (uses last delay for attempts beyond array length) - final delayIndex = (_reconnectionAttempt - 1).clamp(0, _reconnectionDelaysMs.length - 1); + final delayIndex = (_reconnectionAttempt - 1).clamp( + 0, + _reconnectionDelaysMs.length - 1, + ); final delayMs = _reconnectionDelaysMs[delayIndex]; - print('🔄 [BLE] Waiting ${(delayMs / 1000).toStringAsFixed(0)}s before reconnection attempt $_reconnectionAttempt...'); + print( + '🔄 [BLE] Waiting ${(delayMs / 1000).toStringAsFixed(0)}s before reconnection attempt $_reconnectionAttempt...', + ); // Wait before attempting reconnection _reconnectionTimer = Timer(Duration(milliseconds: delayMs), () async { @@ -293,7 +319,9 @@ class BleConnectionManager { if (_reconnectionAttempt < _maxReconnectionAttempts) { _attemptReconnection(); } else { - onError?.call('Connection lost. Unable to reconnect after 15 minutes ($_maxReconnectionAttempts attempts).'); + onError?.call( + 'Connection lost. Unable to reconnect after 15 minutes ($_maxReconnectionAttempts attempts).', + ); } } } catch (e) { @@ -304,7 +332,9 @@ class BleConnectionManager { if (_reconnectionAttempt < _maxReconnectionAttempts) { _attemptReconnection(); } else { - onError?.call('Connection lost. Unable to reconnect after 15 minutes: $e'); + onError?.call( + 'Connection lost. Unable to reconnect after 15 minutes: $e', + ); } } }); @@ -338,7 +368,6 @@ class BleConnectionManager { final rssi = await _device!.readRssi(); if (_lastRssi != rssi) { _lastRssi = rssi; - print('📡 [BLE] RSSI updated: $rssi dBm'); onRssiUpdate?.call(rssi); } } catch (e) { diff --git a/lib/widgets/contacts/contact_tile.dart b/lib/widgets/contacts/contact_tile.dart index d41aa3a..e9d3c07 100644 --- a/lib/widgets/contacts/contact_tile.dart +++ b/lib/widgets/contacts/contact_tile.dart @@ -154,7 +154,7 @@ class ContactTile extends StatelessWidget { ), const SizedBox(width: 2), Text( - contact.hasPath ? 'Direct' : 'Flood', + contact.hasPath ? AppLocalizations.of(context)!.direct : AppLocalizations.of(context)!.flood, style: TextStyle( fontSize: 9, fontWeight: FontWeight.w600, @@ -189,7 +189,7 @@ class ContactTile extends StatelessWidget { const Icon(Icons.admin_panel_settings, size: 10, color: Colors.red), const SizedBox(width: 2), Text( - 'Admin', + AppLocalizations.of(context)!.admin, style: Theme.of(context).textTheme.labelSmall?.copyWith( color: Colors.red, fontWeight: FontWeight.bold, @@ -213,7 +213,7 @@ class ContactTile extends StatelessWidget { const Icon(Icons.check_circle, size: 10, color: Colors.green), const SizedBox(width: 2), Text( - 'Logged In', + AppLocalizations.of(context)!.loggedIn, style: Theme.of(context).textTheme.labelSmall?.copyWith( color: Colors.green, fontWeight: FontWeight.bold, @@ -263,7 +263,7 @@ class ContactTile extends StatelessWidget { const Icon(Icons.sensors_off, size: 12, color: Colors.grey), const SizedBox(width: 4), Text( - 'No GPS data', + AppLocalizations.of(context)!.noGpsData, style: Theme.of(context).textTheme.labelSmall, ), ], @@ -277,7 +277,7 @@ class ContactTile extends StatelessWidget { const Icon(Icons.straighten, size: 12, color: Colors.blue), const SizedBox(width: 4), Text( - 'Distance: $distanceText', + '${AppLocalizations.of(context)!.distance}: $distanceText', style: Theme.of(context).textTheme.labelSmall?.copyWith( color: Colors.blue, fontWeight: FontWeight.w500, @@ -300,8 +300,8 @@ class ContactTile extends StatelessWidget { ToastLogger.info( context, hasPath - ? 'Pinging ${contact.displayName} (direct via path)...' - : 'Pinging ${contact.displayName} (flooding - no path)...', + ? AppLocalizations.of(context)!.pingingDirect(contact.displayName) + : AppLocalizations.of(context)!.pingingFlood(contact.displayName), ); // Use smart ping with automatic fallback @@ -313,7 +313,7 @@ class ContactTile extends StatelessWidget { if (context.mounted) { ToastLogger.warning( context, - 'Direct ping timeout - retrying ${contact.displayName} with flooding...', + AppLocalizations.of(context)!.directPingTimeout(contact.displayName), ); } }, @@ -324,12 +324,15 @@ class ContactTile extends StatelessWidget { if (result.success) { ToastLogger.success( context, - 'Ping successful to ${contact.displayName}${result.retriedWithFlooding ? ' (via flooding fallback)' : ''}', + AppLocalizations.of(context)!.pingSuccessful( + contact.displayName, + result.retriedWithFlooding ? AppLocalizations.of(context)!.viaFloodingFallback : '', + ), ); } else { ToastLogger.error( context, - 'Ping failed to ${contact.displayName} - no response received', + AppLocalizations.of(context)!.pingFailed(contact.displayName), ); } } @@ -362,8 +365,7 @@ class ContactTile extends StatelessWidget { builder: (context) => AlertDialog( title: Text(AppLocalizations.of(context)!.deleteContact), content: Text( - 'Are you sure you want to delete "${contact.displayName}"?\n\n' - 'This will remove the contact from both the app and the companion radio device.', + AppLocalizations.of(context)!.deleteContactConfirmation(contact.displayName), ), actions: [ TextButton( @@ -390,7 +392,7 @@ class ContactTile extends StatelessWidget { try { // Show loading toast - ToastLogger.info(context, 'Removing ${contact.displayName}...'); + ToastLogger.info(context, AppLocalizations.of(context)!.removingContact(contact.displayName)); // Remove contact from provider (which will also remove from device) await contactsProvider.removeContact( @@ -403,11 +405,11 @@ class ContactTile extends StatelessWidget { ); if (context.mounted) { - ToastLogger.success(context, 'Contact "${contact.displayName}" removed'); + ToastLogger.success(context, AppLocalizations.of(context)!.contactRemoved(contact.displayName)); } } catch (e) { if (context.mounted) { - ToastLogger.error(context, 'Failed to remove contact: $e'); + ToastLogger.error(context, AppLocalizations.of(context)!.failedToRemoveContact(e.toString())); } } } @@ -483,18 +485,18 @@ class ContactTile extends StatelessWidget { controller: scrollController, padding: const EdgeInsets.all(16), children: [ - _DetailRow('Type', contact.type.displayName), + _DetailRow(AppLocalizations.of(context)!.type, contact.type.displayName), // Public Key with copy button Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - const SizedBox( + SizedBox( width: 100, child: Text( - 'Public Key:', - style: TextStyle(fontWeight: FontWeight.w500), + '${AppLocalizations.of(context)!.publicKey}:', + style: const TextStyle(fontWeight: FontWeight.w500), ), ), Expanded( @@ -524,40 +526,40 @@ class ContactTile extends StatelessWidget { ], ), ), - _DetailRow('Last Seen', contact.timeSinceLastSeen), + _DetailRow(AppLocalizations.of(context)!.lastSeen, contact.timeSinceLastSeen), const SizedBox(height: 16), // Room Login Status if (roomLoginState != null) ...[ - const Text( - 'Room Status:', - style: TextStyle( + Text( + '${AppLocalizations.of(context)!.roomStatus}:', + style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), const SizedBox(height: 8), _DetailRow( - 'Login Status', - roomLoginState.isLoggedIn ? 'Logged In' : 'Not Logged In', + AppLocalizations.of(context)!.loginStatus, + roomLoginState.isLoggedIn ? AppLocalizations.of(context)!.loggedIn : AppLocalizations.of(context)!.notLoggedIn, ), if (roomLoginState.isLoggedIn) ...[ _DetailRow( - 'Admin Access', - roomLoginState.isAdmin ? 'Yes' : 'No', + AppLocalizations.of(context)!.adminAccess, + roomLoginState.isAdmin ? AppLocalizations.of(context)!.yes : AppLocalizations.of(context)!.no, ), _DetailRow( - 'Permissions', + AppLocalizations.of(context)!.permissions, roomLoginState.permissions.toString(), ), if (roomLoginState.loginDurationFormatted != null) _DetailRow( - 'Logged In', + AppLocalizations.of(context)!.loggedIn, roomLoginState.loginDurationFormatted!, ), ], _DetailRow( - 'Password Saved', - roomLoginState.hasPassword ? 'Yes' : 'No', + AppLocalizations.of(context)!.passwordSaved, + roomLoginState.hasPassword ? AppLocalizations.of(context)!.yes : AppLocalizations.of(context)!.no, ), const SizedBox(height: 16), ], @@ -565,9 +567,9 @@ class ContactTile extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( - 'Location:', - style: TextStyle( + Text( + AppLocalizations.of(context)!.locationColon, + style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), @@ -632,9 +634,9 @@ class ContactTile extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( - 'Telemetry:', - style: TextStyle( + Text( + '${AppLocalizations.of(context)!.telemetry}:', + style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), @@ -643,7 +645,7 @@ class ContactTile extends StatelessWidget { onPressed: () { final connectionProvider = context.read(); connectionProvider.requestTelemetry(contact.publicKey, zeroHop: true); - ToastLogger.info(context, 'Requesting telemetry from ${contact.displayName}...'); + ToastLogger.info(context, AppLocalizations.of(context)!.requestingTelemetry(contact.displayName)); }, icon: const Icon(Icons.refresh, size: 18), label: Text(AppLocalizations.of(context)!.refresh), @@ -656,25 +658,25 @@ class ContactTile extends StatelessWidget { const SizedBox(height: 8), if (contact.telemetry!.batteryMilliVolts != null) _DetailRow( - 'Voltage', + AppLocalizations.of(context)!.voltage, '${(contact.telemetry!.batteryMilliVolts! / 1000).toStringAsFixed(3)}V' '${contact.telemetry!.batteryPercentage != null ? ' (${contact.telemetry!.batteryPercentage!.toStringAsFixed(1)}%)' : ''}', ) else if (contact.telemetry!.batteryPercentage != null) - _DetailRow('Battery', '${contact.telemetry!.batteryPercentage!.toStringAsFixed(1)}%'), + _DetailRow(AppLocalizations.of(context)!.battery, '${contact.telemetry!.batteryPercentage!.toStringAsFixed(1)}%'), if (contact.telemetry!.temperature != null) - _DetailRow('Temperature', '${contact.telemetry!.temperature!.toStringAsFixed(1)}°C'), + _DetailRow(AppLocalizations.of(context)!.temperature, '${contact.telemetry!.temperature!.toStringAsFixed(1)}°C'), if (contact.telemetry!.humidity != null) - _DetailRow('Humidity', '${contact.telemetry!.humidity!.toStringAsFixed(1)}%'), + _DetailRow(AppLocalizations.of(context)!.humidity, '${contact.telemetry!.humidity!.toStringAsFixed(1)}%'), if (contact.telemetry!.pressure != null) - _DetailRow('Pressure', '${contact.telemetry!.pressure!.toStringAsFixed(1)} hPa'), + _DetailRow(AppLocalizations.of(context)!.pressure, '${contact.telemetry!.pressure!.toStringAsFixed(1)} hPa'), if (contact.telemetry!.gpsLocation != null) _DetailRow( - 'GPS (Telemetry)', + AppLocalizations.of(context)!.gpsTelemetry, '${contact.telemetry!.gpsLocation!.latitude.toStringAsFixed(6)}, ${contact.telemetry!.gpsLocation!.longitude.toStringAsFixed(6)}', ), _DetailRow( - 'Updated', + AppLocalizations.of(context)!.updated, '${_formatTimestamp(contact.telemetry!.timestamp)} (${_formatTimeAgo(contact.telemetry!.timestamp)})', ), ], @@ -703,7 +705,7 @@ class ContactTile extends StatelessWidget { child: OutlinedButton.icon( onPressed: () { connectionProvider.resetPath(contact.publicKey); - ToastLogger.info(context, 'Path reset for ${contact.displayName}. Next message will find a new route.'); + ToastLogger.info(context, AppLocalizations.of(context)!.pathResetInfo(contact.displayName)); }, icon: const Icon(Icons.route), label: Text(AppLocalizations.of(context)!.resetPath), @@ -726,7 +728,7 @@ class ContactTile extends StatelessWidget { _showRoomLoginDialog(context, contact); }, icon: const Icon(Icons.login), - label: Text(roomLoginState?.isLoggedIn == true ? 'Re-Login to Room' : 'Login to Room'), + label: Text(roomLoginState?.isLoggedIn == true ? AppLocalizations.of(context)!.reLoginToRoom : AppLocalizations.of(context)!.loginToRoom), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 14), backgroundColor: _getTypeColor(contact.type, context), diff --git a/lib/widgets/contacts/room_login_sheet.dart b/lib/widgets/contacts/room_login_sheet.dart index 255a4a8..8a4ed3e 100644 --- a/lib/widgets/contacts/room_login_sheet.dart +++ b/lib/widgets/contacts/room_login_sheet.dart @@ -151,11 +151,7 @@ class _RoomLoginSheetState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text( - 'Failed to add room to device: $e\n\n' - 'The room may not have advertised yet.\n' - 'Try waiting for the room to broadcast.', - ), + content: Text(AppLocalizations.of(context)!.failedToAddRoom(e.toString())), backgroundColor: Theme.of(context).colorScheme.error, duration: const Duration(seconds: 7), ), @@ -316,7 +312,7 @@ class _RoomLoginSheetState extends State { mainAxisSize: MainAxisSize.min, children: [ Text( - 'Login to Room', + AppLocalizations.of(context)!.loginToRoom, style: TextStyle( color: colorScheme.onSurface, fontSize: 18, @@ -358,7 +354,7 @@ class _RoomLoginSheetState extends State { const SizedBox(width: 12), Expanded( child: Text( - 'Enter the password to access this room. The password will be saved for future use.', + AppLocalizations.of(context)!.enterPasswordInfo, style: TextStyle( color: colorScheme.onPrimaryContainer, fontSize: 12, @@ -392,9 +388,9 @@ class _RoomLoginSheetState extends State { maxLengthEnforcement: MaxLengthEnforcement.enforced, style: TextStyle(color: colorScheme.onSurface), decoration: InputDecoration( - labelText: 'Password', + labelText: AppLocalizations.of(context)!.password, labelStyle: TextStyle(color: colorScheme.onSurfaceVariant), - hintText: 'Enter room password', + hintText: AppLocalizations.of(context)!.enterRoomPassword, hintStyle: TextStyle(color: colorScheme.onSurfaceVariant), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), @@ -436,7 +432,7 @@ class _RoomLoginSheetState extends State { child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.login), - label: Text(_isLoggingIn ? 'Logging in...' : 'Login'), + label: Text(_isLoggingIn ? AppLocalizations.of(context)!.loggingInDots : AppLocalizations.of(context)!.login), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 14), ), diff --git a/lib/widgets/map/compass/compass_contact_list.dart b/lib/widgets/map/compass/compass_contact_list.dart index e5fc769..015ce48 100644 --- a/lib/widgets/map/compass/compass_contact_list.dart +++ b/lib/widgets/map/compass/compass_contact_list.dart @@ -1,6 +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 '../../../models/contact.dart'; /// Contact list section for the compass dialog. @@ -28,7 +29,7 @@ class CompassContactList extends StatelessWidget { } if (position == null) { - return const Text('Location unavailable'); + return Text(AppLocalizations.of(context)!.locationUnavailable); } // Calculate bearings and distances @@ -60,13 +61,14 @@ class CompassContactList extends StatelessWidget { contactsWithBearing.sort((a, b) => (a['distance'] as double).compareTo(b['distance'] as double)); + final l10n = AppLocalizations.of(context)!; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(left: 16, bottom: 8), child: Text( - 'Nearby Contacts', + l10n.nearbyContacts, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), @@ -120,7 +122,7 @@ class CompassContactList extends StatelessWidget { ), if (heading != null) Text( - _formatRelativeBearing(bearing, heading!), + _formatRelativeBearing(bearing, heading!, context), style: Theme.of(context).textTheme.labelSmall?.copyWith( color: Colors.grey, ), @@ -189,7 +191,8 @@ class CompassContactList extends StatelessWidget { } } - String _formatRelativeBearing(double bearing, double heading) { + String _formatRelativeBearing(double bearing, double heading, BuildContext context) { + final l10n = AppLocalizations.of(context)!; // Calculate relative bearing (how much to turn from current heading) double relative = bearing - heading; @@ -204,11 +207,11 @@ class CompassContactList extends StatelessWidget { final absRelative = relative.abs().round(); if (absRelative < 10) { - return 'ahead'; + return l10n.ahead; } else if (relative > 0) { - return '$absRelative° right'; + return l10n.degreesRight(absRelative); } else { - return '$absRelative° left'; + return l10n.degreesLeft(absRelative); } } } diff --git a/lib/widgets/map/compass/compass_filters.dart b/lib/widgets/map/compass/compass_filters.dart index af15020..1735008 100644 --- a/lib/widgets/map/compass/compass_filters.dart +++ b/lib/widgets/map/compass/compass_filters.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../../../l10n/app_localizations.dart'; /// Filter controls for the compass dialog. /// Allows filtering of contacts and SAR marker types. @@ -32,15 +33,16 @@ class CompassFilters extends StatefulWidget { class _CompassFiltersState extends State { void _showFilterDialog() { + final l10n = AppLocalizations.of(context)!; showDialog( context: context, builder: (context) => StatefulBuilder( builder: (context, setDialogState) => AlertDialog( - title: const Row( + title: Row( children: [ - Icon(Icons.filter_list, size: 20), - SizedBox(width: 8), - Text('Filter Markers'), + const Icon(Icons.filter_list, size: 20), + const SizedBox(width: 8), + Text(l10n.filterMarkers), ], ), contentPadding: const EdgeInsets.fromLTRB(24, 16, 24, 0), @@ -52,7 +54,7 @@ class _CompassFiltersState extends State { _CompactFilterItem( icon: Icons.person, color: Theme.of(context).colorScheme.primary, - label: 'Contacts', + label: l10n.contactsFilter, value: widget.showContacts, onChanged: (value) { widget.onShowContactsChanged(value); @@ -66,7 +68,7 @@ class _CompassFiltersState extends State { Padding( padding: const EdgeInsets.only(left: 8, bottom: 8, top: 4), child: Text( - 'SAR Markers', + l10n.sarMarkers, style: Theme.of(context).textTheme.labelLarge?.copyWith( fontWeight: FontWeight.bold, ), @@ -75,7 +77,7 @@ class _CompassFiltersState extends State { _CompactFilterItem( icon: Icons.person_pin, color: Colors.green, - label: 'Found Person', + label: l10n.foundPerson, value: widget.showFoundPerson, onChanged: (value) { widget.onShowFoundPersonChanged(value); @@ -86,7 +88,7 @@ class _CompassFiltersState extends State { _CompactFilterItem( icon: Icons.local_fire_department, color: Colors.red, - label: 'Fire', + label: l10n.fire, value: widget.showFire, onChanged: (value) { widget.onShowFireChanged(value); @@ -97,7 +99,7 @@ class _CompassFiltersState extends State { _CompactFilterItem( icon: Icons.home_work, color: Colors.orange, - label: 'Staging Area', + label: l10n.stagingArea, value: widget.showStagingArea, onChanged: (value) { widget.onShowStagingAreaChanged(value); @@ -112,11 +114,11 @@ class _CompassFiltersState extends State { widget.onShowAll(); setDialogState(() {}); }, - child: const Text('Show All'), + child: Text(l10n.showAll), ), TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Close'), + child: Text(l10n.close), ), ], ), @@ -126,9 +128,10 @@ class _CompassFiltersState extends State { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; return IconButton( icon: const Icon(Icons.filter_list), - tooltip: 'Filter markers', + tooltip: l10n.filterMarkersTooltip, onPressed: () => _showFilterDialog(), ); } diff --git a/lib/widgets/map/compass/compass_header.dart b/lib/widgets/map/compass/compass_header.dart index d438d53..2130d12 100644 --- a/lib/widgets/map/compass/compass_header.dart +++ b/lib/widgets/map/compass/compass_header.dart @@ -3,6 +3,7 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'package:latlong2/latlong.dart'; +import '../../../l10n/app_localizations.dart'; import '../../../models/contact.dart'; import '../../../models/sar_marker.dart'; @@ -76,18 +77,19 @@ class CompassHeader extends StatelessWidget { } Widget _buildInfoRow(BuildContext context, double? heading, Position? position) { + final l10n = AppLocalizations.of(context)!; return Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _buildInfoCard( context, - 'Heading', + l10n.heading, heading != null ? '${heading.round()}°' : '--', Icons.explore, ), _buildInfoCard( context, - 'Elevation', + l10n.elevation, position?.altitude != null ? '${position!.altitude.round()}m' : '--', @@ -95,7 +97,7 @@ class CompassHeader extends StatelessWidget { ), _buildInfoCard( context, - 'Accuracy', + l10n.accuracy, position?.accuracy != null ? '±${position!.accuracy.round()}m' : '--', @@ -566,12 +568,16 @@ class _LocationFormatToggleState extends State<_LocationFormatToggle> { return const SizedBox.shrink(); } + final l10n = AppLocalizations.of(context)!; final String displayText; if (_showDMS) { displayText = '${_formatDMS(position.latitude, true)} ${_formatDMS(position.longitude, false)}'; } else { - displayText = 'Lat: ${position.latitude.toStringAsFixed(5)} Lon: ${position.longitude.toStringAsFixed(5)}'; + displayText = l10n.latLonFormat( + position.latitude.toStringAsFixed(5), + position.longitude.toStringAsFixed(5), + ); } return GestureDetector( diff --git a/lib/widgets/map/compass/compass_sar_list.dart b/lib/widgets/map/compass/compass_sar_list.dart index f3d9dbf..439e9c0 100644 --- a/lib/widgets/map/compass/compass_sar_list.dart +++ b/lib/widgets/map/compass/compass_sar_list.dart @@ -1,6 +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 '../../../models/sar_marker.dart'; /// SAR marker list section for the compass dialog. @@ -28,7 +29,7 @@ class CompassSarList extends StatelessWidget { } if (position == null) { - return const Text('Location unavailable'); + return Text(AppLocalizations.of(context)!.locationUnavailable); } // Calculate bearings and distances for SAR markers @@ -58,13 +59,14 @@ class CompassSarList extends StatelessWidget { markersWithBearing.sort((a, b) => (a['distance'] as double).compareTo(b['distance'] as double)); + final l10n = AppLocalizations.of(context)!; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(left: 16, top: 16, bottom: 8), child: Text( - 'SAR Markers', + l10n.sarMarkers, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), @@ -139,7 +141,7 @@ class CompassSarList extends StatelessWidget { ), if (heading != null) Text( - _formatRelativeBearing(bearing, heading!), + _formatRelativeBearing(bearing, heading!, context), style: Theme.of(context).textTheme.labelSmall?.copyWith( color: Colors.grey, ), @@ -208,7 +210,8 @@ class CompassSarList extends StatelessWidget { } } - String _formatRelativeBearing(double bearing, double heading) { + String _formatRelativeBearing(double bearing, double heading, BuildContext context) { + final l10n = AppLocalizations.of(context)!; // Calculate relative bearing (how much to turn from current heading) double relative = bearing - heading; @@ -223,11 +226,11 @@ class CompassSarList extends StatelessWidget { final absRelative = relative.abs().round(); if (absRelative < 10) { - return 'ahead'; + return l10n.ahead; } else if (relative > 0) { - return '$absRelative° right'; + return l10n.degreesRight(absRelative); } else { - return '$absRelative° left'; + return l10n.degreesLeft(absRelative); } } } diff --git a/lib/widgets/messages/sar_update_sheet.dart b/lib/widgets/messages/sar_update_sheet.dart index 59c39ac..01071ae 100644 --- a/lib/widgets/messages/sar_update_sheet.dart +++ b/lib/widgets/messages/sar_update_sheet.dart @@ -176,7 +176,7 @@ class _SarUpdateSheetState extends State { ), ), Text( - 'Quick location marker', + AppLocalizations.of(context)!.quickLocationMarker, style: TextStyle( color: colorScheme.onSurfaceVariant, fontSize: 14, @@ -203,7 +203,7 @@ class _SarUpdateSheetState extends State { children: [ // Marker type selection Text( - 'Marker Type', + AppLocalizations.of(context)!.markerType, style: TextStyle( color: colorScheme.onSurface, fontSize: 16, @@ -238,7 +238,7 @@ class _SarUpdateSheetState extends State { // Destination selection (compact dropdown with rooms and channel) Text( - 'Send To', + AppLocalizations.of(context)!.sendTo, style: TextStyle( color: colorScheme.onSurface, fontSize: 16, @@ -262,14 +262,14 @@ class _SarUpdateSheetState extends State { width: 1, ), ), - child: const Row( + child: Row( children: [ - Icon(Icons.error_outline, color: Colors.red, size: 20), - SizedBox(width: 8), + const Icon(Icons.error_outline, color: Colors.red, size: 20), + const SizedBox(width: 8), Expanded( child: Text( - 'No destinations available.', - style: TextStyle( + AppLocalizations.of(context)!.noDestinationsAvailable, + style: const TextStyle( color: Colors.white70, fontSize: 11, ), @@ -298,7 +298,7 @@ class _SarUpdateSheetState extends State { Icon(Icons.arrow_drop_down_circle, size: 18, color: colorScheme.onSurfaceVariant), const SizedBox(width: 12), Text( - 'Select destination...', + AppLocalizations.of(context)!.selectDestination, style: TextStyle(color: colorScheme.onSurfaceVariant), ), ], @@ -373,8 +373,8 @@ class _SarUpdateSheetState extends State { Expanded( child: Text( _selectedContact!.isChannel - ? 'Ephemeral: Broadcast over-the-air only. Not stored - nodes must be online.' - : 'Persistent: Stored immutably in room. Synced automatically and preserved offline.', + ? AppLocalizations.of(context)!.ephemeralBroadcastInfo + : AppLocalizations.of(context)!.persistentRoomInfo, style: const TextStyle( color: Colors.white70, fontSize: 11, @@ -390,7 +390,7 @@ class _SarUpdateSheetState extends State { Row( children: [ Text( - 'Location', + AppLocalizations.of(context)!.location, style: TextStyle( color: colorScheme.onSurface, fontSize: 16, @@ -409,9 +409,9 @@ class _SarUpdateSheetState extends State { width: 1, ), ), - child: const Text( - 'From Map', - style: TextStyle( + child: Text( + AppLocalizations.of(context)!.fromMap, + style: const TextStyle( color: Colors.blue, fontSize: 11, fontWeight: FontWeight.bold, @@ -441,7 +441,7 @@ class _SarUpdateSheetState extends State { ), const SizedBox(width: 16), Text( - 'Getting location...', + AppLocalizations.of(context)!.gettingLocation, style: TextStyle(color: colorScheme.onSurface), ), ], @@ -466,9 +466,9 @@ class _SarUpdateSheetState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - 'Location Error', - style: TextStyle( + Text( + AppLocalizations.of(context)!.locationError, + style: const TextStyle( color: Colors.red, fontWeight: FontWeight.bold, fontSize: 13, @@ -488,7 +488,7 @@ class _SarUpdateSheetState extends State { IconButton( icon: const Icon(Icons.refresh, color: Colors.red), onPressed: _getCurrentLocation, - tooltip: 'Retry', + tooltip: AppLocalizations.of(context)!.retry, ), ], ), @@ -529,7 +529,7 @@ class _SarUpdateSheetState extends State { onPressed: _getCurrentLocation, padding: EdgeInsets.zero, constraints: const BoxConstraints(), - tooltip: 'Refresh location', + tooltip: AppLocalizations.of(context)!.refreshLocation, ), ], ), @@ -544,7 +544,9 @@ class _SarUpdateSheetState extends State { ), const SizedBox(width: 6), Text( - 'Accuracy: ±${_currentPosition!.accuracy!.round()}m', + AppLocalizations.of(context)!.accuracyMeters( + _currentPosition!.accuracy!.round(), + ), style: TextStyle( fontSize: 12, color: colorScheme.onSurfaceVariant, @@ -560,7 +562,7 @@ class _SarUpdateSheetState extends State { // Optional notes Text( - 'Notes (optional)', + AppLocalizations.of(context)!.notesOptional, style: TextStyle( color: colorScheme.onSurface, fontSize: 16, @@ -574,7 +576,7 @@ class _SarUpdateSheetState extends State { maxLength: 100, style: TextStyle(fontSize: 14, color: colorScheme.onSurface), decoration: InputDecoration( - hintText: 'Add additional information...', + hintText: AppLocalizations.of(context)!.addAdditionalInformation, hintStyle: TextStyle(fontSize: 14, color: colorScheme.onSurfaceVariant), filled: true, fillColor: colorScheme.surfaceContainerHighest, @@ -648,9 +650,9 @@ class _SarUpdateSheetState extends State { builder: (context) => AlertDialog( title: Text(AppLocalizations.of(context)!.lowLocationAccuracy), content: Text( - 'Location accuracy is ±${_currentPosition!.accuracy!.round()}m. ' - 'This may not be accurate enough for SAR operations.\n\n' - 'Continue anyway?', + AppLocalizations.of(context)!.lowAccuracyWarning( + _currentPosition!.accuracy!.round(), + ), ), actions: [ TextButton(