From b7d0db8d1c61b34a223e6290144120943c3c6cd8 Mon Sep 17 00:00:00 2001 From: Serge Tarkovski Date: Sat, 25 Apr 2026 00:29:20 +0300 Subject: [PATCH] Refactor: move Contact UI labels to l10n extension; rename raw getter to typeLabelRaw --- lib/connector/meshcore_connector.dart | 8 +++--- lib/l10n/contact_localization.dart | 36 +++++++++++++++++++++++++ lib/main.dart | 3 +++ lib/models/contact.dart | 32 +++------------------- lib/screens/chat_screen.dart | 3 ++- lib/screens/contacts_screen.dart | 3 ++- lib/screens/map_screen.dart | 3 ++- lib/screens/repeater_hub_screen.dart | 1 + lib/services/background_service.dart | 12 +++++++++ lib/utils/gpx_export.dart | 6 ++--- lib/widgets/path_management_dialog.dart | 1 + lib/widgets/path_selection_dialog.dart | 3 ++- lib/widgets/repeater_login_dialog.dart | 1 + lib/widgets/room_login_dialog.dart | 1 + 14 files changed, 74 insertions(+), 39 deletions(-) create mode 100644 lib/l10n/contact_localization.dart diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index fceee150..ec887eaf 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -3997,7 +3997,7 @@ class MeshCoreConnector extends ChangeNotifier { ); } else { appLogger.info( - "Discovered contact ${contact.name} (type ${contact.typeLabel}) not added due to auto-add settings", + "Discovered contact ${contact.name} (type ${contact.typeLabelRaw}) not added due to auto-add settings", tag: 'Connector', ); return; @@ -4019,7 +4019,7 @@ class MeshCoreConnector extends ChangeNotifier { if (settings.notificationsEnabled && settings.notifyOnNewAdvert) { _notificationService.showAdvertNotification( contactName: contact.name, - contactType: contact.typeLabel, + contactType: contact.typeLabelRaw, contactId: contact.publicKeyHex, ); } @@ -4094,7 +4094,7 @@ class MeshCoreConnector extends ChangeNotifier { if (settings.notificationsEnabled && settings.notifyOnNewAdvert) { _notificationService.showAdvertNotification( contactName: contact.name, - contactType: contact.typeLabel, + contactType: contact.typeLabelRaw, contactId: contact.publicKeyHex, ); } @@ -6025,7 +6025,7 @@ class MeshCoreConnector extends ChangeNotifier { if (settings.notificationsEnabled && settings.notifyOnNewAdvert) { _notificationService.showAdvertNotification( contactName: contact.name, - contactType: contact.typeLabel, + contactType: contact.typeLabelRaw, contactId: contact.publicKeyHex, ); } diff --git a/lib/l10n/contact_localization.dart b/lib/l10n/contact_localization.dart new file mode 100644 index 00000000..d8344a32 --- /dev/null +++ b/lib/l10n/contact_localization.dart @@ -0,0 +1,36 @@ +import '../connector/meshcore_protocol.dart'; +import '../models/contact.dart'; +import 'app_localizations.dart'; + +/// UI-level localization helpers for [Contact]. +/// +/// Kept out of the model layer so `Contact` does not depend on +/// `AppLocalizations`. Use these from widgets/screens; for logs and +/// non-UI export use `Contact.typeLabelRaw`. +extension ContactLocalization on Contact { + String typeLabel(AppLocalizations l10n) { + switch (type) { + case advTypeChat: + return l10n.contact_typeChat; + case advTypeRepeater: + return l10n.contact_typeRepeater; + case advTypeRoom: + return l10n.contact_typeRoom; + case advTypeSensor: + return l10n.contact_typeSensor; + default: + return l10n.contact_typeUnknown; + } + } + + String pathLabel(AppLocalizations l10n) { + if (pathOverride != null) { + if (pathOverride! < 0) return l10n.chat_floodForced; + if (pathOverride == 0) return l10n.chat_directForced; + return l10n.chat_hopsForced(pathOverride!); + } + if (pathLength < 0) return l10n.channelPath_floodPath; + if (pathLength == 0) return l10n.chat_direct; + return l10n.chat_hopsCount(pathLength); + } +} diff --git a/lib/main.dart b/lib/main.dart index 3e57eb10..cd622811 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -59,6 +59,9 @@ void main() async { final notificationService = NotificationService(); await notificationService.initialize(); await backgroundService.initialize(); + backgroundService.setLanguageOverrideProvider( + () => appSettingsService.settings.languageOverride, + ); _registerThirdPartyLicenses(); await chatTextScaleService.initialize(); diff --git a/lib/models/contact.dart b/lib/models/contact.dart index 020e4290..b5df2af1 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -2,7 +2,6 @@ import 'dart:typed_data'; import 'package:meshcore_open/utils/app_logger.dart'; import '../connector/meshcore_protocol.dart'; -import '../l10n/app_localizations.dart'; class Contact { final Uint8List publicKey; @@ -42,7 +41,10 @@ class Contact { String get publicKeyHex => pubKeyToHex(publicKey); - String get typeLabel { + /// Non-localized type label, intended for logs and non-UI exports + /// (e.g. GPX). For UI use the `typeLabel(l10n)` extension in + /// `lib/l10n/contact_localization.dart`. + String get typeLabelRaw { switch (type) { case advTypeChat: return 'Chat'; @@ -57,32 +59,6 @@ class Contact { } } - String typeLabelLocalized(AppLocalizations l10n) { - switch (type) { - case advTypeChat: - return l10n.contact_typeChat; - case advTypeRepeater: - return l10n.contact_typeRepeater; - case advTypeRoom: - return l10n.contact_typeRoom; - case advTypeSensor: - return l10n.contact_typeSensor; - default: - return l10n.contact_typeUnknown; - } - } - - String pathLabel(AppLocalizations l10n) { - if (pathOverride != null) { - if (pathOverride! < 0) return l10n.chat_floodForced; - if (pathOverride == 0) return l10n.chat_directForced; - return l10n.chat_hopsForced(pathOverride!); - } - if (pathLength < 0) return l10n.channelPath_floodPath; - if (pathLength == 0) return l10n.chat_direct; - return l10n.chat_hopsCount(pathLength); - } - bool get hasLocation { const double epsilon = 1e-6; final lat = latitude ?? 0.0; diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 2d5a4483..ec311f84 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -20,6 +20,7 @@ import '../helpers/gif_helper.dart'; import '../helpers/path_helper.dart'; import '../models/channel_message.dart'; import '../models/contact.dart'; +import '../l10n/contact_localization.dart'; import '../models/message.dart'; import '../models/path_history.dart'; import '../models/translation_support.dart'; @@ -1168,7 +1169,7 @@ class _ChatScreenState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildInfoRow(context.l10n.chat_type, contact.typeLabelLocalized(context.l10n)), + _buildInfoRow(context.l10n.chat_type, contact.typeLabel(context.l10n)), _buildInfoRow(context.l10n.chat_path, contact.pathLabel(context.l10n)), _buildInfoRow( context.l10n.contact_lastSeen, diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 5b855cdf..39ccfcd9 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -13,6 +13,7 @@ import '../connector/meshcore_connector.dart'; import '../l10n/l10n.dart'; import '../connector/meshcore_protocol.dart'; import '../models/contact.dart'; +import '../l10n/contact_localization.dart'; import '../models/contact_group.dart'; import '../services/ui_view_state_service.dart'; import '../utils/contact_search.dart'; @@ -1123,7 +1124,7 @@ class _ContactsScreenState extends State value: isSelected, title: Text(contact.name), subtitle: Text( - contact.typeLabelLocalized(context.l10n), + contact.typeLabel(context.l10n), ), onChanged: (value) { setDialogState(() { diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index afc592f2..9d5c20a5 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -16,6 +16,7 @@ import '../connector/meshcore_protocol.dart'; import '../models/app_settings.dart'; import '../models/channel.dart'; import '../models/contact.dart'; +import '../l10n/contact_localization.dart'; import '../services/app_settings_service.dart'; import '../services/path_history_service.dart'; import '../services/map_marker_service.dart'; @@ -1425,7 +1426,7 @@ class _MapScreenState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildInfoRow(context.l10n.map_type, contact.typeLabelLocalized(context.l10n)), + _buildInfoRow(context.l10n.map_type, contact.typeLabel(context.l10n)), _buildInfoRow(context.l10n.map_path, contact.pathLabel(context.l10n)), if (contact.hasLocation) _buildInfoRow( diff --git a/lib/screens/repeater_hub_screen.dart b/lib/screens/repeater_hub_screen.dart index 0fa87cce..5b1e30ca 100644 --- a/lib/screens/repeater_hub_screen.dart +++ b/lib/screens/repeater_hub_screen.dart @@ -3,6 +3,7 @@ import 'package:meshcore_open/connector/meshcore_protocol.dart'; import 'package:provider/provider.dart'; import '../l10n/l10n.dart'; import '../models/contact.dart'; +import '../l10n/contact_localization.dart'; import '../services/app_settings_service.dart'; import 'repeater_status_screen.dart'; import 'repeater_cli_screen.dart'; diff --git a/lib/services/background_service.dart b/lib/services/background_service.dart index 04468eff..45be09b8 100644 --- a/lib/services/background_service.dart +++ b/lib/services/background_service.dart @@ -6,6 +6,14 @@ import '../utils/platform_info.dart'; class BackgroundService { bool _initialized = false; + String? Function()? _languageOverrideProvider; + + /// Allows the app to expose its current language override (e.g. from + /// AppSettingsService) so the foreground notification matches the app UI + /// language instead of only the system locale. + void setLanguageOverrideProvider(String? Function()? provider) { + _languageOverrideProvider = provider; + } Future initialize() async { if (!PlatformInfo.isAndroid || _initialized) return; @@ -47,6 +55,10 @@ class BackgroundService { Future _loadLocalizations() async { final supported = AppLocalizations.supportedLocales; + final override = _languageOverrideProvider?.call(); + if (override != null && override.isNotEmpty) { + return AppLocalizations.delegate.load(Locale(override)); + } final system = WidgetsBinding.instance.platformDispatcher.locale; final match = basicLocaleListResolution( diff --git a/lib/utils/gpx_export.dart b/lib/utils/gpx_export.dart index 296cc3ae..1b82a72d 100644 --- a/lib/utils/gpx_export.dart +++ b/lib/utils/gpx_export.dart @@ -72,7 +72,7 @@ class GpxExport { contact.name, contact.latitude!, contact.longitude!, - "Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}", + "Type: ${contact.typeLabelRaw}\nPublic Key: ${contact.publicKeyHex}", url, ); } @@ -91,7 +91,7 @@ class GpxExport { contact.name, contact.latitude!, contact.longitude!, - "Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}", + "Type: ${contact.typeLabelRaw}\nPublic Key: ${contact.publicKeyHex}", url, ); } @@ -110,7 +110,7 @@ class GpxExport { contact.name, contact.latitude ?? 0.0, contact.longitude ?? 0.0, - "Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}", + "Type: ${contact.typeLabelRaw}\nPublic Key: ${contact.publicKeyHex}", url, ); } diff --git a/lib/widgets/path_management_dialog.dart b/lib/widgets/path_management_dialog.dart index c63ce76e..a2122f46 100644 --- a/lib/widgets/path_management_dialog.dart +++ b/lib/widgets/path_management_dialog.dart @@ -9,6 +9,7 @@ import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; import '../l10n/l10n.dart'; import '../models/contact.dart'; +import '../l10n/contact_localization.dart'; import '../helpers/path_helper.dart'; import '../services/path_history_service.dart'; import '../helpers/snack_bar_builder.dart'; diff --git a/lib/widgets/path_selection_dialog.dart b/lib/widgets/path_selection_dialog.dart index e061e731..44ae58f9 100644 --- a/lib/widgets/path_selection_dialog.dart +++ b/lib/widgets/path_selection_dialog.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:meshcore_open/connector/meshcore_protocol.dart'; import '../l10n/l10n.dart'; import '../models/contact.dart'; +import '../l10n/contact_localization.dart'; import '../helpers/snack_bar_builder.dart'; class PathSelectionDialog extends StatefulWidget { @@ -311,7 +312,7 @@ class _PathSelectionDialogState extends State { style: const TextStyle(fontSize: 14), ), subtitle: Text( - '${contact.typeLabelLocalized(l10n)} • ${contact.publicKeyHex.substring(0, 2)}', + '${contact.typeLabel(l10n)} • ${contact.publicKeyHex.substring(0, 2)}', style: const TextStyle(fontSize: 10), ), trailing: isSelected diff --git a/lib/widgets/repeater_login_dialog.dart b/lib/widgets/repeater_login_dialog.dart index d2cfce09..0973faec 100644 --- a/lib/widgets/repeater_login_dialog.dart +++ b/lib/widgets/repeater_login_dialog.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:provider/provider.dart'; import '../l10n/l10n.dart'; import '../models/contact.dart'; +import '../l10n/contact_localization.dart'; import '../services/storage_service.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; diff --git a/lib/widgets/room_login_dialog.dart b/lib/widgets/room_login_dialog.dart index 1a5ff7fc..2641c023 100644 --- a/lib/widgets/room_login_dialog.dart +++ b/lib/widgets/room_login_dialog.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:provider/provider.dart'; import '../l10n/l10n.dart'; import '../models/contact.dart'; +import '../l10n/contact_localization.dart'; import '../services/storage_service.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart';