From 30bcbedf5efeeb186d3079f2082795df560edaf8 Mon Sep 17 00:00:00 2001 From: 446564 Date: Tue, 20 Jan 2026 17:21:44 -0800 Subject: [PATCH 1/5] update tooltips add missing tooltip: - channels, add channel button - map, filter nodes button --- lib/screens/channels_screen.dart | 1 + lib/screens/map_screen.dart | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 1cb66ab..101828b 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -299,6 +299,7 @@ class _ChannelsScreenState extends State ), floatingActionButton: FloatingActionButton( onPressed: () => _showAddChannelDialog(context), + tooltip: context.l10n.channels_addChannel, child: const Icon(Icons.add), ), bottomNavigationBar: SafeArea( diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 5b804eb..74e5cf9 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -354,6 +354,7 @@ class _MapScreenState extends State { ), floatingActionButton: FloatingActionButton( onPressed: () => _showFilterDialog(context, settingsService), + tooltip: context.l10n.map_filterNodes, child: const Icon(Icons.filter_list), ), ), From 75356fe20d8c079accc0e5ea6b02dab3a41ba010 Mon Sep 17 00:00:00 2001 From: anupoh <41981106+anupoh@users.noreply.github.com> Date: Fri, 23 Jan 2026 16:58:16 +0700 Subject: [PATCH 2/5] Russian translation for the app I've prepared the Russian localization files for the app. It would be great if localization were included in the app. Thanx a lot! --- lib/l10n/app_localizations_ru.dart | 2626 ++++++++++++++++++++++++++++ lib/l10n/app_ru.arb | 761 ++++++++ 2 files changed, 3387 insertions(+) create mode 100644 lib/l10n/app_localizations_ru.dart create mode 100644 lib/l10n/app_ru.arb diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart new file mode 100644 index 0000000..b1e6c1f --- /dev/null +++ b/lib/l10n/app_localizations_ru.dart @@ -0,0 +1,2626 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; +// ignore_for_file: type=lint +/// The translations for Russian (`ru`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsRu([String locale = 'ru']) : super(locale); + + @override + String get appTitle => 'MeshCore Open'; + + @override + String get nav_contacts => 'Контакты'; + + @override + String get nav_channels => 'Каналы'; + + @override + String get nav_map => 'Карта'; + + @override + String get common_cancel => 'Отмена'; + + @override + String get common_ok => 'OK'; + + @override + String get common_connect => 'Коннект'; + + @override + String get common_unknownDevice => 'Неизвестное устройство'; + + @override + String get common_save => 'Сохранить'; + + @override + String get common_delete => 'Удалить'; + + @override + String get common_close => 'Закрыть'; + + @override + String get common_edit => 'Изменить'; + + @override + String get common_add => 'Добавить'; + + @override + String get common_settings => 'Настройки'; + + @override + String get common_disconnect => 'Отключить'; + + @override + String get common_connected => 'Подключено'; + + @override + String get common_disconnected => 'Отключено'; + + @override + String get common_create => 'Создать'; + + @override + String get common_continue => 'Продолжить'; + + @override + String get common_share => 'Поделиться'; + + @override + String get common_copy => 'Копировать'; + + @override + String get common_retry => 'Повторить'; + + @override + String get common_hide => 'Скрыть'; + + @override + String get common_remove => 'Убрать'; + + @override + String get common_enable => 'Включить'; + + @override + String get common_disable => 'Выключить'; + + @override + String get common_reboot => 'Перезагрузить'; + + @override + String get common_loading => 'Загрузка...'; + + @override + String get common_notAvailable => '—'; + + @override + String common_voltageValue(String volts) { + return '$volts В'; +} + + @override + String common_percentValue(int percent) { + return '$percent%'; +} + + @override + String get scanner_title => 'MeshCore Open'; + + @override + String get scanner_scanning => 'Поиск устройств...'; + + @override + String get scanner_connecting => 'Подключение...'; + + @override + String get scanner_disconnecting => 'Отключение...'; + + @override + String get scanner_notConnected => 'Не подключено'; + + @override + String scanner_connectedTo(String deviceName) { + return 'Подключено к $deviceName'; +} + + @override + String get scanner_searchingDevices => 'Поиск устройств MeshCore...'; + + @override + String get scanner_tapToScan => 'Нажмите для поиска MeshCore устройств'; + + @override + String scanner_connectionFailed(String error) { + return 'Подключение не удалось: $error'; +} + + @override + String get scanner_stop => 'Стоп'; + + @override + String get scanner_scan => 'Сканирование'; + + @override + String get device_quickSwitch => 'Быстрое переключение'; + + @override + String get device_meshcore => 'MeshCore'; + + @override + String get settings_title => 'Настройки'; + + @override + String get settings_deviceInfo => 'Информация об устройстве'; + + @override + String get settings_appSettings => 'Настройки приложения'; + + @override + String get settings_appSettingsSubtitle => + 'Уведомления, сообщения и настройки карты'; + + @override + String get settings_nodeSettings => 'Настройки ноды'; + + @override + String get settings_nodeName => 'Имя ноды'; + + @override + String get settings_nodeNameNotSet => 'Не установлено'; + + @override + String get settings_nodeNameHint => 'Введите имя ноды'; + + @override + String get settings_nodeNameUpdated => 'Имя обновлено'; + + @override + String get settings_radioSettings => 'Настройки радио'; + + @override + String get settings_radioSettingsSubtitle => + 'Частота, мощность и коэффициент распространения'; + + @override + String get settings_radioSettingsUpdated => 'Настройки радио обновлены'; + + @override + String get settings_location => 'Позиция'; + + @override + String get settings_locationSubtitle => 'Координаты GPS'; + + @override + String get settings_locationUpdated => 'Позиция и настройки GPS обновлены'; + + @override + String get settings_locationBothRequired => + 'Введите широту и долготу.'; + + @override + String get settings_locationInvalid => 'Неверная широта или долгота.'; + + @override + String get settings_locationGPSEnable => 'Включить GPS'; + + @override + String get settings_locationGPSEnableSubtitle => + 'Включение GPS для автоматического обновления позиции.'; + + @override + String get settings_locationIntervalSec => 'Интервал для позиционирования GPS (секунды)'; + + @override + String get settings_locationIntervalInvalid => + 'Интервал должен составлять не менее 60 секунд и не более 86400 секунд.'; + + @override + String get settings_latitude => 'Широта'; + + @override + String get settings_longitude => 'Долгота'; + + @override + String get settings_privacyMode => 'Режим конфиденциальности'; + + @override + String get settings_privacyModeSubtitle => + 'Скрыть имя/позицию в анонсировании'; + + @override + String get settings_privacyModeToggle => + 'Включите режим конфиденциальности, чтобы скрыть свое имя и местоположение в анонсировании.'; + + @override + String get settings_privacyModeEnabled => 'Режим конфиденциальности включен'; + + @override + String get settings_privacyModeDisabled => 'Режим конфиденциальности выключен'; + + @override + String get settings_actions => 'Действия'; + + @override + String get settings_sendAdvertisement => 'Отправить анонсирование'; + + @override + String get settings_sendAdvertisementSubtitle => 'Отправить анонсирование о присутствии сейчас'; + + @override + String get settings_advertisementSent => 'Анонсирование отправлено'; + + @override + String get settings_syncTime => 'Синхронизация времени'; + + @override + String get settings_syncTimeSubtitle => 'Синхронизировать время с телефоном'; + + @override + String get settings_timeSynchronized => 'Время синхронизировано'; + + @override + String get settings_refreshContacts => 'Обновить контакты'; + + @override + String get settings_refreshContactsSubtitle => + 'Перезагрузить список контактов с устройства'; + + @override + String get settings_rebootDevice => 'Перезагрузить устройство'; + + @override + String get settings_rebootDeviceSubtitle => 'Перезапустить устройство MeshCore'; + + @override + String get settings_rebootDeviceConfirm => + 'Вы уверены, что хотите перезагрузить устройство? Вы будете отключены.'; + + @override + String get settings_debug => 'Отладка'; + + @override + String get settings_bleDebugLog => 'Журнал отладки BLE'; + + @override + String get settings_bleDebugLogSubtitle => + 'Команды BLE, ответы и сырые данные'; + + @override + String get settings_appDebugLog => 'Журнал отладки приложения'; + + @override + String get settings_appDebugLogSubtitle => 'Сообщения отладки приложения'; + + @override + String get settings_about => 'О программе'; + + @override + String settings_aboutVersion(String version) { + return 'MeshCore Open v$version'; +} + + @override + String get settings_aboutLegalese => '2026 MeshCore Open Source Project'; + + @override + String get settings_aboutDescription => + 'Открытое клиентское приложение на Flutter для устройств MeshCore с LoRa-сетями.'; + + @override + String get settings_infoName => 'Имя'; + + @override + String get settings_infoId => 'ID'; + + @override + String get settings_infoStatus => 'Статус'; + + @override + String get settings_infoBattery => 'Батарея'; + + @override + String get settings_infoPublicKey => 'Публичный ключ'; + + @override + String get settings_infoContactsCount => 'Количество контактов'; + + @override + String get settings_infoChannelCount => 'Количество каналов'; + + @override + String get settings_presets => 'Пресеты'; + + @override + String get settings_preset915Mhz => '915 МГц'; + + @override + String get settings_preset868Mhz => '868 МГц'; + + @override + String get settings_preset433Mhz => '433 МГц'; + + @override + String get settings_frequency => 'Частота (МГц)'; + + @override + String get settings_frequencyHelper => '300.0 – 2500.0'; + + @override + String get settings_frequencyInvalid => 'Недопустимая частота (300–2500 МГц)'; + + @override + String get settings_bandwidth => 'Полоса пропускания'; + + @override + String get settings_spreadingFactor => 'Коэффициент расширения'; + + @override + String get settings_codingRate => 'Коэффициент кодирования'; + + @override + String get settings_txPower => 'Мощность передачи (дБм)'; + + @override + String get settings_txPowerHelper => '0 – 22'; + + @override + String get settings_txPowerInvalid => 'Недопустимая мощность передачи (0–22 дБм)'; + + @override + String get settings_longRange => 'Дальний радиус'; + + @override + String get settings_fastSpeed => 'Высокая скорость'; + + @override + String settings_error(String message) { + return 'Ошибка: $message'; +} + + @override + String get appSettings_title => 'Настройки приложения'; + + @override + String get appSettings_appearance => 'Внешний вид'; + + @override + String get appSettings_theme => 'Тема'; + + @override + String get appSettings_themeSystem => 'Как в системе'; + + @override + String get appSettings_themeLight => 'Светлая'; + + @override + String get appSettings_themeDark => 'Тёмная'; + + @override + String get appSettings_language => 'Язык'; + + @override + String get appSettings_languageSystem => 'Как в системе'; + + @override + String get appSettings_languageEn => 'Английский'; + + @override + String get appSettings_languageFr => 'Французский'; + + @override + String get appSettings_languageEs => 'Испанский'; + + @override + String get appSettings_languageDe => 'Немецкий'; + + @override + String get appSettings_languagePl => 'Польский'; + + @override + String get appSettings_languageSl => 'Словенский'; + + @override + String get appSettings_languagePt => 'Португальский'; + + @override + String get appSettings_languageIt => 'Итальянский'; + + @override + String get appSettings_languageZh => 'Китайский'; + + @override + String get appSettings_languageSv => 'Шведский'; + + @override + String get appSettings_languageNl => 'Нидерландский'; + + @override + String get appSettings_languageSk => 'Словацкий'; + + @override + String get appSettings_languageBg => 'Болгарский'; + + @override + String get appSettings_languageRu => 'Русский'; + + @override + String get appSettings_notifications => 'Уведомления'; + + @override + String get appSettings_enableNotifications => 'Включить уведомления'; + + @override + String get appSettings_enableNotificationsSubtitle => + 'Получать уведомления о сообщениях и оповещениях'; + + @override + String get appSettings_notificationPermissionDenied => + 'Разрешение на уведомления отклонено'; + + @override + String get appSettings_notificationsEnabled => 'Уведомления включены'; + + @override + String get appSettings_notificationsDisabled => 'Уведомления отключены'; + + @override + String get appSettings_messageNotifications => 'Уведомления о сообщениях'; + + @override + String get appSettings_messageNotificationsSubtitle => + 'Показывать уведомление при получении новых сообщений'; + + @override + String get appSettings_channelMessageNotifications => + 'Уведомления о сообщениях в каналах'; + + @override + String get appSettings_channelMessageNotificationsSubtitle => + 'Показывать уведомление при получении сообщений в каналах'; + + @override + String get appSettings_advertisementNotifications => + 'Уведомления об анонсированиях'; + + @override + String get appSettings_advertisementNotificationsSubtitle => + 'Показывать уведомление при обнаружении новых нод'; + + @override + String get appSettings_messaging => 'Обмен сообщениями'; + + @override + String get appSettings_clearPathOnMaxRetry => 'Сбросить маршрут после максимального числа попыток'; + + @override + String get appSettings_clearPathOnMaxRetrySubtitle => + 'Сбросить маршрут контакта после 5 неудачных попыток отправки'; + + @override + String get appSettings_pathsWillBeCleared => + 'Маршруты будут сброшены после 5 неудачных попыток'; + + @override + String get appSettings_pathsWillNotBeCleared => + 'Маршруты не будут автоматически сбрасываться'; + + @override + String get appSettings_autoRouteRotation => 'Автоматическое переключение маршрутов'; + + @override + String get appSettings_autoRouteRotationSubtitle => + 'Циклически переключаться между лучшими маршрутами и режимом рассылки'; + + @override + String get appSettings_autoRouteRotationEnabled => + 'Автоматическое переключение маршрутов включено'; + + @override + String get appSettings_autoRouteRotationDisabled => + 'Автоматическое переключение маршрутов отключено'; + + @override + String get appSettings_battery => 'Батарея'; + + @override + String get appSettings_batteryChemistry => 'Химия батареи'; + + @override + String appSettings_batteryChemistryPerDevice(String deviceName) { + return 'Установить для устройства ($deviceName)'; +} + + @override + String get appSettings_batteryChemistryConnectFirst => + 'Подключитесь к устройству, чтобы выбрать'; + + @override + String get appSettings_batteryNmc => '18650 NMC (3.0–4.2 В)'; + + @override + String get appSettings_batteryLifepo4 => 'LiFePO4 (2.6–3.65 В)'; + + @override + String get appSettings_batteryLipo => 'LiPo (3.0–4.2 В)'; + + @override + String get appSettings_mapDisplay => 'Отображение карты'; + + @override + String get appSettings_showRepeaters => 'Показывать репитеры'; + + @override + String get appSettings_showRepeatersSubtitle => + 'Отображать репитеры на карте'; + + @override + String get appSettings_showChatNodes => 'Показывать чат-ноды'; + + @override + String get appSettings_showChatNodesSubtitle => + 'Отображать чат-ноды на карте'; + + @override + String get appSettings_showOtherNodes => 'Показывать другие ноды'; + + @override + String get appSettings_showOtherNodesSubtitle => + 'Отображать другие типы нод на карте'; + + @override + String get appSettings_timeFilter => 'Фильтр по времени'; + + @override + String get appSettings_timeFilterShowAll => 'Показывать все ноды'; + + @override + String appSettings_timeFilterShowLast(int hours) { + return 'Показывать ноды за последние $hours ч'; +} + + @override + String get appSettings_mapTimeFilter => 'Временной фильтр карты'; + + @override + String get appSettings_showNodesDiscoveredWithin => + 'Показывать ноды, обнаруженные за:'; + + @override + String get appSettings_allTime => 'Всё время'; + + @override + String get appSettings_lastHour => 'Последний час'; + + @override + String get appSettings_last6Hours => 'Последние 6 часов'; + + @override + String get appSettings_last24Hours => 'Последние 24 часа'; + + @override + String get appSettings_lastWeek => 'Последнюю неделю'; + + @override + String get appSettings_offlineMapCache => 'Кэш офлайн-карты'; + + @override + String get appSettings_noAreaSelected => 'Область не выбрана'; + + @override + String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { + return 'Область выбрана (масштаб $minZoom–$maxZoom)'; +} + + @override + String get appSettings_debugCard => 'Отладка'; + + @override + String get appSettings_appDebugLogging => 'Журнал отладки приложения'; + + @override + String get appSettings_appDebugLoggingSubtitle => + 'Записывать отладочные сообщения приложения для диагностики'; + + @override + String get appSettings_appDebugLoggingEnabled => 'Журнал отладки приложения включён'; + + @override + String get appSettings_appDebugLoggingDisabled => + 'Журнал отладки приложения отключён'; + + @override + String get contacts_title => 'Контакты'; + + @override + String get contacts_noContacts => 'Контактов пока нет'; + + @override + String get contacts_contactsWillAppear => + 'Контакты появятся, когда устройства начнут рассылать оповещения'; + + @override + String get contacts_searchContacts => 'Поиск контактов...'; + + @override + String get contacts_noUnreadContacts => 'Нет непрочитанных контактов'; + + @override + String get contacts_noContactsFound => 'Контакты или группы не найдены'; + + @override + String get contacts_deleteContact => 'Удалить контакт'; + + @override + String contacts_removeConfirm(String contactName) { + return 'Удалить $contactName из контактов?'; +} + + @override + String get contacts_manageRepeater => 'Управление репитером'; + + @override + String get contacts_manageRoom => 'Управление сервером комнат'; + + @override + String get contacts_roomLogin => 'Вход на сервер комнат'; + + @override + String get contacts_openChat => 'Открыть чат'; + + @override + String get contacts_editGroup => 'Изменить группу'; + + @override + String get contacts_deleteGroup => 'Удалить группу'; + + @override + String contacts_deleteGroupConfirm(String groupName) { + return 'Удалить \"$groupName\"?'; +} + + @override + String get contacts_newGroup => 'Новая группа'; + + @override + String get contacts_groupName => 'Имя группы'; + + @override + String get contacts_groupNameRequired => 'Имя группы обязательно'; + + @override + String contacts_groupAlreadyExists(String name) { + return 'Группа \"$name\" уже существует'; +} + + @override + String get contacts_filterContacts => 'Фильтр контактов...'; + + @override + String get contacts_noContactsMatchFilter => 'Нет контактов, соответствующих фильтру'; + + @override + String get contacts_noMembers => 'Нет участников'; + + @override + String get contacts_lastSeenNow => 'Видели только что'; + + @override + String contacts_lastSeenMinsAgo(int minutes) { + return 'Видели $minutes мин назад'; +} + + @override + String get contacts_lastSeenHourAgo => 'Видели 1 час назад'; + + @override + String contacts_lastSeenHoursAgo(int hours) { + return 'Видели $hours ч назад'; +} + + @override + String get contacts_lastSeenDayAgo => 'Видели 1 день назад'; + + @override + String contacts_lastSeenDaysAgo(int days) { + return 'Видели $days дн. назад'; +} + + @override + String get channels_title => 'Каналы'; + + @override + String get channels_noChannelsConfigured => 'Каналы не настроены'; + + @override + String get channels_addPublicChannel => 'Добавить публичный канал'; + + @override + String get channels_searchChannels => 'Поиск каналов...'; + + @override + String get channels_noChannelsFound => 'Каналы не найдены'; + + @override + String channels_channelIndex(int index) { + return 'Канал $index'; +} + + @override + String get channels_hashtagChannel => 'Хэштег-канал'; + + @override + String get channels_public => 'Публичный'; + + @override + String get channels_private => 'Приватный'; + + @override + String get channels_publicChannel => 'Публичный канал'; + + @override + String get channels_privateChannel => 'Приватный канал'; + + @override + String get channels_editChannel => 'Изменить канал'; + + @override + String get channels_deleteChannel => 'Удалить канал'; + + @override + String channels_deleteChannelConfirm(String name) { + return 'Удалить \"$name\"? Это действие нельзя отменить.'; +} + + @override + String channels_channelDeleted(String name) { + return 'Канал \"$name\" удалён'; +} + + @override + String get channels_addChannel => 'Добавить канал'; + + @override + String get channels_channelIndexLabel => 'Индекс канала'; + + @override + String get channels_channelName => 'Имя канала'; + + @override + String get channels_usePublicChannel => 'Использовать публичный канал'; + + @override + String get channels_standardPublicPsk => 'Стандартный публичный PSK'; + + @override + String get channels_pskHex => 'PSK (Hex)'; + + @override + String get channels_generateRandomPsk => 'Сгенерировать случайный PSK'; + + @override + String get channels_enterChannelName => 'Введите имя канала'; + + @override + String get channels_pskMustBe32Hex => 'PSK должен содержать 32 шестнадцатеричных символа'; + + @override + String channels_channelAdded(String name) { + return 'Канал \"$name\" добавлен'; +} + + @override + String channels_editChannelTitle(int index) { + return 'Изменить канал $index'; +} + + @override + String get channels_smazCompression => 'Сжатие SMAZ'; + + @override + String channels_channelUpdated(String name) { + return 'Канал \"$name\" обновлён'; +} + + @override + String get channels_publicChannelAdded => 'Публичный канал добавлен'; + + @override + String get channels_sortBy => 'Сортировка'; + + @override + String get channels_sortManual => 'Вручную'; + + @override + String get channels_sortAZ => 'По алфавиту'; + + @override + String get channels_sortLatestMessages => 'По последним сообщениям'; + + @override + String get channels_sortUnread => 'По непрочитанным'; + + @override + String get channels_createPrivateChannel => 'Создать приватный канал'; + + @override + String get channels_createPrivateChannelDesc => 'Защищён секретным ключом.'; + + @override + String get channels_joinPrivateChannel => 'Присоединиться к приватному каналу'; + + @override + String get channels_joinPrivateChannelDesc => 'Введите секретный ключ вручную.'; + + @override + String get channels_joinPublicChannel => 'Присоединиться к публичному каналу'; + + @override + String get channels_joinPublicChannelDesc => 'К этому каналу может присоединиться любой.'; + + @override + String get channels_joinHashtagChannel => 'Присоединиться к хэштег-каналу'; + + @override + String get channels_joinHashtagChannelDesc => + 'К хэштег-каналам может присоединиться любой.'; + + @override + String get channels_scanQrCode => 'Сканировать QR-код'; + + @override + String get channels_scanQrCodeComingSoon => 'Скоро будет'; + + @override + String get channels_enterHashtag => 'Введите хэштег'; + + @override + String get channels_hashtagHint => 'например, #команда'; + + @override + String get chat_noMessages => 'Сообщений пока нет'; + + @override + String get chat_sendMessageToStart => 'Отправьте сообщение, чтобы начать'; + + @override + String get chat_originalMessageNotFound => 'Исходное сообщение не найдено'; + + @override + String chat_replyingTo(String name) { + return 'Ответ для $name'; +} + + @override + String chat_replyTo(String name) { + return 'Ответить $name'; +} + + @override + String get chat_location => 'Местоположение'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Отправить сообщение $contactName'; +} + + @override + String get chat_typeMessage => 'Напишите сообщение...'; + + @override + String chat_messageTooLong(int maxBytes) { + return 'Сообщение слишком длинное (макс. $maxBytes байт).'; +} + + @override + String get chat_messageCopied => 'Сообщение скопировано'; + + @override + String get chat_messageDeleted => 'Сообщение удалено'; + + @override + String get chat_retryingMessage => 'Повтор отправки сообщения'; + + @override + String chat_retryCount(int current, int max) { + return 'Попытка $current/$max'; +} + + @override + String get chat_sendGif => 'Отправить GIF'; + + @override + String get chat_reply => 'Ответить'; + + @override + String get chat_addReaction => 'Добавить реакцию'; + + @override + String get chat_me => 'Я'; + + @override + String get emojiCategorySmileys => 'Смайлы'; + + @override + String get emojiCategoryGestures => 'Жесты'; + + @override + String get emojiCategoryHearts => 'Сердечки'; + + @override + String get emojiCategoryObjects => 'Предметы'; + + @override + String get gifPicker_title => 'Выберите GIF'; + + @override + String get gifPicker_searchHint => 'Поиск GIF...'; + + @override + String get gifPicker_poweredBy => 'Работает на GIPHY'; + + @override + String get gifPicker_noGifsFound => 'GIF не найдены'; + + @override + String get gifPicker_failedLoad => 'Не удалось загрузить GIF'; + + @override + String get gifPicker_failedSearch => 'Не удалось выполнить поиск GIF'; + + @override + String get gifPicker_noInternet => 'Нет подключения к интернету'; + + @override + String get debugLog_appTitle => 'Журнал отладки приложения'; + + @override + String get debugLog_bleTitle => 'Журнал отладки BLE'; + + @override + String get debugLog_copyLog => 'Копировать журнал'; + + @override + String get debugLog_clearLog => 'Очистить журнал'; + + @override + String get debugLog_copied => 'Журнал отладки скопирован'; + + @override + String get debugLog_bleCopied => 'Журнал BLE скопирован'; + + @override + String get debugLog_noEntries => 'Журнал отладки пока пуст'; + + @override + String get debugLog_enableInSettings => + 'Включите запись журнала отладки в настройках'; + + @override + String get debugLog_frames => 'Фреймы'; + + @override + String get debugLog_rawLogRx => 'Сырой журнал приёма'; + + @override + String get debugLog_noBleActivity => 'Активность BLE пока отсутствует'; + + @override + String debugFrame_length(int count) { + return 'Длина фрейма: $count байт'; +} + + @override + String debugFrame_command(String value) { + return 'Команда: 0x$value'; +} + + @override + String get debugFrame_textMessageHeader => 'Фрейм текстового сообщения:'; + + @override + String debugFrame_destinationPubKey(String pubKey) { + return '- Публичный ключ получателя: $pubKey'; +} + + @override + String debugFrame_timestamp(int timestamp) { + return '- Временная метка: $timestamp'; +} + + @override + String debugFrame_flags(String value) { + return '- Флаги: 0x$value'; +} + + @override + String debugFrame_textType(int type, String label) { + return '- Тип текста: $type ($label)'; +} + + @override + String get debugFrame_textTypeCli => 'CLI'; + + @override + String get debugFrame_textTypePlain => 'Обычный'; + + @override + String debugFrame_text(String text) { + return '- Текст: \"$text\"'; +} + + @override + String get debugFrame_hexDump => 'Шестнадцатеричный дамп:'; + + @override + String get chat_pathManagement => 'Управление маршрутами'; + + @override + String get chat_routingMode => 'Режим маршрутизации'; + + @override + String get chat_autoUseSavedPath => 'Авто (использовать сохранённый маршрут)'; + + @override + String get chat_forceFloodMode => 'Принудительный режим рассылки'; + + @override + String get chat_recentAckPaths => 'Недавние подтверждённые маршруты (нажмите, чтобы использовать):'; + + @override + String get chat_pathHistoryFull => + 'История маршрутов заполнена. Удалите записи, чтобы добавить новые.'; + + @override + String get chat_hopSingular => 'хоп'; + + @override + String get chat_hopPlural => 'хопов'; + + @override + String chat_hopsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( +count, +locale: localeName, +other: 'хопов', +one: 'хоп', +); + return '$count $_temp0'; +} + + @override + String get chat_successes => 'успешно'; + + @override + String get chat_removePath => 'Удалить маршрут'; + + @override + String get chat_noPathHistoryYet => + 'История маршрутов пока пуста. +Отправьте сообщение, чтобы обнаружить маршруты.'; + + @override + String get chat_pathActions => 'Действия с маршрутом:'; + + @override + String get chat_setCustomPath => 'Указать маршрут вручную'; + + @override + String get chat_setCustomPathSubtitle => 'Вручную задать маршрут передачи'; + + @override + String get chat_clearPath => 'Очистить маршрут'; + + @override + String get chat_clearPathSubtitle => 'Принудительно обновить маршрут при следующей отправке'; + + @override + String get chat_pathCleared => + 'Маршрут очищен. Следующее сообщение обновит маршрут.'; + + @override + String get chat_floodModeSubtitle => 'Используйте переключатель маршрутизации в панели приложения'; + + @override + String get chat_floodModeEnabled => + 'Режим рассылки включён. Отключите через значок маршрутизации в панели приложения.'; + + @override + String get chat_fullPath => 'Полный маршрут'; + + @override + String get chat_pathDetailsNotAvailable => + 'Детали маршрута ещё недоступны. Попробуйте отправить сообщение для обновления.'; + + @override + String chat_pathSetHops(int hopCount, String status) { + String _temp0 = intl.Intl.pluralLogic( +hopCount, +locale: localeName, +other: 'хопов', +one: 'хоп', +); + return 'Маршрут установлен: $hopCount $_temp0 — $status'; +} + + @override + String get chat_pathSavedLocally => 'Сохранено локально. Подключитесь для синхронизации.'; + + @override + String get chat_pathDeviceConfirmed => 'Подтверждено устройством.'; + + @override + String get chat_pathDeviceNotConfirmed => 'Ещё не подтверждено устройством.'; + + @override + String get chat_type => 'Тип'; + + @override + String get chat_path => 'Маршрут'; + + @override + String get chat_publicKey => 'Публичный ключ'; + + @override + String get chat_compressOutgoingMessages => 'Сжимать исходящие сообщения'; + + @override + String get chat_floodForced => 'Рассылка (принудительно)'; + + @override + String get chat_directForced => 'Прямой (принудительно)'; + + @override + String chat_hopsForced(int count) { + return '$count хоп(ов) (принудительно)'; +} + + @override + String get chat_floodAuto => 'Рассылка (авто)'; + + @override + String get chat_direct => 'Прямой'; + + @override + String get chat_poiShared => 'Точка интереса отправлена'; + + @override + String chat_unread(int count) { + return 'Непрочитанных: $count'; +} + + @override + String get map_title => 'Карта нод'; + + @override + String get map_noNodesWithLocation => 'Нет нод с данными о местоположении'; + + @override + String get map_nodesNeedGps => + 'Ноды должны передавать свои GPS-координаты, чтобы отображаться на карте'; + + @override + String map_nodesCount(int count) { + return 'Нод: $count'; +} + + @override + String map_pinsCount(int count) { + return 'Меток: $count'; +} + + @override + String get map_chat => 'Чат'; + + @override + String get map_repeater => 'Репитер'; + + @override + String get map_room => 'Комната'; + + @override + String get map_sensor => 'Сенсор'; + + @override + String get map_pinDm => 'Метка (ЛС)'; + + @override + String get map_pinPrivate => 'Метка (Приватная)'; + + @override + String get map_pinPublic => 'Метка (Публичная)'; + + @override + String get map_lastSeen => 'Последнее появление'; + + @override + String get map_disconnectConfirm => + 'Вы уверены, что хотите отключиться от этого устройства?'; + + @override + String get map_from => 'От'; + + @override + String get map_source => 'Источник'; + + @override + String get map_flags => 'Флаги'; + + @override + String get map_shareMarkerHere => 'Поделиться меткой здесь'; + + @override + String get map_pinLabel => 'Метка'; + + @override + String get map_label => 'Подпись'; + + @override + String get map_pointOfInterest => 'Точка интереса'; + + @override + String get map_sendToContact => 'Отправить контакту'; + + @override + String get map_sendToChannel => 'Отправить в канал'; + + @override + String get map_noChannelsAvailable => 'Нет доступных каналов'; + + @override + String get map_publicLocationShare => 'Публичная передача местоположения'; + + @override + String map_publicLocationShareConfirm(String channelLabel) { + return 'Вы собираетесь поделиться местоположением в $channelLabel. Этот канал публичный, и любой, у кого есть PSK, сможет его увидеть.'; +} + + @override + String get map_connectToShareMarkers => + 'Подключитесь к устройству, чтобы делиться метками'; + + @override + String get map_filterNodes => 'Фильтр нод'; + + @override + String get map_nodeTypes => 'Типы нод'; + + @override + String get map_chatNodes => 'Чат-ноды'; + + @override + String get map_repeaters => 'Репитеры'; + + @override + String get map_otherNodes => 'Другие ноды'; + + @override + String get map_keyPrefix => 'Префикс ключа'; + + @override + String get map_filterByKeyPrefix => 'Фильтр по префиксу ключа'; + + @override + String get map_publicKeyPrefix => 'Префикс публичного ключа'; + + @override + String get map_markers => 'Метки'; + + @override + String get map_showSharedMarkers => 'Показывать общие метки'; + + @override + String get map_lastSeenTime => 'Время последнего появления'; + + @override + String get map_sharedPin => 'Общая метка'; + + @override + String get map_joinRoom => 'Присоединиться к комнате'; + + @override + String get map_manageRepeater => 'Управление репитером'; + + @override + String get mapCache_title => 'Кэш офлайн-карты'; + + @override + String get mapCache_selectAreaFirst => 'Сначала выберите область для кэширования'; + + @override + String get mapCache_noTilesToDownload => 'Нет плиток для загрузки в этой области'; + + @override + String get mapCache_downloadTilesTitle => 'Загрузить плитки'; + + @override + String mapCache_downloadTilesPrompt(int count) { + return 'Загрузить $count плиток для офлайн-использования?'; +} + + @override + String get mapCache_downloadAction => 'Загрузить'; + + @override + String mapCache_cachedTiles(int count) { + return 'Закэшировано $count плиток'; +} + + @override + String mapCache_cachedTilesWithFailed(int downloaded, int failed) { + return 'Закэшировано $downloaded плиток ($failed не загружено)'; +} + + @override + String get mapCache_clearOfflineCacheTitle => 'Очистить офлайн-кэш'; + + @override + String get mapCache_clearOfflineCachePrompt => 'Удалить все закэшированные плитки карты?'; + + @override + String get mapCache_offlineCacheCleared => 'Офлайн-кэш очищен'; + + @override + String get mapCache_noAreaSelected => 'Область не выбрана'; + + @override + String get mapCache_cacheArea => 'Область кэширования'; + + @override + String get mapCache_useCurrentView => 'Использовать текущий вид'; + + @override + String get mapCache_zoomRange => 'Диапазон масштаба'; + + @override + String mapCache_estimatedTiles(int count) { + return 'Оценочное количество плиток: $count'; +} + + @override + String mapCache_downloadedTiles(int completed, int total) { + return 'Загружено $completed из $total'; +} + + @override + String get mapCache_downloadTilesButton => 'Загрузить плитки'; + + @override + String get mapCache_clearCacheButton => 'Очистить кэш'; + + @override + String mapCache_failedDownloads(int count) { + return 'Неудачных загрузок: $count'; +} + + @override + String mapCache_boundsLabel( + String north, + String south, + String east, + String west, +) { + return 'С $north, Ю $south, В $east, З $west'; +} + + @override + String get time_justNow => 'Только что'; + + @override + String time_minutesAgo(int minutes) { + return '${minutes} мин назад'; +} + + @override + String time_hoursAgo(int hours) { + return '${hours} ч назад'; +} + + @override + String time_daysAgo(int days) { + return '${days} дн. назад'; +} + + @override + String get time_hour => 'час'; + + @override + String get time_hours => 'часов'; + + @override + String get time_day => 'день'; + + @override + String get time_days => 'дней'; + + @override + String get time_week => 'неделя'; + + @override + String get time_weeks => 'недель'; + + @override + String get time_month => 'месяц'; + + @override + String get time_months => 'месяцев'; + + @override + String get time_minutes => 'минут'; + + @override + String get time_allTime => 'Всё время'; + + @override + String get dialog_disconnect => 'Отключиться'; + + @override + String get dialog_disconnectConfirm => + 'Вы уверены, что хотите отключиться от этого устройства?'; + + @override + String get login_repeaterLogin => 'Вход в репитер'; + + @override + String get login_roomLogin => 'Вход на сервер комнат'; + + @override + String get login_password => 'Пароль'; + + @override + String get login_enterPassword => 'Введите пароль'; + + @override + String get login_savePassword => 'Сохранить пароль'; + + @override + String get login_savePasswordSubtitle => + 'Пароль будет надёжно сохранён на этом устройстве'; + + @override + String get login_repeaterDescription => + 'Введите пароль репитера для доступа к настройкам и статусу.'; + + @override + String get login_roomDescription => + 'Введите пароль комнаты для доступа к настройкам и статусу.'; + + @override + String get login_routing => 'Маршрутизация'; + + @override + String get login_routingMode => 'Режим маршрутизации'; + + @override + String get login_autoUseSavedPath => 'Авто (использовать сохранённый маршрут)'; + + @override + String get login_forceFloodMode => 'Принудительный режим рассылки'; + + @override + String get login_managePaths => 'Управление маршрутами'; + + @override + String get login_login => 'Войти'; + + @override + String login_attempt(int current, int max) { + return 'Попытка $current/$max'; +} + + @override + String login_failed(String error) { + return 'Ошибка входа: $error'; +} + + @override + String get login_failedMessage => + 'Не удалось войти. Либо пароль неверен, либо репитер недоступен.'; + + @override + String get common_reload => 'Обновить'; + + @override + String get common_clear => 'Очистить'; + + @override + String path_currentPath(String path) { + return 'Текущий маршрут: $path'; +} + + @override + String path_usingHopsPath(int count) { + String _temp0 = intl.Intl.pluralLogic( +count, +locale: localeName, +other: 'хопов', +one: 'хоп', +); + return 'Используется маршрут из $count $_temp0'; +} + + @override + String get path_enterCustomPath => 'Введите маршрут вручную'; + + @override + String get path_currentPathLabel => 'Текущий маршрут'; + + @override + String get path_hexPrefixInstructions => + 'Введите 2-символьные шестнадцатеричные префиксы для каждого хопа, разделённые запятыми.'; + + @override + String get path_hexPrefixExample => + 'Пример: A1,F2,3C (каждый узел использует первый байт своего публичного ключа)'; + + @override + String get path_labelHexPrefixes => 'Маршрут (шестнадцатеричные префиксы)'; + + @override + String get path_helperMaxHops => + 'Максимум 64 хопа. Каждый префикс — 2 шестнадцатеричных символа (1 байт)'; + + @override + String get path_selectFromContacts => 'Или выберите из контактов:'; + + @override + String get path_noRepeatersFound => 'Репитеры или серверы комнат не найдены.'; + + @override + String get path_customPathsRequire => + 'Пользовательские маршруты требуют промежуточных узлов, способных ретранслировать сообщения.'; + + @override + String path_invalidHexPrefixes(String prefixes) { + return 'Недопустимые шестнадцатеричные префиксы: $prefixes'; +} + + @override + String get path_tooLong => 'Маршрут слишком длинный. Максимум 64 хопа.'; + + @override + String get path_setPath => 'Установить маршрут'; + + @override + String get repeater_management => 'Управление репитером'; + + @override + String get room_management => 'Управление сервером комнат'; + + @override + String get repeater_managementTools => 'Инструменты управления'; + + @override + String get repeater_status => 'Статус'; + + @override + String get repeater_statusSubtitle => + 'Просмотр статуса, статистики и соседей репитера'; + + @override + String get repeater_telemetry => 'Телеметрия'; + + @override + String get repeater_telemetrySubtitle => + 'Просмотр телеметрии датчиков и системной статистики'; + + @override + String get repeater_cli => 'CLI'; + + @override + String get repeater_cliSubtitle => 'Отправка команд репитеру'; + + @override + String get repeater_neighbours => 'Соседи'; + + @override + String get repeater_neighboursSubtitle => 'Просмотр соседей на нулевом хопе.'; + + @override + String get repeater_settings => 'Настройки'; + + @override + String get repeater_settingsSubtitle => 'Настройка параметров репитера'; + + @override + String get repeater_statusTitle => 'Статус репитера'; + + @override + String get repeater_routingMode => 'Режим маршрутизации'; + + @override + String get repeater_autoUseSavedPath => 'Авто (использовать сохранённый маршрут)'; + + @override + String get repeater_forceFloodMode => 'Принудительный режим рассылки'; + + @override + String get repeater_pathManagement => 'Управление маршрутами'; + + @override + String get repeater_refresh => 'Обновить'; + + @override + String get repeater_statusRequestTimeout => 'Время ожидания статуса истекло.'; + + @override + String repeater_errorLoadingStatus(String error) { + return 'Ошибка загрузки статуса: $error'; +} + + @override + String get repeater_systemInformation => 'Системная информация'; + + @override + String get repeater_battery => 'Батарея'; + + @override + String get repeater_clockAtLogin => 'Время (при входе)'; + + @override + String get repeater_uptime => 'Время работы'; + + @override + String get repeater_queueLength => 'Длина очереди'; + + @override + String get repeater_debugFlags => 'Флаги отладки'; + + @override + String get repeater_radioStatistics => 'Радиостатистика'; + + @override + String get repeater_lastRssi => 'Последний RSSI'; + + @override + String get repeater_lastSnr => 'Последний SNR'; + + @override + String get repeater_noiseFloor => 'Уровень шума'; + + @override + String get repeater_txAirtime => 'Время эфира (передача)'; + + @override + String get repeater_rxAirtime => 'Время эфира (приём)'; + + @override + String get repeater_packetStatistics => 'Статистика пакетов'; + + @override + String get repeater_sent => 'Отправлено'; + + @override + String get repeater_received => 'Получено'; + + @override + String get repeater_duplicates => 'Дубликаты'; + + @override + String repeater_daysHoursMinsSecs( +int days, +int hours, +int minutes, +int seconds, +) { + return '$days дн. ${hours}ч ${minutes}м ${seconds}с'; +} + + @override + String repeater_packetTxTotal(int total, String flood, String direct) { + return 'Всего: $total, Рассылка: $flood, Прямые: $direct'; +} + + @override + String repeater_packetRxTotal(int total, String flood, String direct) { + return 'Всего: $total, Рассылка: $flood, Прямые: $direct'; +} + + @override + String repeater_duplicatesFloodDirect(String flood, String direct) { + return 'Рассылка: $flood, Прямые: $direct'; +} + + @override + String repeater_duplicatesTotal(int total) { + return 'Всего: $total'; +} + + @override + String get repeater_settingsTitle => 'Настройки репитера'; + + @override + String get repeater_basicSettings => 'Основные настройки'; + + @override + String get repeater_repeaterName => 'Имя репитера'; + + @override + String get repeater_repeaterNameHelper => 'Отображаемое имя этого репитера'; + + @override + String get repeater_adminPassword => 'Пароль администратора'; + + @override + String get repeater_adminPasswordHelper => 'Пароль с полным доступом'; + + @override + String get repeater_guestPassword => 'Гостевой пароль'; + + @override + String get repeater_guestPasswordHelper => 'Пароль для доступа только для чтения'; + + @override + String get repeater_radioSettings => 'Настройки радио'; + + @override + String get repeater_frequencyMhz => 'Частота (МГц)'; + + @override + String get repeater_frequencyHelper => '300–2500 МГц'; + + @override + String get repeater_txPower => 'Мощность передачи'; + + @override + String get repeater_txPowerHelper => '1–30 дБм'; + + @override + String get repeater_bandwidth => 'Полоса пропускания'; + + @override + String get repeater_spreadingFactor => 'Коэффициент расширения'; + + @override + String get repeater_codingRate => 'Коэффициент кодирования'; + + @override + String get repeater_locationSettings => 'Настройки местоположения'; + + @override + String get repeater_latitude => 'Широта'; + + @override + String get repeater_latitudeHelper => 'В десятичных градусах (напр., 37.7749)'; + + @override + String get repeater_longitude => 'Долгота'; + + @override + String get repeater_longitudeHelper => 'В десятичных градусах (напр., -122.4194)'; + + @override + String get repeater_features => 'Функции'; + + @override + String get repeater_packetForwarding => 'Пересылка пакетов'; + + @override + String get repeater_packetForwardingSubtitle => + 'Разрешить репитеру пересылать пакеты'; + + @override + String get repeater_guestAccess => 'Гостевой доступ'; + + @override + String get repeater_guestAccessSubtitle => 'Разрешить гостевой доступ только для чтения'; + + @override + String get repeater_privacyMode => 'Режим конфиденциальности'; + + @override + String get repeater_privacyModeSubtitle => + 'Скрывать имя/местоположение в оповещениях'; + + @override + String get repeater_advertisementSettings => 'Настройки анонсирования'; + + @override + String get repeater_localAdvertInterval => 'Интервал локальных анонсирований'; + + @override + String repeater_localAdvertIntervalMinutes(int minutes) { + return '$minutes минут'; +} + + @override + String get repeater_floodAdvertInterval => 'Интервал анонсирований рассылкой (flood)'; + + @override + String repeater_floodAdvertIntervalHours(int hours) { + return '$hours часов'; +} + + @override + String get repeater_encryptedAdvertInterval => + 'Интервал зашифрованных анонсирований'; + + @override + String get repeater_dangerZone => 'Опасная зона'; + + @override + String get repeater_rebootRepeater => 'Перезагрузить репитер'; + + @override + String get repeater_rebootRepeaterSubtitle => 'Перезапустить устройство репитера'; + + @override + String get repeater_rebootRepeaterConfirm => + 'Вы уверены, что хотите перезагрузить этот репитер?'; + + @override + String get repeater_regenerateIdentityKey => 'Пересоздать ключ идентификации'; + + @override + String get repeater_regenerateIdentityKeySubtitle => + 'Сгенерировать новую пару публичного/приватного ключей'; + + @override + String get repeater_regenerateIdentityKeyConfirm => + 'Это создаст новую идентичность для репитера. Продолжить?'; + + @override + String get repeater_eraseFileSystem => 'Стереть файловую систему'; + + @override + String get repeater_eraseFileSystemSubtitle => + 'Отформатировать файловую систему репитера'; + + @override + String get repeater_eraseFileSystemConfirm => + 'ВНИМАНИЕ: это удалит все данные на репитере. Действие нельзя отменить!'; + + @override + String get repeater_eraseSerialOnly => + 'Очистка доступна только через последовательную консоль.'; + + @override + String repeater_commandSent(String command) { + return 'Команда отправлена: $command'; +} + + @override + String repeater_errorSendingCommand(String error) { + return 'Ошибка отправки команды: $error'; +} + + @override + String get repeater_confirm => 'Подтвердить'; + + @override + String get repeater_settingsSaved => 'Настройки успешно сохранены'; + + @override + String repeater_errorSavingSettings(String error) { + return 'Ошибка сохранения настроек: $error'; +} + + @override + String get repeater_refreshBasicSettings => 'Обновить основные настройки'; + + @override + String get repeater_refreshRadioSettings => 'Обновить настройки радио'; + + @override + String get repeater_refreshTxPower => 'Обновить мощность передачи'; + + @override + String get repeater_refreshLocationSettings => 'Обновить настройки местоположения'; + + @override + String get repeater_refreshPacketForwarding => 'Обновить пересылку пакетов'; + + @override + String get repeater_refreshGuestAccess => 'Обновить гостевой доступ'; + + @override + String get repeater_refreshPrivacyMode => 'Обновить режим конфиденциальности'; + + @override + String get repeater_refreshAdvertisementSettings => + 'Обновить настройки анонсирований'; + + @override + String repeater_refreshed(String label) { + return '$label обновлён'; +} + + @override + String repeater_errorRefreshing(String label) { + return 'Ошибка обновления $label'; +} + + @override + String get repeater_cliTitle => 'CLI репитера'; + + @override + String get repeater_debugNextCommand => 'Отладка следующей команды'; + + @override + String get repeater_commandHelp => 'Справка по командам'; + + @override + String get repeater_clearHistory => 'Очистить историю'; + + @override + String get repeater_noCommandsSent => 'Команды ещё не отправлялись'; + + @override + String get repeater_typeCommandOrUseQuick => + 'Введите команду ниже или используйте быстрые команды'; + + @override + String get repeater_enterCommandHint => 'Введите команду...'; + + @override + String get repeater_previousCommand => 'Предыдущая команда'; + + @override + String get repeater_nextCommand => 'Следующая команда'; + + @override + String get repeater_enterCommandFirst => 'Сначала введите команду'; + + @override + String get repeater_cliCommandFrameTitle => 'Фрейм CLI-команды'; + + @override + String repeater_cliCommandError(String error) { + return 'Ошибка: $error'; +} + + @override + String get repeater_cliQuickGetName => 'Получить имя'; + + @override + String get repeater_cliQuickGetRadio => 'Получить радио'; + + @override + String get repeater_cliQuickGetTx => 'Получить TX'; + + @override + String get repeater_cliQuickNeighbors => 'Соседи'; + + @override + String get repeater_cliQuickVersion => 'Версия'; + + @override + String get repeater_cliQuickAdvertise => 'Анонсировать'; + + @override + String get repeater_cliQuickClock => 'Время'; + + @override + String get repeater_cliHelpAdvert => 'Отправляет пакет анонсирования'; + + @override + String get repeater_cliHelpReboot => + 'Перезагружает устройство. (обычно вы получите «Тайм-аут» — это нормально)'; + + @override + String get repeater_cliHelpClock => + 'Показывает текущее время по часам устройства.'; + + @override + String get repeater_cliHelpPassword => + 'Устанавливает новый пароль администратора для устройства.'; + + @override + String get repeater_cliHelpVersion => + 'Показывает версию устройства и дату сборки прошивки.'; + + @override + String get repeater_cliHelpClearStats => + 'Сбрасывает различные счётчики статистики в ноль.'; + + @override + String get repeater_cliHelpSetAf => 'Устанавливает коэффициент времени в эфире.'; + + @override + String get repeater_cliHelpSetTx => + 'Устанавливает мощность передачи LoRa в дБм. (требуется перезагрузка)'; + + @override + String get repeater_cliHelpSetRepeat => + 'Включает или отключает роль репитера для этой ноды.'; + + @override + String get repeater_cliHelpSetAllowReadOnly => + '(Сервер комнат) Если «on», то вход без пароля разрешён, но публиковать в комнату нельзя (только чтение)'; + + @override + String get repeater_cliHelpSetFloodMax => + 'Устанавливает максимальное число хопов для входящих пакетов в режиме рассылки (если >= макс., пакет не пересылается)'; + + @override + String get repeater_cliHelpSetIntThresh => + 'Устанавливает порог интерференции (в дБ). По умолчанию 14. Установите 0, чтобы отключить обнаружение помех.'; + + @override + String get repeater_cliHelpSetAgcResetInterval => + 'Устанавливает интервал сброса автоматической регулировки усиления. Установите 0, чтобы отключить.'; + + @override + String get repeater_cliHelpSetMultiAcks => + 'Включает или отключает функцию «двойных ACK».'; + + @override + String get repeater_cliHelpSetAdvertInterval => + 'Устанавливает интервал (в минутах) отправки локального (нулевой хоп) анонсирования. Установите 0, чтобы отключить.'; + + @override + String get repeater_cliHelpSetFloodAdvertInterval => + 'Устанавливает интервал (в часах) отправки анонсирований рассылкой. Установите 0, чтобы отключить.'; + + @override + String get repeater_cliHelpSetGuestPassword => + 'Устанавливает/обновляет гостевой пароль. (для репитеров гости могут отправлять запрос «Get Stats»)'; + + @override + String get repeater_cliHelpSetName => 'Устанавливает имя в оповещениях.'; + + @override + String get repeater_cliHelpSetLat => + 'Устанавливает широту для карты в оповещениях. (десятичные градусы)'; + + @override + String get repeater_cliHelpSetLon => + 'Устанавливает долготу для карты в оповещениях. (десятичные градусы)'; + + @override + String get repeater_cliHelpSetRadio => + 'Устанавливает полностью новые параметры радио и сохраняет их в настройки. Требуется команда «reboot» для применения.'; + + @override + String get repeater_cliHelpSetRxDelay => + 'Устанавливает (экспериментально) базовую задержку (>1 для эффекта) для принятых пакетов на основе качества сигнала. Установите 0, чтобы отключить.'; + + @override + String get repeater_cliHelpSetTxDelay => + 'Устанавливает множитель времени в эфире для пакета в режиме рассылки и применяет случайную задержку перед пересылкой (чтобы уменьшить коллизии).'; + + @override + String get repeater_cliHelpSetDirectTxDelay => + 'То же, что txdelay, но для случайной задержки пересылки пакетов в прямом режиме.'; + + @override + String get repeater_cliHelpSetBridgeEnabled => 'Включить/выключить мост.'; + + @override + String get repeater_cliHelpSetBridgeDelay => + 'Установить задержку перед ретрансляцией пакетов.'; + + @override + String get repeater_cliHelpSetBridgeSource => + 'Выбрать, будет ли мост ретранслировать полученные или отправленные пакеты.'; + + @override + String get repeater_cliHelpSetBridgeBaud => + 'Установить скорость последовательного соединения для мостов RS232.'; + + @override + String get repeater_cliHelpSetBridgeSecret => + 'Установить секрет моста для мостов ESP-NOW.'; + + @override + String get repeater_cliHelpSetAdcMultiplier => + 'Устанавливает пользовательский коэффициент коррекции напряжения батареи (поддерживается только на некоторых платах).'; + + @override + String get repeater_cliHelpTempRadio => + 'Устанавливает временные параметры радио на заданное число минут, затем возвращает исходные. (НЕ сохраняется в настройки).'; + + @override + String get repeater_cliHelpSetPerm => + 'Изменяет ACL. Удаляет запись (по префиксу публичного ключа), если «permissions» равен нулю. Добавляет новую запись, если указан полный ключ и он отсутствует в ACL. Обновляет запись по совпадению префикса. Биты прав зависят от роли прошивки, но младшие 2 бита: 0 (Гость), 1 (Только чтение), 2 (Чтение/запись), 3 (Админ)'; + + @override + String get repeater_cliHelpGetBridgeType => + 'Получает тип моста: none, rs232, espnow'; + + @override + String get repeater_cliHelpLogStart => + 'Начинает запись пакетов в файловую систему.'; + + @override + String get repeater_cliHelpLogStop => 'Останавливает запись пакетов в файловую систему.'; + + @override + String get repeater_cliHelpLogErase => + 'Удаляет журналы пакетов из файловой системы.'; + + @override + String get repeater_cliHelpNeighbors => + 'Показывает список других репитеров, услышанных через оповещения нулевого хопа. Каждая строка: префикс-id-в-hex:временная-метка:snr×4'; + + @override + String get repeater_cliHelpNeighborRemove => + 'Удаляет первую подходящую запись (по префиксу публичного ключа в hex) из списка соседей.'; + + @override + String get repeater_cliHelpRegion => + '(только через последовательный порт) Показывает все определённые регионы и текущие права на рассылку.'; + + @override + String get repeater_cliHelpRegionLoad => + 'ПРИМЕЧАНИЕ: это специальная многострочная команда. Каждая следующая строка — имя региона (с отступом пробелами для указания иерархии, минимум один пробел). Завершается пустой строкой.'; + + @override + String get repeater_cliHelpRegionGet => + 'Ищет регион по префиксу имени (или «*» для глобальной области). Отвечает: «-> имя-региона (родитель) \'F\'»'; + + @override + String get repeater_cliHelpRegionPut => + 'Добавляет или обновляет определение региона с заданным именем.'; + + @override + String get repeater_cliHelpRegionRemove => + 'Удаляет определение региона с заданным именем. (должно точно совпадать и не иметь дочерних регионов)'; + + @override + String get repeater_cliHelpRegionAllowf => + 'Разрешает рассылку («F»lood) для заданного региона. («*» для глобальной/устаревшей области)'; + + @override + String get repeater_cliHelpRegionDenyf => + 'Запрещает рассылку («F»lood) для заданного региона. (НЕ рекомендуется для глобальной области!)'; + + @override + String get repeater_cliHelpRegionHome => + 'Показывает текущий «домашний» регион. (Пока не используется, зарезервировано на будущее)'; + + @override + String get repeater_cliHelpRegionHomeSet => 'Устанавливает «домашний» регион.'; + + @override + String get repeater_cliHelpRegionSave => + 'Сохраняет список/карту регионов в память.'; + + @override + String get repeater_cliHelpGps => + 'Показывает статус GPS. Если GPS выключен — отвечает только «off». Если включён — показывает статус, фиксацию, количество спутников.'; + + @override + String get repeater_cliHelpGpsOnOff => 'Переключает состояние питания GPS.'; + + @override + String get repeater_cliHelpGpsSync => 'Синхронизирует время ноды с часами GPS.'; + + @override + String get repeater_cliHelpGpsSetLoc => + 'Устанавливает позицию ноды по координатам GPS и сохраняет в настройки.'; + + @override + String get repeater_cliHelpGpsAdvert => + 'Показывает конфигурацию передачи местоположения в анонсированиях: + - none: не включать местоположение + - share: передавать GPS-координаты (из SensorManager) + - prefs: передавать координаты из настроек'; + + @override + String get repeater_cliHelpGpsAdvertSet => + 'Устанавливает конфигурацию передачи местоположения.'; + + @override + String get repeater_commandsListTitle => 'Список команд'; + + @override + String get repeater_commandsListNote => + 'ПРИМЕЧАНИЕ: для большинства команд «set ...» существуют соответствующие команды «get ...».'; + + @override + String get repeater_general => 'Общие'; + + @override + String get repeater_settingsCategory => 'Настройки'; + + @override + String get repeater_bridge => 'Мост'; + + @override + String get repeater_logging => 'Журналирование'; + + @override + String get repeater_neighborsRepeaterOnly => 'Соседи (только для репитеров)'; + + @override + String get repeater_regionManagementRepeaterOnly => + 'Управление регионами (только для репитеров)'; + + @override + String get repeater_regionNote => + 'Команды регионов введены для управления определениями регионов и правами доступа.'; + + @override + String get repeater_gpsManagement => 'Управление GPS'; + + @override + String get repeater_gpsNote => + 'Команда gps введена для управления параметрами, связанными с местоположением.'; + + @override + String get telemetry_receivedData => 'Полученные телеметрические данные'; + + @override + String get telemetry_requestTimeout => 'Время ожидания телеметрии истекло.'; + + @override + String telemetry_errorLoading(String error) { + return 'Ошибка загрузки телеметрии: $error'; +} + + @override + String get telemetry_noData => 'Данные телеметрии недоступны.'; + + @override + String telemetry_channelTitle(int channel) { + return 'Канал $channel'; +} + + @override + String get telemetry_batteryLabel => 'Батарея'; + + @override + String get telemetry_voltageLabel => 'Напряжение'; + + @override + String get telemetry_mcuTemperatureLabel => 'Температура МК'; + + @override + String get telemetry_temperatureLabel => 'Температура'; + + @override + String get telemetry_currentLabel => 'Ток'; + + @override + String telemetry_batteryValue(int percent, String volts) { + return '$percent% / ${volts}В'; +} + + @override + String telemetry_voltageValue(String volts) { + return '${volts}В'; +} + + @override + String telemetry_currentValue(String amps) { + return '${amps}А'; +} + + @override + String telemetry_temperatureValue(String celsius, String fahrenheit) { + return '$celsius°C / $fahrenheit°F'; +} + + @override + String get neighbors_receivedData => 'Полученные данные о соседях'; + + @override + String get neighbors_requestTimedOut => 'Время ожидания данных о соседях истекло.'; + + @override + String neighbors_errorLoading(String error) { + return 'Ошибка загрузки соседей: $error'; +} + + @override + String get neighbors_repeatersNeighbours => 'Соседи репитеров'; + + @override + String get neighbors_noData => 'Данные о соседях недоступны.'; + + @override + String neighbors_unknownContact(String pubkey) { + return 'Неизвестный $pubkey'; +} + + @override + String neighbors_heardAgo(String time) { + return 'Слышали: $time назад'; +} + + @override + String get channelPath_title => 'Путь пакета'; + + @override + String get channelPath_viewMap => 'Посмотреть на карте'; + + @override + String get channelPath_otherObservedPaths => 'Другие наблюдаемые пути'; + + @override + String get channelPath_repeaterHops => 'Хопы через репитеры'; + + @override + String get channelPath_noHopDetails => + 'Детали хопов для этого пакета не предоставлены.'; + + @override + String get channelPath_messageDetails => 'Детали сообщения'; + + @override + String get channelPath_senderLabel => 'Отправитель'; + + @override + String get channelPath_timeLabel => 'Время'; + + @override + String get channelPath_repeatsLabel => 'Повторы'; + + @override + String channelPath_pathLabel(int index) { + return 'Путь $index'; +} + + @override + String get channelPath_observedLabel => 'Наблюдаемый'; + + @override + String channelPath_observedPathTitle(int index, String hops) { + return 'Наблюдаемый путь $index • $hops'; +} + + @override + String get channelPath_noLocationData => 'Нет данных о местоположении'; + + @override + String channelPath_timeWithDate(int day, int month, String time) { + return '$day/$month $time'; +} + + @override + String channelPath_timeOnly(String time) { + return '$time'; +} + + @override + String get channelPath_unknownPath => 'Неизвестный'; + + @override + String get channelPath_floodPath => 'Рассылка'; + + @override + String get channelPath_directPath => 'Прямой'; + + @override + String channelPath_observedZeroOf(int total) { + return '0 из $total хопов'; +} + + @override + String channelPath_observedSomeOf(int observed, int total) { + return '$observed из $total хопов'; +} + + @override + String get channelPath_mapTitle => 'Карта пути'; + + @override + String get channelPath_noRepeaterLocations => + 'Нет данных о местоположении репитеров для этого пути.'; + + @override + String channelPath_primaryPath(int index) { + return 'Путь $index (Основной)'; +} + + @override + String get channelPath_pathLabelTitle => 'Путь'; + + @override + String get channelPath_observedPathHeader => 'Наблюдаемый путь'; + + @override + String channelPath_selectedPathLabel(String label, String prefixes) { + return '$label • $prefixes'; +} + + @override + String get channelPath_noHopDetailsAvailable => + 'Детали хопов для этого пакета недоступны.'; + + @override + String get channelPath_unknownRepeater => 'Неизвестный репитер'; + + @override + String get community_title => 'Сообщество'; + + @override + String get community_create => 'Создать сообщество'; + + @override + String get community_createDesc => + 'Создать новое сообщество и поделиться через QR-код.'; + + @override + String get community_join => 'Присоединиться'; + + @override + String get community_joinTitle => 'Присоединиться к сообществу'; + + @override + String community_joinConfirmation(String name) { + return 'Вы хотите присоединиться к сообществу \"$name\"?'; +} + + @override + String get community_scanQr => 'Сканировать QR-код сообщества'; + + @override + String get community_scanInstructions => + 'Наведите камеру на QR-код сообщества'; + + @override + String get community_showQr => 'Показать QR-код'; + + @override + String get community_publicChannel => 'Публичный канал сообщества'; + + @override + String get community_hashtagChannel => 'Хэштег-канал сообщества'; + + @override + String get community_name => 'Имя сообщества'; + + @override + String get community_enterName => 'Введите имя сообщества'; + + @override + String community_created(String name) { + return 'Сообщество \"$name\" создано'; +} + + @override + String community_joined(String name) { + return 'Присоединились к сообществу \"$name\"'; +} + + @override + String get community_qrTitle => 'Поделиться сообществом'; + + @override + String community_qrInstructions(String name) { + return 'Отсканируйте этот QR-код, чтобы присоединиться к \"$name\"'; +} + + @override + String get community_hashtagPrivacyHint => + 'Хэштег-каналы сообщества доступны только его участникам'; + + @override + String get community_invalidQrCode => 'Недопустимый QR-код сообщества'; + + @override + String get community_alreadyMember => 'Уже участник'; + + @override + String community_alreadyMemberMessage(String name) { + return 'Вы уже участник сообщества \"$name\".'; +} + + @override + String get community_addPublicChannel => 'Добавить публичный канал сообщества'; + + @override + String get community_addPublicChannelHint => + 'Автоматически добавить публичный канал для этого сообщества'; + + @override + String get community_noCommunities => 'Вы ещё не присоединились ни к одному сообществу'; + + @override + String get community_scanOrCreate => + 'Отсканируйте QR-код или создайте сообщество, чтобы начать'; + + @override + String get community_manageCommunities => 'Управление сообществами'; + + @override + String get community_delete => 'Покинуть сообщество'; + + @override + String community_deleteConfirm(String name) { + return 'Покинуть \"$name\"?'; +} + + @override + String community_deleteChannelsWarning(int count) { + return 'Это также удалит $count канал(ов) и их сообщения.'; +} + + @override + String community_deleted(String name) { + return 'Покинули сообщество \"$name\"'; +} + + @override + String get community_regenerateSecret => 'Пересоздать секрет'; + + @override + String community_regenerateSecretConfirm(String name) { + return 'Пересоздать секретный ключ для \"$name\"? Все участники должны будут отсканировать новый QR-код для продолжения общения.'; +} + + @override + String get community_regenerate => 'Пересоздать'; + + @override + String community_secretRegenerated(String name) { + return 'Секрет пересоздан для \"$name\"'; +} + + @override + String get community_updateSecret => 'Обновить секрет'; + + @override + String community_secretUpdated(String name) { + return 'Секрет обновлён для \"$name\"'; +} + + @override + String community_scanToUpdateSecret(String name) { + return 'Отсканируйте новый QR-код, чтобы обновить секрет для \"$name\"'; +} + + @override + String get community_addHashtagChannel => 'Добавить хэштег-канал сообщества'; + + @override + String get community_addHashtagChannelDesc => + 'Добавить хэштег-канал для этого сообщества'; + + @override + String get community_selectCommunity => 'Выбрать сообщество'; + + @override + String get community_regularHashtag => 'Обычный хэштег'; + + @override + String get community_regularHashtagDesc => 'Публичный хэштег (любой может присоединиться)'; + + @override + String get community_communityHashtag => 'Хэштег сообщества'; + + @override + String get community_communityHashtagDesc => 'Доступен только участникам сообщества'; + + @override + String community_forCommunity(String name) { + return 'Для $name'; +} + + @override + String get listFilter_tooltip => 'Фильтр и сортировка'; + + @override + String get listFilter_sortBy => 'Сортировка по'; + + @override + String get listFilter_latestMessages => 'Последние сообщения'; + + @override + String get listFilter_heardRecently => 'Слышали недавно'; + + @override + String get listFilter_az => 'По алфавиту'; + + @override + String get listFilter_filters => 'Фильтры'; + + @override + String get listFilter_all => 'Все'; + + @override + String get listFilter_users => 'Пользователи'; + + @override + String get listFilter_repeaters => 'Репитеры'; + + @override + String get listFilter_roomServers => 'Серверы комнат'; + + @override + String get listFilter_unreadOnly => 'Только непрочитанные'; + + @override + String get listFilter_newGroup => 'Новая группа'; +} \ No newline at end of file diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb new file mode 100644 index 0000000..7ac1e85 --- /dev/null +++ b/lib/l10n/app_ru.arb @@ -0,0 +1,761 @@ +{ + "@@locale": "ru", + + "appTitle": "MeshCore Open", + + "nav_contacts": "Контакты", + "nav_channels": "Каналы", + "nav_map": "Карта", + + "common_cancel": "Отмена", + "common_ok": "OK", + "common_connect": "Коннект", + "common_unknownDevice": "Неизвестное устройство", + "common_save": "Сохранить", + "common_delete": "Удалить", + "common_close": "Закрыть", + "common_edit": "Изменить", + "common_add": "Добавить", + "common_settings": "Настройки", + "common_disconnect": "Отключить", + "common_connected": "Подключено", + "common_disconnected": "Отключено", + "common_create": "Создать", + "common_continue": "Продолжить", + "common_share": "Поделиться", + "common_copy": "Копировать", + "common_retry": "Повторить", + "common_hide": "Скрыть", + "common_remove": "Убрать", + "common_enable": "Включить", + "common_disable": "Выключить", + "common_reboot": "Перезагрузить", + "common_loading": "Загрузка...", + "common_notAvailable": "—", + "common_voltageValue": "{volts} В", + "common_percentValue": "{percent}%", + "scanner_title": "MeshCore Open", + "scanner_scanning": "Поиск устройств...", + "scanner_connecting": "Подключение...", + "scanner_disconnecting": "Отключение...", + "scanner_notConnected": "Не подключено", + "scanner_connectedTo": "Подключено к {deviceName}", + "scanner_searchingDevices": "Поиск устройств MeshCore...", + "scanner_tapToScan": "Нажмите для поиска MeshCore устройств", + "scanner_connectionFailed": "Подключение не удалось: {error}", + "scanner_stop": "Стоп", + "scanner_scan": "Сканирование", + "device_quickSwitch": "Быстрое переключение", + "device_meshcore": "MeshCore", + "settings_title": "Настройки", + "settings_deviceInfo": "Информация об устройстве", + "settings_appSettings": "Настройки приложения", + "settings_appSettingsSubtitle": "Уведомления, сообщения и настройки карты", + "settings_nodeSettings": "Настройки ноды", + "settings_nodeName": "Имя ноды", + "settings_nodeNameNotSet": "Не установлено", + "settings_nodeNameHint": "Введите имя ноды", + "settings_nodeNameUpdated": "Имя обновлено", + "settings_radioSettings": "Настройки радио", + "settings_radioSettingsSubtitle": "Частота, мощность и коэффициент распространения", + "settings_radioSettingsUpdated": "Настройки радио обновлены", + "settings_location": "Позиция", + "settings_locationSubtitle": "Координаты GPS", + "settings_locationUpdated": "Позиция и настройки GPS обновлены", + "settings_locationBothRequired": "Введите широту и долготу.", + "settings_locationInvalid": "Неверная широта или долгота.", + "settings_locationGPSEnable": "Включить GPS", + "settings_locationGPSEnableSubtitle": "Включение GPS для автоматического обновления позиции.", + "settings_locationIntervalSec": "Интервал для позиционирования GPS (секунды)", + "settings_locationIntervalInvalid": "Интервал должен составлять не менее 60 секунд и не более 86400 секунд.", + "settings_latitude": "Широта", + "settings_longitude": "Долгота", + "settings_privacyMode": "Режим конфиденциальности", + "settings_privacyModeSubtitle": "Скрыть имя/позицию в анонсировании", + "settings_privacyModeToggle": "Включите режим конфиденциальности, чтобы скрыть свое имя и местоположение в анонсировании.", + "settings_privacyModeEnabled": "Режим конфиденциальности включен", + "settings_privacyModeDisabled": "Режим конфиденциальности выключен", + "settings_actions": "Действия", + "settings_sendAdvertisement": "Отправить анонсирование", + "settings_sendAdvertisementSubtitle": "Отправить анонсирование о присутствии сейчас", + "settings_advertisementSent": "Анонсирование отправлено", + "settings_syncTime": "Синхронизация времени", + "settings_syncTimeSubtitle": "Синхронизировать время с телефоном", + "settings_timeSynchronized": "Время синхронизировано", + "settings_refreshContacts": "Обновить контакты", + "settings_refreshContactsSubtitle": "Перезагрузить список контактов с устройства", + "settings_rebootDevice": "Перезагрузить устройство", + "settings_rebootDeviceSubtitle": "Перезапустить устройство MeshCore", + "settings_rebootDeviceConfirm": "Вы уверены, что хотите перезагрузить устройство? Вы будете отключены.", + "settings_debug": "Отладка", + "settings_bleDebugLog": "Журнал отладки BLE", + "settings_bleDebugLogSubtitle": "Команды BLE, ответы и сырые данные", + "settings_appDebugLog": "Журнал отладки приложения", + "settings_appDebugLogSubtitle": "Сообщения отладки приложения", + "settings_about": "О программе", + "settings_aboutVersion": "MeshCore Open v{version}", + "settings_aboutLegalese": "2026 MeshCore Open Source Project", + "settings_aboutDescription": "Открытое клиентское приложение на Flutter для устройств MeshCore с LoRa-сетями.", + "settings_infoName": "Имя", + "settings_infoId": "ID", + "settings_infoStatus": "Статус", + "settings_infoBattery": "Батарея", + "settings_infoPublicKey": "Публичный ключ", + "settings_infoContactsCount": "Количество контактов", + "settings_infoChannelCount": "Количество каналов", + "settings_presets": "Пресеты", + "settings_preset915Mhz": "915 МГц", + "settings_preset868Mhz": "868 МГц", + "settings_preset433Mhz": "433 МГц", + "settings_frequency": "Частота (МГц)", + "settings_frequencyHelper": "300.0 – 2500.0", + "settings_frequencyInvalid": "Недопустимая частота (300–2500 МГц)", + "settings_bandwidth": "Полоса пропускания", + "settings_spreadingFactor": "Коэффициент расширения", + "settings_codingRate": "Коэффициент кодирования", + "settings_txPower": "Мощность передачи (дБм)", + "settings_txPowerHelper": "0 – 22", + "settings_txPowerInvalid": "Недопустимая мощность передачи (0–22 дБм)", + "settings_longRange": "Дальний радиус", + "settings_fastSpeed": "Высокая скорость", + "settings_error": "Ошибка: {message}", + "appSettings_title": "Настройки приложения", + "appSettings_appearance": "Внешний вид", + "appSettings_theme": "Тема", + "appSettings_themeSystem": "Как в системе", + "appSettings_themeLight": "Светлая", + "appSettings_themeDark": "Тёмная", + "appSettings_language": "Язык", + "appSettings_languageSystem": "Как в системе", + "appSettings_languageEn": "Английский", + "appSettings_languageFr": "Французский", + "appSettings_languageEs": "Испанский", + "appSettings_languageDe": "Немецкий", + "appSettings_languagePl": "Польский", + "appSettings_languageSl": "Словенский", + "appSettings_languagePt": "Португальский", + "appSettings_languageIt": "Итальянский", + "appSettings_languageZh": "Китайский", + "appSettings_languageSv": "Шведский", + "appSettings_languageNl": "Нидерландский", + "appSettings_languageSk": "Словацкий", + "appSettings_languageBg": "Болгарский", + "appSettings_languageRu": "Русский", + "appSettings_notifications": "Уведомления", + "appSettings_enableNotifications": "Включить уведомления", + "appSettings_enableNotificationsSubtitle": "Получать уведомления о сообщениях и оповещениях", + "appSettings_notificationPermissionDenied": "Разрешение на уведомления отклонено", + "appSettings_notificationsEnabled": "Уведомления включены", + "appSettings_notificationsDisabled": "Уведомления отключены", + "appSettings_messageNotifications": "Уведомления о сообщениях", + "appSettings_messageNotificationsSubtitle": "Показывать уведомление при получении новых сообщений", + "appSettings_channelMessageNotifications": "Уведомления о сообщениях в каналах", + "appSettings_channelMessageNotificationsSubtitle": "Показывать уведомление при получении сообщений в каналах", + "appSettings_advertisementNotifications": "Уведомления об анонсированиях", + "appSettings_advertisementNotificationsSubtitle": "Показывать уведомление при обнаружении новых нод", + "appSettings_messaging": "Обмен сообщениями", + "appSettings_clearPathOnMaxRetry": "Сбросить маршрут после максимального числа попыток", + "appSettings_clearPathOnMaxRetrySubtitle": "Сбросить маршрут контакта после 5 неудачных попыток отправки", + "appSettings_pathsWillBeCleared": "Маршруты будут сброшены после 5 неудачных попыток", + "appSettings_pathsWillNotBeCleared": "Маршруты не будут автоматически сбрасываться", + "appSettings_autoRouteRotation": "Автоматическое переключение маршрутов", + "appSettings_autoRouteRotationSubtitle": "Циклически переключаться между лучшими маршрутами и режимом рассылки", + "appSettings_autoRouteRotationEnabled": "Автоматическое переключение маршрутов включено", + "appSettings_autoRouteRotationDisabled": "Автоматическое переключение маршрутов отключено", + "appSettings_battery": "Батарея", + "appSettings_batteryChemistry": "Химия батареи", + "appSettings_batteryChemistryPerDevice": "Установить для устройства ({deviceName})", + "appSettings_batteryChemistryConnectFirst": "Подключитесь к устройству, чтобы выбрать", + "appSettings_batteryNmc": "18650 NMC (3.0–4.2 В)", + "appSettings_batteryLifepo4": "LiFePO4 (2.6–3.65 В)", + "appSettings_batteryLipo": "LiPo (3.0–4.2 В)", + "appSettings_mapDisplay": "Отображение карты", + "appSettings_showRepeaters": "Показывать репитеры", + "appSettings_showRepeatersSubtitle": "Отображать репитеры на карте", + "appSettings_showChatNodes": "Показывать чат-ноды", + "appSettings_showChatNodesSubtitle": "Отображать чат-ноды на карте", + "appSettings_showOtherNodes": "Показывать другие ноды", + "appSettings_showOtherNodesSubtitle": "Отображать другие типы нод на карте", + "appSettings_timeFilter": "Фильтр по времени", + "appSettings_timeFilterShowAll": "Показывать все ноды", + "appSettings_timeFilterShowLast": "Показывать ноды за последние {hours} ч", + "appSettings_mapTimeFilter": "Временной фильтр карты", + "appSettings_showNodesDiscoveredWithin": "Показывать ноды, обнаруженные за:", + "appSettings_allTime": "Всё время", + "appSettings_lastHour": "Последний час", + "appSettings_last6Hours": "Последние 6 часов", + "appSettings_last24Hours": "Последние 24 часа", + "appSettings_lastWeek": "Последнюю неделю", + "appSettings_offlineMapCache": "Кэш офлайн-карты", + "appSettings_noAreaSelected": "Область не выбрана", + "appSettings_areaSelectedZoom": "Область выбрана (масштаб {minZoom}–{maxZoom})", + "appSettings_debugCard": "Отладка", + "appSettings_appDebugLogging": "Журнал отладки приложения", + "appSettings_appDebugLoggingSubtitle": "Записывать отладочные сообщения приложения для диагностики", + "appSettings_appDebugLoggingEnabled": "Журнал отладки приложения включён", + "appSettings_appDebugLoggingDisabled": "Журнал отладки приложения отключён", + "contacts_title": "Контакты", + "contacts_noContacts": "Контактов пока нет", + "contacts_contactsWillAppear": "Контакты появятся, когда устройства начнут рассылать оповещения", + "contacts_searchContacts": "Поиск контактов...", + "contacts_noUnreadContacts": "Нет непрочитанных контактов", + "contacts_noContactsFound": "Контакты или группы не найдены", + "contacts_deleteContact": "Удалить контакт", + "contacts_removeConfirm": "Удалить {contactName} из контактов?", + "contacts_manageRepeater": "Управление репитером", + "contacts_manageRoom": "Управление сервером комнат", + "contacts_roomLogin": "Вход на сервер комнат", + "contacts_openChat": "Открыть чат", + "contacts_editGroup": "Изменить группу", + "contacts_deleteGroup": "Удалить группу", + "contacts_deleteGroupConfirm": "Удалить \"{groupName}\"?", + "contacts_newGroup": "Новая группа", + "contacts_groupName": "Имя группы", + "contacts_groupNameRequired": "Имя группы обязательно", + "contacts_groupAlreadyExists": "Группа \"{name}\" уже существует", + "contacts_filterContacts": "Фильтр контактов...", + "contacts_noContactsMatchFilter": "Нет контактов, соответствующих фильтру", + "contacts_noMembers": "Нет участников", + "contacts_lastSeenNow": "Видели только что", + "contacts_lastSeenMinsAgo": "Видели {minutes} мин назад", + "contacts_lastSeenHourAgo": "Видели 1 час назад", + "contacts_lastSeenHoursAgo": "Видели {hours} ч назад", + "contacts_lastSeenDayAgo": "Видели 1 день назад", + "contacts_lastSeenDaysAgo": "Видели {days} дн. назад", + "channels_title": "Каналы", + "channels_noChannelsConfigured": "Каналы не настроены", + "channels_addPublicChannel": "Добавить публичный канал", + "channels_searchChannels": "Поиск каналов...", + "channels_noChannelsFound": "Каналы не найдены", + "channels_channelIndex": "Канал {index}", + "channels_hashtagChannel": "Хэштег-канал", + "channels_public": "Публичный", + "channels_private": "Приватный", + "channels_publicChannel": "Публичный канал", + "channels_privateChannel": "Приватный канал", + "channels_editChannel": "Изменить канал", + "channels_deleteChannel": "Удалить канал", + "channels_deleteChannelConfirm": "Удалить \"{name}\"? Это действие нельзя отменить.", + "channels_channelDeleted": "Канал \"{name}\" удалён", + "channels_addChannel": "Добавить канал", + "channels_channelIndexLabel": "Индекс канала", + "channels_channelName": "Имя канала", + "channels_usePublicChannel": "Использовать публичный канал", + "channels_standardPublicPsk": "Стандартный публичный PSK", + "channels_pskHex": "PSK (Hex)", + "channels_generateRandomPsk": "Сгенерировать случайный PSK", + "channels_enterChannelName": "Введите имя канала", + "channels_pskMustBe32Hex": "PSK должен содержать 32 шестнадцатеричных символа", + "channels_channelAdded": "Канал \"{name}\" добавлен", + "channels_editChannelTitle": "Изменить канал {index}", + "channels_smazCompression": "Сжатие SMAZ", + "channels_channelUpdated": "Канал \"{name}\" обновлён", + "channels_publicChannelAdded": "Публичный канал добавлен", + "channels_sortBy": "Сортировка", + "channels_sortManual": "Вручную", + "channels_sortAZ": "По алфавиту", + "channels_sortLatestMessages": "По последним сообщениям", + "channels_sortUnread": "По непрочитанным", + "channels_createPrivateChannel": "Создать приватный канал", + "channels_createPrivateChannelDesc": "Защищён секретным ключом.", + "channels_joinPrivateChannel": "Присоединиться к приватному каналу", + "channels_joinPrivateChannelDesc": "Введите секретный ключ вручную.", + "channels_joinPublicChannel": "Присоединиться к публичному каналу", + "channels_joinPublicChannelDesc": "К этому каналу может присоединиться любой.", + "channels_joinHashtagChannel": "Присоединиться к хэштег-каналу", + "channels_joinHashtagChannelDesc": "К хэштег-каналам может присоединиться любой.", + "channels_scanQrCode": "Сканировать QR-код", + "channels_scanQrCodeComingSoon": "Скоро будет", + "channels_enterHashtag": "Введите хэштег", + "channels_hashtagHint": "например, #команда", + "chat_noMessages": "Сообщений пока нет", + "chat_sendMessageToStart": "Отправьте сообщение, чтобы начать", + "chat_originalMessageNotFound": "Исходное сообщение не найдено", + "chat_replyingTo": "Ответ для {name}", + "chat_replyTo": "Ответить {name}", + "chat_location": "Местоположение", + "chat_sendMessageTo": "Отправить сообщение {contactName}", + "chat_typeMessage": "Напишите сообщение...", + "chat_messageTooLong": "Сообщение слишком длинное (макс. {maxBytes} байт).", + "chat_messageCopied": "Сообщение скопировано", + "chat_messageDeleted": "Сообщение удалено", + "chat_retryingMessage": "Повтор отправки сообщения", + "chat_retryCount": "Попытка {current}/{max}", + "chat_sendGif": "Отправить GIF", + "chat_reply": "Ответить", + "chat_addReaction": "Добавить реакцию", + "chat_me": "Я", + "emojiCategorySmileys": "Смайлы", + "emojiCategoryGestures": "Жесты", + "emojiCategoryHearts": "Сердечки", + "emojiCategoryObjects": "Предметы", + "gifPicker_title": "Выберите GIF", + "gifPicker_searchHint": "Поиск GIF...", + "gifPicker_poweredBy": "Работает на GIPHY", + "gifPicker_noGifsFound": "GIF не найдены", + "gifPicker_failedLoad": "Не удалось загрузить GIF", + "gifPicker_failedSearch": "Не удалось выполнить поиск GIF", + "gifPicker_noInternet": "Нет подключения к интернету", + "debugLog_appTitle": "Журнал отладки приложения", + "debugLog_bleTitle": "Журнал отладки BLE", + "debugLog_copyLog": "Копировать журнал", + "debugLog_clearLog": "Очистить журнал", + "debugLog_copied": "Журнал отладки скопирован", + "debugLog_bleCopied": "Журнал BLE скопирован", + "debugLog_noEntries": "Журнал отладки пока пуст", + "debugLog_enableInSettings": "Включите запись журнала отладки в настройках", + "debugLog_frames": "Фреймы", + "debugLog_rawLogRx": "Сырой журнал приёма", + "debugLog_noBleActivity": "Активность BLE пока отсутствует", + "debugFrame_length": "Длина фрейма: {count} байт", + "debugFrame_command": "Команда: 0x{value}", + "debugFrame_textMessageHeader": "Фрейм текстового сообщения:", + "debugFrame_destinationPubKey": "- Публичный ключ получателя: {pubKey}", + "debugFrame_timestamp": "- Временная метка: {timestamp}", + "debugFrame_flags": "- Флаги: 0x{value}", + "debugFrame_textType": "- Тип текста: {type} ({label})", + "debugFrame_textTypeCli": "CLI", + "debugFrame_textTypePlain": "Обычный", + "debugFrame_text": "- Текст: \"{text}\"", + "debugFrame_hexDump": "Шестнадцатеричный дамп:", + "chat_pathManagement": "Управление маршрутами", + "chat_routingMode": "Режим маршрутизации", + "chat_autoUseSavedPath": "Авто (использовать сохранённый маршрут)", + "chat_forceFloodMode": "Принудительный режим рассылки", + "chat_recentAckPaths": "Недавние подтверждённые маршруты (нажмите, чтобы использовать):", + "chat_pathHistoryFull": "История маршрутов заполнена. Удалите записи, чтобы добавить новые.", + "chat_hopSingular": "хоп", + "chat_hopPlural": "хопов", + "chat_hopsCount": "{count} {plural, select, one {хоп} other {хопов}}", + "chat_successes": "успешно", + "chat_removePath": "Удалить маршрут", + "chat_noPathHistoryYet": "История маршрутов пока пуста.\nОтправьте сообщение, чтобы обнаружить маршруты.", + "chat_pathActions": "Действия с маршрутом:", + "chat_setCustomPath": "Указать маршрут вручную", + "chat_setCustomPathSubtitle": "Вручную задать маршрут передачи", + "chat_clearPath": "Очистить маршрут", + "chat_clearPathSubtitle": "Принудительно обновить маршрут при следующей отправке", + "chat_pathCleared": "Маршрут очищен. Следующее сообщение обновит маршрут.", + "chat_floodModeSubtitle": "Используйте переключатель маршрутизации в панели приложения", + "chat_floodModeEnabled": "Режим рассылки включён. Отключите через значок маршрутизации в панели приложения.", + "chat_fullPath": "Полный маршрут", + "chat_pathDetailsNotAvailable": "Детали маршрута ещё недоступны. Попробуйте отправить сообщение для обновления.", + "chat_pathSetHops": "Маршрут установлен: {hopCount} {plural, select, one {хоп} other {хопов}} — {status}", + "chat_pathSavedLocally": "Сохранено локально. Подключитесь для синхронизации.", + "chat_pathDeviceConfirmed": "Подтверждено устройством.", + "chat_pathDeviceNotConfirmed": "Ещё не подтверждено устройством.", + "chat_type": "Тип", + "chat_path": "Маршрут", + "chat_publicKey": "Публичный ключ", + "chat_compressOutgoingMessages": "Сжимать исходящие сообщения", + "chat_floodForced": "Рассылка (принудительно)", + "chat_directForced": "Прямой (принудительно)", + "chat_hopsForced": "{count} хоп(ов) (принудительно)", + "chat_floodAuto": "Рассылка (авто)", + "chat_direct": "Прямой", + "chat_poiShared": "Точка интереса отправлена", + "chat_unread": "Непрочитанных: {count}", + "map_title": "Карта нод", + "map_noNodesWithLocation": "Нет нод с данными о местоположении", + "map_nodesNeedGps": "Ноды должны передавать свои GPS-координаты, чтобы отображаться на карте", + "map_nodesCount": "Нод: {count}", + "map_pinsCount": "Меток: {count}", + "map_chat": "Чат", + "map_repeater": "Репитер", + "map_room": "Комната", + "map_sensor": "Сенсор", + "map_pinDm": "Метка (ЛС)", + "map_pinPrivate": "Метка (Приватная)", + "map_pinPublic": "Метка (Публичная)", + "map_lastSeen": "Последнее появление", + "map_disconnectConfirm": "Вы уверены, что хотите отключиться от этого устройства?", + "map_from": "От", + "map_source": "Источник", + "map_flags": "Флаги", + "map_shareMarkerHere": "Поделиться меткой здесь", + "map_pinLabel": "Метка", + "map_label": "Подпись", + "map_pointOfInterest": "Точка интереса", + "map_sendToContact": "Отправить контакту", + "map_sendToChannel": "Отправить в канал", + "map_noChannelsAvailable": "Нет доступных каналов", + "map_publicLocationShare": "Публичная передача местоположения", + "map_publicLocationShareConfirm": "Вы собираетесь поделиться местоположением в {channelLabel}. Этот канал публичный, и любой, у кого есть PSK, сможет его увидеть.", + "map_connectToShareMarkers": "Подключитесь к устройству, чтобы делиться метками", + "map_filterNodes": "Фильтр нод", + "map_nodeTypes": "Типы нод", + "map_chatNodes": "Чат-ноды", + "map_repeaters": "Репитеры", + "map_otherNodes": "Другие ноды", + "map_keyPrefix": "Префикс ключа", + "map_filterByKeyPrefix": "Фильтр по префиксу ключа", + "map_publicKeyPrefix": "Префикс публичного ключа", + "map_markers": "Метки", + "map_showSharedMarkers": "Показывать общие метки", + "map_lastSeenTime": "Время последнего появления", + "map_sharedPin": "Общая метка", + "map_joinRoom": "Присоединиться к комнате", + "map_manageRepeater": "Управление репитером", + "mapCache_title": "Кэш офлайн-карты", + "mapCache_selectAreaFirst": "Сначала выберите область для кэширования", + "mapCache_noTilesToDownload": "Нет плиток для загрузки в этой области", + "mapCache_downloadTilesTitle": "Загрузить плитки", + "mapCache_downloadTilesPrompt": "Загрузить {count} плиток для офлайн-использования?", + "mapCache_downloadAction": "Загрузить", + "mapCache_cachedTiles": "Закэшировано {count} плиток", + "mapCache_cachedTilesWithFailed": "Закэшировано {downloaded} плиток ({failed} не загружено)", + "mapCache_clearOfflineCacheTitle": "Очистить офлайн-кэш", + "mapCache_clearOfflineCachePrompt": "Удалить все закэшированные плитки карты?", + "mapCache_offlineCacheCleared": "Офлайн-кэш очищен", + "mapCache_noAreaSelected": "Область не выбрана", + "mapCache_cacheArea": "Область кэширования", + "mapCache_useCurrentView": "Использовать текущий вид", + "mapCache_zoomRange": "Диапазон масштаба", + "mapCache_estimatedTiles": "Оценочное количество плиток: {count}", + "mapCache_downloadedTiles": "Загружено {completed} из {total}", + "mapCache_downloadTilesButton": "Загрузить плитки", + "mapCache_clearCacheButton": "Очистить кэш", + "mapCache_failedDownloads": "Неудачных загрузок: {count}", + "mapCache_boundsLabel": "С {north}, Ю {south}, В {east}, З {west}", + "time_justNow": "Только что", + "time_minutesAgo": "{minutes} мин назад", + "time_hoursAgo": "{hours} ч назад", + "time_daysAgo": "{days} дн. назад", + "time_hour": "час", + "time_hours": "часов", + "time_day": "день", + "time_days": "дней", + "time_week": "неделя", + "time_weeks": "недель", + "time_month": "месяц", + "time_months": "месяцев", + "time_minutes": "минут", + "time_allTime": "Всё время", + "dialog_disconnect": "Отключиться", + "dialog_disconnectConfirm": "Вы уверены, что хотите отключиться от этого устройства?", + "login_repeaterLogin": "Вход в репитер", + "login_roomLogin": "Вход на сервер комнат", + "login_password": "Пароль", + "login_enterPassword": "Введите пароль", + "login_savePassword": "Сохранить пароль", + "login_savePasswordSubtitle": "Пароль будет надёжно сохранён на этом устройстве", + "login_repeaterDescription": "Введите пароль репитера для доступа к настройкам и статусу.", + "login_roomDescription": "Введите пароль комнаты для доступа к настройкам и статусу.", + "login_routing": "Маршрутизация", + "login_routingMode": "Режим маршрутизации", + "login_autoUseSavedPath": "Авто (использовать сохранённый маршрут)", + "login_forceFloodMode": "Принудительный режим рассылки", + "login_managePaths": "Управление маршрутами", + "login_login": "Войти", + "login_attempt": "Попытка {current}/{max}", + "login_failed": "Ошибка входа: {error}", + "login_failedMessage": "Не удалось войти. Либо пароль неверен, либо репитер недоступен.", + "common_reload": "Обновить", + "common_clear": "Очистить", + "path_currentPath": "Текущий маршрут: {path}", + "path_usingHopsPath": "Используется маршрут из {count} {plural, select, one {хоп} other {хопов}}", + "path_enterCustomPath": "Введите маршрут вручную", + "path_currentPathLabel": "Текущий маршрут", + "path_hexPrefixInstructions": "Введите 2-символьные шестнадцатеричные префиксы для каждого хопа, разделённые запятыми.", + "path_hexPrefixExample": "Пример: A1,F2,3C (каждый узел использует первый байт своего публичного ключа)", + "path_labelHexPrefixes": "Маршрут (шестнадцатеричные префиксы)", + "path_helperMaxHops": "Максимум 64 хопа. Каждый префикс — 2 шестнадцатеричных символа (1 байт)", + "path_selectFromContacts": "Или выберите из контактов:", + "path_noRepeatersFound": "Репитеры или серверы комнат не найдены.", + "path_customPathsRequire": "Пользовательские маршруты требуют промежуточных узлов, способных ретранслировать сообщения.", + "path_invalidHexPrefixes": "Недопустимые шестнадцатеричные префиксы: {prefixes}", + "path_tooLong": "Маршрут слишком длинный. Максимум 64 хопа.", + "path_setPath": "Установить маршрут", + "repeater_management": "Управление репитером", + "room_management": "Управление сервером комнат", + "repeater_managementTools": "Инструменты управления", + "repeater_status": "Статус", + "repeater_statusSubtitle": "Просмотр статуса, статистики и соседей репитера", + "repeater_telemetry": "Телеметрия", + "repeater_telemetrySubtitle": "Просмотр телеметрии датчиков и системной статистики", + "repeater_cli": "CLI", + "repeater_cliSubtitle": "Отправка команд репитеру", + "repeater_neighbours": "Соседи", + "repeater_neighboursSubtitle": "Просмотр соседей на нулевом хопе.", + "repeater_settings": "Настройки", + "repeater_settingsSubtitle": "Настройка параметров репитера", + "repeater_statusTitle": "Статус репитера", + "repeater_routingMode": "Режим маршрутизации", + "repeater_autoUseSavedPath": "Авто (использовать сохранённый маршрут)", + "repeater_forceFloodMode": "Принудительный режим рассылки", + "repeater_pathManagement": "Управление маршрутами", + "repeater_refresh": "Обновить", + "repeater_statusRequestTimeout": "Время ожидания статуса истекло.", + "repeater_errorLoadingStatus": "Ошибка загрузки статуса: {error}", + "repeater_systemInformation": "Системная информация", + "repeater_battery": "Батарея", + "repeater_clockAtLogin": "Время (при входе)", + "repeater_uptime": "Время работы", + "repeater_queueLength": "Длина очереди", + "repeater_debugFlags": "Флаги отладки", + "repeater_radioStatistics": "Радиостатистика", + "repeater_lastRssi": "Последний RSSI", + "repeater_lastSnr": "Последний SNR", + "repeater_noiseFloor": "Уровень шума", + "repeater_txAirtime": "Время эфира (передача)", + "repeater_rxAirtime": "Время эфира (приём)", + "repeater_packetStatistics": "Статистика пакетов", + "repeater_sent": "Отправлено", + "repeater_received": "Получено", + "repeater_duplicates": "Дубликаты", + "repeater_daysHoursMinsSecs": "{days} дн. {hours}ч {minutes}м {seconds}с", + "repeater_packetTxTotal": "Всего: {total}, Рассылка: {flood}, Прямые: {direct}", + "repeater_packetRxTotal": "Всего: {total}, Рассылка: {flood}, Прямые: {direct}", + "repeater_duplicatesFloodDirect": "Рассылка: {flood}, Прямые: {direct}", + "repeater_duplicatesTotal": "Всего: {total}", + "repeater_settingsTitle": "Настройки репитера", + "repeater_basicSettings": "Основные настройки", + "repeater_repeaterName": "Имя репитера", + "repeater_repeaterNameHelper": "Отображаемое имя этого репитера", + "repeater_adminPassword": "Пароль администратора", + "repeater_adminPasswordHelper": "Пароль с полным доступом", + "repeater_guestPassword": "Гостевой пароль", + "repeater_guestPasswordHelper": "Пароль для доступа только для чтения", + "repeater_radioSettings": "Настройки радио", + "repeater_frequencyMhz": "Частота (МГц)", + "repeater_frequencyHelper": "300–2500 МГц", + "repeater_txPower": "Мощность передачи", + "repeater_txPowerHelper": "1–30 дБм", + "repeater_bandwidth": "Полоса пропускания", + "repeater_spreadingFactor": "Коэффициент расширения", + "repeater_codingRate": "Коэффициент кодирования", + "repeater_locationSettings": "Настройки местоположения", + "repeater_latitude": "Широта", + "repeater_latitudeHelper": "В десятичных градусах (напр., 37.7749)", + "repeater_longitude": "Долгота", + "repeater_longitudeHelper": "В десятичных градусах (напр., -122.4194)", + "repeater_features": "Функции", + "repeater_packetForwarding": "Пересылка пакетов", + "repeater_packetForwardingSubtitle": "Разрешить репитеру пересылать пакеты", + "repeater_guestAccess": "Гостевой доступ", + "repeater_guestAccessSubtitle": "Разрешить гостевой доступ только для чтения", + "repeater_privacyMode": "Режим конфиденциальности", + "repeater_privacyModeSubtitle": "Скрывать имя/местоположение в оповещениях", + "repeater_advertisementSettings": "Настройки анонсирования", + "repeater_localAdvertInterval": "Интервал локальных анонсирований", + "repeater_localAdvertIntervalMinutes": "{minutes} минут", + "repeater_floodAdvertInterval": "Интервал анонсирований рассылкой (flood)", + "repeater_floodAdvertIntervalHours": "{hours} часов", + "repeater_encryptedAdvertInterval": "Интервал зашифрованных анонсирований", + "repeater_dangerZone": "Опасная зона", + "repeater_rebootRepeater": "Перезагрузить репитер", + "repeater_rebootRepeaterSubtitle": "Перезапустить устройство репитера", + "repeater_rebootRepeaterConfirm": "Вы уверены, что хотите перезагрузить этот репитер?", + "repeater_regenerateIdentityKey": "Пересоздать ключ идентификации", + "repeater_regenerateIdentityKeySubtitle": "Сгенерировать новую пару публичного/приватного ключей", + "repeater_regenerateIdentityKeyConfirm": "Это создаст новую идентичность для репитера. Продолжить?", + "repeater_eraseFileSystem": "Стереть файловую систему", + "repeater_eraseFileSystemSubtitle": "Отформатировать файловую систему репитера", + "repeater_eraseFileSystemConfirm": "ВНИМАНИЕ: это удалит все данные на репитере. Действие нельзя отменить!", + "repeater_eraseSerialOnly": "Очистка доступна только через последовательную консоль.", + "repeater_commandSent": "Команда отправлена: {command}", + "repeater_errorSendingCommand": "Ошибка отправки команды: {error}", + "repeater_confirm": "Подтвердить", + "repeater_settingsSaved": "Настройки успешно сохранены", + "repeater_errorSavingSettings": "Ошибка сохранения настроек: {error}", + "repeater_refreshBasicSettings": "Обновить основные настройки", + "repeater_refreshRadioSettings": "Обновить настройки радио", + "repeater_refreshTxPower": "Обновить мощность передачи", + "repeater_refreshLocationSettings": "Обновить настройки местоположения", + "repeater_refreshPacketForwarding": "Обновить пересылку пакетов", + "repeater_refreshGuestAccess": "Обновить гостевой доступ", + "repeater_refreshPrivacyMode": "Обновить режим конфиденциальности", + "repeater_refreshAdvertisementSettings": "Обновить настройки анонсирований", + "repeater_refreshed": "{label} обновлён", + "repeater_errorRefreshing": "Ошибка обновления {label}", + "repeater_cliTitle": "CLI репитера", + "repeater_debugNextCommand": "Отладка следующей команды", + "repeater_commandHelp": "Справка по командам", + "repeater_clearHistory": "Очистить историю", + "repeater_noCommandsSent": "Команды ещё не отправлялись", + "repeater_typeCommandOrUseQuick": "Введите команду ниже или используйте быстрые команды", + "repeater_enterCommandHint": "Введите команду...", + "repeater_previousCommand": "Предыдущая команда", + "repeater_nextCommand": "Следующая команда", + "repeater_enterCommandFirst": "Сначала введите команду", + "repeater_cliCommandFrameTitle": "Фрейм CLI-команды", + "repeater_cliCommandError": "Ошибка: {error}", + "repeater_cliQuickGetName": "Получить имя", + "repeater_cliQuickGetRadio": "Получить радио", + "repeater_cliQuickGetTx": "Получить TX", + "repeater_cliQuickNeighbors": "Соседи", + "repeater_cliQuickVersion": "Версия", + "repeater_cliQuickAdvertise": "Анонсировать", + "repeater_cliQuickClock": "Время", + "repeater_cliHelpAdvert": "Отправляет пакет анонсирования", + "repeater_cliHelpReboot": "Перезагружает устройство. (обычно вы получите «Тайм-аут» — это нормально)", + "repeater_cliHelpClock": "Показывает текущее время по часам устройства.", + "repeater_cliHelpPassword": "Устанавливает новый пароль администратора для устройства.", + "repeater_cliHelpVersion": "Показывает версию устройства и дату сборки прошивки.", + "repeater_cliHelpClearStats": "Сбрасывает различные счётчики статистики в ноль.", + "repeater_cliHelpSetAf": "Устанавливает коэффициент времени в эфире.", + "repeater_cliHelpSetTx": "Устанавливает мощность передачи LoRa в дБм. (требуется перезагрузка)", + "repeater_cliHelpSetRepeat": "Включает или отключает роль репитера для этой ноды.", + "repeater_cliHelpSetAllowReadOnly": "(Сервер комнат) Если «on», то вход без пароля разрешён, но публиковать в комнату нельзя (только чтение)", + "repeater_cliHelpSetFloodMax": "Устанавливает максимальное число хопов для входящих пакетов в режиме рассылки (если >= макс., пакет не пересылается)", + "repeater_cliHelpSetIntThresh": "Устанавливает порог интерференции (в дБ). По умолчанию 14. Установите 0, чтобы отключить обнаружение помех.", + "repeater_cliHelpSetAgcResetInterval": "Устанавливает интервал сброса автоматической регулировки усиления. Установите 0, чтобы отключить.", + "repeater_cliHelpSetMultiAcks": "Включает или отключает функцию «двойных ACK».", + "repeater_cliHelpSetAdvertInterval": "Устанавливает интервал (в минутах) отправки локального (нулевой хоп) анонсирования. Установите 0, чтобы отключить.", + "repeater_cliHelpSetFloodAdvertInterval": "Устанавливает интервал (в часах) отправки анонсирований рассылкой. Установите 0, чтобы отключить.", + "repeater_cliHelpSetGuestPassword": "Устанавливает/обновляет гостевой пароль. (для репитеров гости могут отправлять запрос «Get Stats»)", + "repeater_cliHelpSetName": "Устанавливает имя в оповещениях.", + "repeater_cliHelpSetLat": "Устанавливает широту для карты в оповещениях. (десятичные градусы)", + "repeater_cliHelpSetLon": "Устанавливает долготу для карты в оповещениях. (десятичные градусы)", + "repeater_cliHelpSetRadio": "Устанавливает полностью новые параметры радио и сохраняет их в настройки. Требуется команда «reboot» для применения.", + "repeater_cliHelpSetRxDelay": "Устанавливает (экспериментально) базовую задержку (>1 для эффекта) для принятых пакетов на основе качества сигнала. Установите 0, чтобы отключить.", + "repeater_cliHelpSetTxDelay": "Устанавливает множитель времени в эфире для пакета в режиме рассылки и применяет случайную задержку перед пересылкой (чтобы уменьшить коллизии).", + "repeater_cliHelpSetDirectTxDelay": "То же, что txdelay, но для случайной задержки пересылки пакетов в прямом режиме.", + "repeater_cliHelpSetBridgeEnabled": "Включить/выключить мост.", + "repeater_cliHelpSetBridgeDelay": "Установить задержку перед ретрансляцией пакетов.", + "repeater_cliHelpSetBridgeSource": "Выбрать, будет ли мост ретранслировать полученные или отправленные пакеты.", + "repeater_cliHelpSetBridgeBaud": "Установить скорость последовательного соединения для мостов RS232.", + "repeater_cliHelpSetBridgeSecret": "Установить секрет моста для мостов ESP-NOW.", + "repeater_cliHelpSetAdcMultiplier": "Устанавливает пользовательский коэффициент коррекции напряжения батареи (поддерживается только на некоторых платах).", + "repeater_cliHelpTempRadio": "Устанавливает временные параметры радио на заданное число минут, затем возвращает исходные. (НЕ сохраняется в настройки).", + "repeater_cliHelpSetPerm": "Изменяет ACL. Удаляет запись (по префиксу публичного ключа), если «permissions» равен нулю. Добавляет новую запись, если указан полный ключ и он отсутствует в ACL. Обновляет запись по совпадению префикса. Биты прав зависят от роли прошивки, но младшие 2 бита: 0 (Гость), 1 (Только чтение), 2 (Чтение/запись), 3 (Админ)", + "repeater_cliHelpGetBridgeType": "Получает тип моста: none, rs232, espnow", + "repeater_cliHelpLogStart": "Начинает запись пакетов в файловую систему.", + "repeater_cliHelpLogStop": "Останавливает запись пакетов в файловую систему.", + "repeater_cliHelpLogErase": "Удаляет журналы пакетов из файловой системы.", + "repeater_cliHelpNeighbors": "Показывает список других репитеров, услышанных через оповещения нулевого хопа. Каждая строка: префикс-id-в-hex:временная-метка:snr×4", + "repeater_cliHelpNeighborRemove": "Удаляет первую подходящую запись (по префиксу публичного ключа в hex) из списка соседей.", + "repeater_cliHelpRegion": "(только через последовательный порт) Показывает все определённые регионы и текущие права на рассылку.", + "repeater_cliHelpRegionLoad": "ПРИМЕЧАНИЕ: это специальная многострочная команда. Каждая следующая строка — имя региона (с отступом пробелами для указания иерархии, минимум один пробел). Завершается пустой строкой.", + "repeater_cliHelpRegionGet": "Ищет регион по префиксу имени (или «*» для глобальной области). Отвечает: «-> имя-региона (родитель) 'F'»", + "repeater_cliHelpRegionPut": "Добавляет или обновляет определение региона с заданным именем.", + "repeater_cliHelpRegionRemove": "Удаляет определение региона с заданным именем. (должно точно совпадать и не иметь дочерних регионов)", + "repeater_cliHelpRegionAllowf": "Разрешает рассылку («F»lood) для заданного региона. («*» для глобальной/устаревшей области)", + "repeater_cliHelpRegionDenyf": "Запрещает рассылку («F»lood) для заданного региона. (НЕ рекомендуется для глобальной области!)", + "repeater_cliHelpRegionHome": "Показывает текущий «домашний» регион. (Пока не используется, зарезервировано на будущее)", + "repeater_cliHelpRegionHomeSet": "Устанавливает «домашний» регион.", + "repeater_cliHelpRegionSave": "Сохраняет список/карту регионов в память.", + "repeater_cliHelpGps": "Показывает статус GPS. Если GPS выключен — отвечает только «off». Если включён — показывает статус, фиксацию, количество спутников.", + "repeater_cliHelpGpsOnOff": "Переключает состояние питания GPS.", + "repeater_cliHelpGpsSync": "Синхронизирует время ноды с часами GPS.", + "repeater_cliHelpGpsSetLoc": "Устанавливает позицию ноды по координатам GPS и сохраняет в настройки.", + "repeater_cliHelpGpsAdvert": "Показывает конфигурацию передачи местоположения в анонсированиях:\n- none: не включать местоположение\n- share: передавать GPS-координаты (из SensorManager)\n- prefs: передавать координаты из настроек", + "repeater_cliHelpGpsAdvertSet": "Устанавливает конфигурацию передачи местоположения.", + "repeater_commandsListTitle": "Список команд", + "repeater_commandsListNote": "ПРИМЕЧАНИЕ: для большинства команд «set ...» существуют соответствующие команды «get ...».", + "repeater_general": "Общие", + "repeater_settingsCategory": "Настройки", + "repeater_bridge": "Мост", + "repeater_logging": "Журналирование", + "repeater_neighborsRepeaterOnly": "Соседи (только для репитеров)", + "repeater_regionManagementRepeaterOnly": "Управление регионами (только для репитеров)", + "repeater_regionNote": "Команды регионов введены для управления определениями регионов и правами доступа.", + "repeater_gpsManagement": "Управление GPS", + "repeater_gpsNote": "Команда gps введена для управления параметрами, связанными с местоположением.", + "telemetry_receivedData": "Полученные телеметрические данные", + "telemetry_requestTimeout": "Время ожидания телеметрии истекло.", + "telemetry_errorLoading": "Ошибка загрузки телеметрии: {error}", + "telemetry_noData": "Данные телеметрии недоступны.", + "telemetry_channelTitle": "Канал {channel}", + "telemetry_batteryLabel": "Батарея", + "telemetry_voltageLabel": "Напряжение", + "telemetry_mcuTemperatureLabel": "Температура МК", + "telemetry_temperatureLabel": "Температура", + "telemetry_currentLabel": "Ток", + "telemetry_batteryValue": "{percent}% / {volts}В", + "telemetry_voltageValue": "{volts}В", + "telemetry_currentValue": "{amps}А", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "neighbors_receivedData": "Полученные данные о соседях", + "neighbors_requestTimedOut": "Время ожидания данных о соседях истекло.", + "neighbors_errorLoading": "Ошибка загрузки соседей: {error}", + "neighbors_repeatersNeighbours": "Соседи репитеров", + "neighbors_noData": "Данные о соседях недоступны.", + "neighbors_unknownContact": "Неизвестный {pubkey}", + "neighbors_heardA ago": "Слышали: {time} назад", + "channelPath_title": "Путь пакета", + "channelPath_viewMap": "Посмотреть на карте", + "channelPath_otherObservedPaths": "Другие наблюдаемые пути", + "channelPath_repeaterHops": "Хопы через репитеры", + "channelPath_noHopDetails": "Детали хопов для этого пакета не предоставлены.", + "channelPath_messageDetails": "Детали сообщения", + "channelPath_senderLabel": "Отправитель", + "channelPath_timeLabel": "Время", + "channelPath_repeatsLabel": "Повторы", + "channelPath_pathLabel": "Путь {index}", + "channelPath_observedLabel": "Наблюдаемый", + "channelPath_observedPathTitle": "Наблюдаемый путь {index} • {hops}", + "channelPath_noLocationData": "Нет данных о местоположении", + "channelPath_timeWithDate": "{day}/{month} {time}", + "channelPath_timeOnly": "{time}", + "channelPath_unknownPath": "Неизвестный", + "channelPath_floodPath": "Рассылка", + "channelPath_directPath": "Прямой", + "channelPath_observedZeroOf": "0 из {total} хопов", + "channelPath_observedSomeOf": "{observed} из {total} хопов", + "channelPath_mapTitle": "Карта пути", + "channelPath_noRepeaterLocations": "Нет данных о местоположении репитеров для этого пути.", + "channelPath_primaryPath": "Путь {index} (Основной)", + "channelPath_pathLabelTitle": "Путь", + "channelPath_observedPathHeader": "Наблюдаемый путь", + "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_noHopDetailsAvailable": "Детали хопов для этого пакета недоступны.", + "channelPath_unknownRepeater": "Неизвестный репитер", + "community_title": "Сообщество", + "community_create": "Создать сообщество", + "community_createDesc": "Создать новое сообщество и поделиться через QR-код.", + "community_join": "Присоединиться", + "community_joinTitle": "Присоединиться к сообществу", + "community_joinConfirmation": "Вы хотите присоединиться к сообществу \"{name}\"?", + "community_scanQr": "Сканировать QR-код сообщества", + "community_scanInstructions": "Наведите камеру на QR-код сообщества", + "community_showQr": "Показать QR-код", + "community_publicChannel": "Публичный канал сообщества", + "community_hashtagChannel": "Хэштег-канал сообщества", + "community_name": "Имя сообщества", + "community_enterName": "Введите имя сообщества", + "community_created": "Сообщество \"{name}\" создано", + "community_joined": "Присоединились к сообществу \"{name}\"", + "community_qrTitle": "Поделиться сообществом", + "community_qrInstructions": "Отсканируйте этот QR-код, чтобы присоединиться к \"{name}\"", + "community_hashtagPrivacyHint": "Хэштег-каналы сообщества доступны только его участникам", + "community_invalidQrCode": "Недопустимый QR-код сообщества", + "community_alreadyMember": "Уже участник", + "community_alreadyMemberMessage": "Вы уже участник сообщества \"{name}\".", + "community_addPublicChannel": "Добавить публичный канал сообщества", + "community_addPublicChannelHint": "Автоматически добавить публичный канал для этого сообщества", + "community_noCommunities": "Вы ещё не присоединились ни к одному сообществу", + "community_scanOrCreate": "Отсканируйте QR-код или создайте сообщество, чтобы начать", + "community_manageCommunities": "Управление сообществами", + "community_delete": "Покинуть сообщество", + "community_deleteConfirm": "Покинуть \"{name}\"?", + "community_deleteChannelsWarning": "Это также удалит {count} канал(ов) и их сообщения.", + "community_deleted": "Покинули сообщество \"{name}\"", + "community_regenerateSecret": "Пересоздать секрет", + "community_regenerateSecretConfirm": "Пересоздать секретный ключ для \"{name}\"? Все участники должны будут отсканировать новый QR-код для продолжения общения.", + "community_regenerate": "Пересоздать", + "community_secretRegenerated": "Секрет пересоздан для \"{name}\"", + "community_updateSecret": "Обновить секрет", + "community_secretUpdated": "Секрет обновлён для \"{name}\"", + "community_scanToUpdateSecret": "Отсканируйте новый QR-код, чтобы обновить секрет для \"{name}\"", + "community_addHashtagChannel": "Добавить хэштег-канал сообщества", + "community_addHashtagChannelDesc": "Добавить хэштег-канал для этого сообщества", + "community_selectCommunity": "Выбрать сообщество", + "community_regularHashtag": "Обычный хэштег", + "community_regularHashtagDesc": "Публичный хэштег (любой может присоединиться)", + "community_communityHashtag": "Хэштег сообщества", + "community_communityHashtagDesc": "Доступен только участникам сообщества", + "community_forCommunity": "Для {name}", + "listFilter_tooltip": "Фильтр и сортировка", + "listFilter_sortBy": "Сортировка по", + "listFilter_latestMessages": "Последние сообщения", + "listFilter_heardRecently": "Слышали недавно", + "listFilter_az": "По алфавиту", + "listFilter_filters": "Фильтры", + "listFilter_all": "Все", + "listFilter_users": "Пользователи", + "listFilter_repeaters": "Репитеры", + "listFilter_roomServers": "Серверы комнат", + "listFilter_unreadOnly": "Только непрочитанные", + "listFilter_newGroup": "Новая группа" +} \ No newline at end of file From fa514533eb921770a5610534a4a8e7a61791a276 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Fri, 23 Jan 2026 17:56:06 -0700 Subject: [PATCH 3/5] feat: add ChatScrollController and JumpToBottomButton for improved chat scrolling experience - Implemented ChatScrollController to manage scroll behavior and visibility of jump-to-bottom button. - Added functionality to automatically scroll to the bottom when the keyboard opens. - Created JumpToBottomButton widget that appears when the user scrolls up, allowing quick navigation back to the bottom of the chat. --- .../reports/problems/problems-report.html | 663 ++++++++++++++++++ lib/connector/meshcore_connector.dart | 5 + lib/helpers/chat_scroll_controller.dart | 68 ++ lib/screens/channel_chat_screen.dart | 244 ++++--- lib/screens/chat_screen.dart | 211 ++++-- lib/widgets/gif_message.dart | 67 +- lib/widgets/jump_to_bottom_button.dart | 29 + 7 files changed, 1097 insertions(+), 190 deletions(-) create mode 100644 android/build/reports/problems/problems-report.html create mode 100644 lib/helpers/chat_scroll_controller.dart create mode 100644 lib/widgets/jump_to_bottom_button.dart diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html new file mode 100644 index 0000000..2220133 --- /dev/null +++ b/android/build/reports/problems/problems-report.html @@ -0,0 +1,663 @@ + + + + + + + + + + + + + Gradle Configuration Cache + + + +
+ +
+ Loading... +
+ + + + + + diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 29f92af..28b1082 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -146,6 +146,7 @@ class MeshCoreConnector extends ChangeNotifier { final Set _knownContactKeys = {}; final Map _contactLastReadMs = {}; final Map _channelLastReadMs = {}; + bool _unreadStateLoaded = false; final Map _pendingRepeaterAcks = {}; String? _activeContactKey; int? _activeChannelIndex; @@ -317,6 +318,7 @@ class MeshCoreConnector extends ChangeNotifier { } int getUnreadCountForContactKey(String contactKeyHex) { + if (!_unreadStateLoaded) return 0; if (!_shouldTrackUnreadForContactKey(contactKeyHex)) return 0; final messages = _conversations[contactKeyHex]; if (messages == null || messages.isEmpty) return 0; @@ -336,6 +338,7 @@ class MeshCoreConnector extends ChangeNotifier { } int getUnreadCountForChannelIndex(int channelIndex) { + if (!_unreadStateLoaded) return 0; final messages = _channelMessages[channelIndex]; if (messages == null || messages.isEmpty) return 0; final lastReadMs = _channelLastReadMs[channelIndex] ?? 0; @@ -350,6 +353,7 @@ class MeshCoreConnector extends ChangeNotifier { } int getTotalUnreadCount() { + if (!_unreadStateLoaded) return 0; var total = 0; // Count unread contact messages for (final contact in _contacts) { @@ -381,6 +385,7 @@ class MeshCoreConnector extends ChangeNotifier { _channelLastReadMs ..clear() ..addAll(await _unreadStore.loadChannelLastRead()); + _unreadStateLoaded = true; notifyListeners(); } diff --git a/lib/helpers/chat_scroll_controller.dart b/lib/helpers/chat_scroll_controller.dart new file mode 100644 index 0000000..d2c73fb --- /dev/null +++ b/lib/helpers/chat_scroll_controller.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; + +class ChatScrollController extends ScrollController { + final ValueNotifier showJumpToBottom = ValueNotifier(false); + VoidCallback? onScrollNearTop; + + static const _bottomThreshold = 100.0; + static const _topThreshold = 50.0; + + ChatScrollController() { + addListener(_handleScroll); + } + + void _handleScroll() { + if (!hasClients) return; + final pos = position; + + // With reverse: true, position 0 is bottom, maxScrollExtent is top + // Show jump button when scrolled away from bottom (position > threshold) + final isAtBottom = pos.pixels <= _bottomThreshold; + if (showJumpToBottom.value == isAtBottom) { + showJumpToBottom.value = !isAtBottom; + } + + // Pagination trigger when scrolled near top (maxScrollExtent) + if (pos.pixels >= pos.maxScrollExtent - _topThreshold) { + onScrollNearTop?.call(); + } + } + + void jumpToBottom() { + if (hasClients && position.maxScrollExtent > 0) { + animateTo( + 0, // With reverse: true, position 0 is bottom + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + } + + void handleKeyboardOpen() { + // Simple: just scroll to bottom when keyboard opens + if (hasClients) { + animateTo( + 0, // With reverse: true, position 0 is bottom + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + ); + } + } + + void scrollToBottomIfAtBottom() { + // Only scroll if jump button is NOT showing (i.e., already at bottom) + if (!showJumpToBottom.value && hasClients && position.maxScrollExtent > 0) { + animateTo( + 0, // With reverse: true, position 0 is bottom + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + ); + } + } + + @override + void dispose() { + showJumpToBottom.dispose(); + super.dispose(); + } +} diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 380c7ce..f45ed34 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -8,6 +8,7 @@ import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; +import '../helpers/chat_scroll_controller.dart'; import '../connector/meshcore_protocol.dart'; import '../helpers/link_handler.dart'; import '../helpers/utf8_length_limiter.dart'; @@ -17,6 +18,7 @@ import '../models/channel_message.dart'; import '../utils/emoji_utils.dart'; import '../widgets/emoji_picker.dart'; import '../widgets/gif_message.dart'; +import '../widgets/jump_to_bottom_button.dart'; import '../widgets/gif_picker.dart'; import 'channel_message_path_screen.dart'; import 'map_screen.dart'; @@ -35,42 +37,51 @@ class ChannelChatScreen extends StatefulWidget { class _ChannelChatScreenState extends State { final TextEditingController _textController = TextEditingController(); - final ScrollController _scrollController = ScrollController(); + final ChatScrollController _scrollController = ChatScrollController(); + final FocusNode _textFieldFocusNode = FocusNode(); ChannelMessage? _replyingToMessage; final Map _messageKeys = {}; + bool _isLoadingOlder = false; @override void initState() { super.initState(); + _textFieldFocusNode.addListener(_onTextFieldFocusChange); + _scrollController.onScrollNearTop = _loadOlderMessages; SchedulerBinding.instance.addPostFrameCallback((_) { if (!mounted) return; context.read().setActiveChannel(widget.channel.index); - - // Scroll to bottom when opening channel chat - use SchedulerBinding for next frame - if (_scrollController.hasClients) { - _scrollController.jumpTo(_scrollController.position.maxScrollExtent); - } }); } + void _onTextFieldFocusChange() { + if (_textFieldFocusNode.hasFocus && mounted) { + _scrollController.handleKeyboardOpen(); + } + } + + Future _loadOlderMessages() async { + if (_isLoadingOlder) return; + setState(() => _isLoadingOlder = true); + + final connector = context.read(); + await connector.loadOlderChannelMessages(widget.channel.index); + + if (mounted) { + setState(() => _isLoadingOlder = false); + } + } + @override void dispose() { context.read().setActiveChannel(null); + _textFieldFocusNode.removeListener(_onTextFieldFocusChange); + _textFieldFocusNode.dispose(); _textController.dispose(); _scrollController.dispose(); super.dispose(); } - void _scrollToBottom() { - if (_scrollController.hasClients) { - _scrollController.animateTo( - _scrollController.position.maxScrollExtent, - duration: const Duration(milliseconds: 300), - curve: Curves.easeOut, - ); - } - } - void _setReplyingTo(ChannelMessage message) { setState(() { _replyingToMessage = message; @@ -155,10 +166,6 @@ class _ChannelChatScreenState extends State { builder: (context, connector, child) { final messages = connector.getChannelMessages(widget.channel); - SchedulerBinding.instance.addPostFrameCallback((_) { - _scrollToBottom(); - }); - if (messages.isEmpty) { return Center( child: Column( @@ -192,20 +199,51 @@ class _ChannelChatScreenState extends State { ); } - return ListView.builder( - controller: _scrollController, - padding: const EdgeInsets.all(8), - itemCount: messages.length, - itemBuilder: (context, index) { - final message = messages[index]; - if (!_messageKeys.containsKey(message.messageId)) { - _messageKeys[message.messageId] = GlobalKey(); - } - return Container( - key: _messageKeys[message.messageId]!, - child: _buildMessageBubble(message), - ); - }, + // Reverse messages so newest appear at bottom with reverse: true + final reversedMessages = messages.reversed.toList(); + final itemCount = reversedMessages.length + (_isLoadingOlder ? 1 : 0); + + // Auto-scroll to bottom if user is already at bottom + WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollController.scrollToBottomIfAtBottom(); + }); + + return Stack( + children: [ + ListView.builder( + reverse: true, // List grows from bottom up + controller: _scrollController, + padding: const EdgeInsets.all(8), + itemCount: itemCount, + itemBuilder: (context, index) { + // Loading indicator now appears at end (bottom) of reversed list + if (_isLoadingOlder && index == itemCount - 1) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Center( + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ), + ); + } + final messageIndex = index; + final message = reversedMessages[messageIndex]; + if (!_messageKeys.containsKey(message.messageId)) { + _messageKeys[message.messageId] = GlobalKey(); + } + return Container( + key: _messageKeys[message.messageId]!, + child: _buildMessageBubble(message), + ); + }, + ), + JumpToBottomButton( + scrollController: _scrollController, + ), + ], ); }, ), @@ -243,7 +281,9 @@ class _ChannelChatScreenState extends State { onTap: () => _showMessagePathInfo(message), onLongPress: () => _showMessageActions(message), child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + padding: gifId != null + ? const EdgeInsets.all(4) + : const EdgeInsets.symmetric(horizontal: 12, vertical: 8), constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.65, ), @@ -257,15 +297,20 @@ class _ChannelChatScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!isOutgoing) ...[ - Text( - message.senderName, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, + Padding( + padding: gifId != null + ? const EdgeInsets.only(left: 8, top: 4, bottom: 4) + : EdgeInsets.zero, + child: Text( + message.senderName, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), ), ), - const SizedBox(height: 4), + if (gifId == null) const SizedBox(height: 4), ], if (message.replyToMessageId != null) ...[ _buildReplyPreview(message), @@ -274,12 +319,15 @@ class _ChannelChatScreenState extends State { if (poi != null) _buildPoiMessage(context, poi, isOutgoing) else if (gifId != null) - GifMessage( - url: 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, - fallbackTextColor: isOutgoing - ? Theme.of(context).colorScheme.onPrimaryContainer.withValues(alpha: 0.7) - : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: GifMessage( + url: 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: Colors.transparent, + fallbackTextColor: isOutgoing + ? Theme.of(context).colorScheme.onPrimaryContainer.withValues(alpha: 0.7) + : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), + ), ) else Linkify( @@ -299,46 +347,56 @@ class _ChannelChatScreenState extends State { ), if (displayPath.isNotEmpty) ...[ const SizedBox(height: 4), - Text( - 'via ${_formatPathPrefixes(displayPath)}', - style: TextStyle(fontSize: 11, color: Colors.grey[600]), + Padding( + padding: gifId != null + ? const EdgeInsets.symmetric(horizontal: 8) + : EdgeInsets.zero, + child: Text( + 'via ${_formatPathPrefixes(displayPath)}', + style: TextStyle(fontSize: 11, color: Colors.grey[600]), + ), ), ], const SizedBox(height: 4), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - _formatTime(message.timestamp), - style: TextStyle( - fontSize: 11, - color: Colors.grey[600], - ), - ), - if (message.repeatCount > 0) ...[ - const SizedBox(width: 6), - Icon(Icons.repeat, size: 12, color: Colors.grey[600]), - const SizedBox(width: 2), + Padding( + padding: gifId != null + ? const EdgeInsets.only(left: 8, right: 8, bottom: 4) + : EdgeInsets.zero, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ Text( - '${message.repeatCount}', - style: TextStyle(fontSize: 11, color: Colors.grey[600]), + _formatTime(message.timestamp), + style: TextStyle( + fontSize: 11, + color: Colors.grey[600], + ), ), + if (message.repeatCount > 0) ...[ + const SizedBox(width: 6), + Icon(Icons.repeat, size: 12, color: Colors.grey[600]), + const SizedBox(width: 2), + Text( + '${message.repeatCount}', + style: TextStyle(fontSize: 11, color: Colors.grey[600]), + ), + ], + if (isOutgoing) ...[ + const SizedBox(width: 4), + Icon( + message.status == ChannelMessageStatus.sent + ? Icons.check + : message.status == ChannelMessageStatus.pending + ? Icons.schedule + : Icons.error_outline, + size: 14, + color: message.status == ChannelMessageStatus.failed + ? Colors.red + : Colors.grey[600], + ), + ], ], - if (isOutgoing) ...[ - const SizedBox(width: 4), - Icon( - message.status == ChannelMessageStatus.sent - ? Icons.check - : message.status == ChannelMessageStatus.pending - ? Icons.schedule - : Icons.error_outline, - size: 14, - color: message.status == ChannelMessageStatus.failed - ? Colors.red - : Colors.grey[600], - ), - ], - ], + ), ), ], ), @@ -377,8 +435,7 @@ class _ChannelChatScreenState extends State { url: 'https://media.giphy.com/media/$gifId/giphy.gif', backgroundColor: colorScheme.surfaceContainerHighest, fallbackTextColor: previewTextColor, - width: 120, - height: 80, + maxSize: 80, ), ); } else if (poi != null) { @@ -703,14 +760,16 @@ class _ChannelChatScreenState extends State { return Row( children: [ Expanded( - child: GifMessage( - url: 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: - Theme.of(context).colorScheme.surfaceContainerHighest, - fallbackTextColor: - Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), - width: 160, - height: 110, + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: GifMessage( + url: 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: + Theme.of(context).colorScheme.surfaceContainerHighest, + fallbackTextColor: + Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), + maxSize: 160, + ), ), ), const SizedBox(width: 8), @@ -724,6 +783,7 @@ class _ChannelChatScreenState extends State { return TextField( controller: _textController, + focusNode: _textFieldFocusNode, inputFormatters: [ Utf8LengthLimitingTextInputFormatter(maxBytes), ], diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 079f25d..efc3537 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -11,6 +11,7 @@ import 'package:latlong2/latlong.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; +import '../helpers/chat_scroll_controller.dart'; import '../helpers/link_handler.dart'; import '../helpers/utf8_length_limiter.dart'; import '../models/channel_message.dart'; @@ -22,6 +23,7 @@ import 'map_screen.dart'; import '../utils/emoji_utils.dart'; import '../widgets/emoji_picker.dart'; import '../widgets/gif_message.dart'; +import '../widgets/jump_to_bottom_button.dart'; import '../widgets/gif_picker.dart'; import '../widgets/path_selection_dialog.dart'; import '../utils/app_logger.dart'; @@ -38,25 +40,44 @@ class ChatScreen extends StatefulWidget { class _ChatScreenState extends State { final _textController = TextEditingController(); - final _scrollController = ScrollController(); + final _scrollController = ChatScrollController(); + final _textFieldFocusNode = FocusNode(); + bool _isLoadingOlder = false; @override void initState() { super.initState(); + _textFieldFocusNode.addListener(_onTextFieldFocusChange); + _scrollController.onScrollNearTop = _loadOlderMessages; SchedulerBinding.instance.addPostFrameCallback((_) { if (!mounted) return; context.read().setActiveContact(widget.contact.publicKeyHex); - - // Scroll to bottom when opening chat use SchedulerBinding for next frame - if (_scrollController.hasClients) { - _scrollController.jumpTo(_scrollController.position.maxScrollExtent); - } }); } + void _onTextFieldFocusChange() { + if (_textFieldFocusNode.hasFocus && mounted) { + _scrollController.handleKeyboardOpen(); + } + } + + Future _loadOlderMessages() async { + if (_isLoadingOlder) return; + setState(() => _isLoadingOlder = true); + + final connector = context.read(); + await connector.loadOlderMessages(widget.contact.publicKeyHex); + + if (mounted) { + setState(() => _isLoadingOlder = false); + } + } + @override void dispose() { context.read().setActiveContact(null); + _textFieldFocusNode.removeListener(_onTextFieldFocusChange); + _textFieldFocusNode.dispose(); _textController.dispose(); _scrollController.dispose(); super.dispose(); @@ -169,9 +190,16 @@ class _ChatScreenState extends State { return Column( children: [ Expanded( - child: messages.isEmpty - ? _buildEmptyState() - : _buildMessageList(messages, connector), + child: Stack( + children: [ + messages.isEmpty + ? _buildEmptyState() + : _buildMessageList(messages, connector), + JumpToBottomButton( + scrollController: _scrollController, + ), + ], + ), ), _buildInputBar(connector), ], @@ -203,13 +231,37 @@ class _ChatScreenState extends State { } Widget _buildMessageList(List messages, MeshCoreConnector connector) { + // Reverse messages so newest appear at bottom with reverse: true + final reversedMessages = messages.reversed.toList(); + final itemCount = reversedMessages.length + (_isLoadingOlder ? 1 : 0); + + // Auto-scroll to bottom if user is already at bottom + WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollController.scrollToBottomIfAtBottom(); + }); + return ListView.builder( + reverse: true, // List grows from bottom up controller: _scrollController, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16), - itemCount: messages.length, + itemCount: itemCount, itemBuilder: (context, index) { + // Loading indicator now appears at end (bottom) of reversed list + if (_isLoadingOlder && index == itemCount - 1) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Center( + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ), + ); + } + final messageIndex = index; Contact contact = widget.contact; - final message = messages[index]; + final message = reversedMessages[messageIndex]; String fourByteHex = ''; if (widget.contact.type == advTypeRoom) { contact = _resolveContactFrom4Bytes( @@ -258,13 +310,15 @@ class _ChatScreenState extends State { return Row( children: [ Expanded( - child: GifMessage( - url: 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: colorScheme.surfaceContainerHighest, - fallbackTextColor: - colorScheme.onSurface.withValues(alpha: 0.6), - width: 160, - height: 110, + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: GifMessage( + url: 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: colorScheme.surfaceContainerHighest, + fallbackTextColor: + colorScheme.onSurface.withValues(alpha: 0.6), + maxSize: 160, + ), ), ), const SizedBox(width: 8), @@ -278,6 +332,7 @@ class _ChatScreenState extends State { return TextField( controller: _textController, + focusNode: _textFieldFocusNode, inputFormatters: [ Utf8LengthLimitingTextInputFormatter(maxBytes), ], @@ -339,16 +394,6 @@ class _ChatScreenState extends State { text, ); _textController.clear(); - - Future.delayed(const Duration(milliseconds: 100), () { - if (_scrollController.hasClients) { - _scrollController.animateTo( - _scrollController.position.maxScrollExtent, - duration: const Duration(milliseconds: 200), - curve: Curves.easeOut, - ); - } - }); } @@ -960,7 +1005,9 @@ class _MessageBubble extends StatelessWidget { ], Flexible( child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + padding: gifId != null + ? const EdgeInsets.all(4) + : const EdgeInsets.symmetric(horizontal: 12, vertical: 8), constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.65, ), @@ -972,23 +1019,31 @@ class _MessageBubble extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!isOutgoing) ...[ - Text( - senderName, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: colorScheme.primary, + Padding( + padding: gifId != null + ? const EdgeInsets.only(left: 8, top: 4, bottom: 4) + : EdgeInsets.zero, + child: Text( + senderName, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: colorScheme.primary, + ), ), ), - const SizedBox(height: 4), + if (gifId == null) const SizedBox(height: 4), ], if (poi != null) _buildPoiMessage(context, poi, textColor, metaColor) else if (gifId != null) - GifMessage( - url: 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: bubbleColor, - fallbackTextColor: textColor.withValues(alpha: 0.7), + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: GifMessage( + url: 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: Colors.transparent, + fallbackTextColor: textColor.withValues(alpha: 0.7), + ), ) else Linkify( @@ -1009,48 +1064,58 @@ class _MessageBubble extends StatelessWidget { ), if (isOutgoing && message.retryCount > 0) ...[ const SizedBox(height: 4), - Text( - context.l10n.chat_retryCount(message.retryCount, 4), - style: TextStyle( - fontSize: 10, - color: metaColor, - fontWeight: FontWeight.w500, + Padding( + padding: gifId != null + ? const EdgeInsets.symmetric(horizontal: 8) + : EdgeInsets.zero, + child: Text( + context.l10n.chat_retryCount(message.retryCount, 4), + style: TextStyle( + fontSize: 10, + color: metaColor, + fontWeight: FontWeight.w500, + ), ), ), ], const SizedBox(height: 4), - Wrap( - spacing: 4, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Text( - _formatTime(message.timestamp), - style: TextStyle( - fontSize: 10, - color: metaColor, - ), - ), - if (isOutgoing) ...[ - const SizedBox(width: 4), - _buildStatusIcon(metaColor), - ], - if (message.tripTimeMs != null && - message.status == MessageStatus.delivered) ...[ - const SizedBox(width: 4), - Icon( - Icons.speed, - size: 10, - color: isOutgoing ? metaColor : Colors.green[700], - ), + Padding( + padding: gifId != null + ? const EdgeInsets.only(left: 8, right: 8, bottom: 4) + : EdgeInsets.zero, + child: Wrap( + spacing: 4, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ Text( - '${(message.tripTimeMs! / 1000).toStringAsFixed(1)}s', + _formatTime(message.timestamp), style: TextStyle( - fontSize: 9, - color: isOutgoing ? metaColor : Colors.green[700], + fontSize: 10, + color: metaColor, ), ), + if (isOutgoing) ...[ + const SizedBox(width: 4), + _buildStatusIcon(metaColor), + ], + if (message.tripTimeMs != null && + message.status == MessageStatus.delivered) ...[ + const SizedBox(width: 4), + Icon( + Icons.speed, + size: 10, + color: isOutgoing ? metaColor : Colors.green[700], + ), + Text( + '${(message.tripTimeMs! / 1000).toStringAsFixed(1)}s', + style: TextStyle( + fontSize: 9, + color: isOutgoing ? metaColor : Colors.green[700], + ), + ), + ], ], - ], + ), ), ], ), diff --git a/lib/widgets/gif_message.dart b/lib/widgets/gif_message.dart index 402565f..b98bdc6 100644 --- a/lib/widgets/gif_message.dart +++ b/lib/widgets/gif_message.dart @@ -6,16 +6,14 @@ class GifMessage extends StatefulWidget { final String url; final Color backgroundColor; final Color fallbackTextColor; - final double width; - final double height; + final double maxSize; const GifMessage({ super.key, required this.url, required this.backgroundColor, required this.fallbackTextColor, - this.width = 200, - this.height = 140, + this.maxSize = 200, }); @override @@ -122,6 +120,28 @@ class _GifMessageState extends State { @override Widget build(BuildContext context) { + // Calculate display size based on image aspect ratio + // Use 4:3 placeholder aspect ratio during loading to minimize layout shifts + double displayWidth = widget.maxSize; + double displayHeight = widget.maxSize * 0.75; + + if (_image != null) { + final imageWidth = _image!.width.toDouble(); + final imageHeight = _image!.height.toDouble(); + final aspectRatio = imageWidth / imageHeight; + + // Fit within maxSize, calculating dimensions from aspect ratio + if (aspectRatio >= 1) { + // Wider than tall: constrain by width + displayWidth = widget.maxSize; + displayHeight = displayWidth / aspectRatio; + } else { + // Taller than wide: constrain by height + displayHeight = widget.maxSize; + displayWidth = displayHeight * aspectRatio; + } + } + Widget content; if (_error != null) { @@ -151,33 +171,30 @@ class _GifMessageState extends State { } else { content = RawImage( image: _image, - fit: BoxFit.cover, - width: widget.width, - height: widget.height, + fit: BoxFit.contain, + width: displayWidth, + height: displayHeight, ); } return GestureDetector( onTap: _togglePause, - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: Container( - color: widget.backgroundColor, - width: widget.width, - height: widget.height, - child: Stack( - fit: StackFit.expand, - children: [ - content, - if (_isPaused && _image != null) - Container( - color: Colors.black.withValues(alpha: 0.2), - child: const Center( - child: Icon(Icons.pause, color: Colors.white70, size: 28), - ), + child: Container( + color: widget.backgroundColor, + width: displayWidth, + height: displayHeight, + child: Stack( + fit: StackFit.expand, + children: [ + content, + if (_isPaused && _image != null) + Container( + color: Colors.black.withValues(alpha: 0.2), + child: const Center( + child: Icon(Icons.pause, color: Colors.white70, size: 28), ), - ], - ), + ), + ], ), ), ); diff --git a/lib/widgets/jump_to_bottom_button.dart b/lib/widgets/jump_to_bottom_button.dart new file mode 100644 index 0000000..08614f3 --- /dev/null +++ b/lib/widgets/jump_to_bottom_button.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import '../helpers/chat_scroll_controller.dart'; + +class JumpToBottomButton extends StatelessWidget { + final ChatScrollController scrollController; + + const JumpToBottomButton({ + super.key, + required this.scrollController, + }); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: scrollController.showJumpToBottom, + builder: (context, show, _) { + if (!show) return const SizedBox.shrink(); + return Positioned( + right: 16, + bottom: 16, + child: FloatingActionButton.small( + onPressed: scrollController.jumpToBottom, + child: const Icon(Icons.keyboard_arrow_down), + ), + ); + }, + ); + } +} From 09e1cd2b8dba49f92c877f2b5fcf23b8b78d42e5 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 24 Jan 2026 00:17:18 -0700 Subject: [PATCH 4/5] fix: improve BLE scanning reliability and filter out own node from contacts list improve text scaling --- lib/connector/meshcore_connector.dart | 11 +++++++ lib/screens/contacts_screen.dart | 45 ++++++++++++++++++--------- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 28b1082..0d5b4b1 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -625,6 +625,17 @@ class MeshCoreConnector extends ChangeNotifier { _scanResults.clear(); _setState(MeshCoreConnectionState.scanning); + // Ensure any previous scan is fully stopped + await FlutterBluePlus.stopScan(); + await _scanSubscription?.cancel(); + + // On iOS/macOS, add a small delay to allow BLE stack to reset + // This prevents cached results from interfering with new scans + if (defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.macOS) { + await Future.delayed(const Duration(milliseconds: 300)); + } + _scanSubscription = FlutterBluePlus.scanResults.listen((results) { _scanResults.clear(); for (var result in results) { diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index e91cd94..54f819c 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -313,6 +313,14 @@ class _ContactsScreenState extends State return matchesContactQuery(contact, _searchQuery); }).toList(); + // Filter out own node from the list + if (connector.selfPublicKey != null) { + final selfPubKeyHex = pubKeyToHex(connector.selfPublicKey!); + filtered = filtered.where((contact) { + return contact.publicKeyHex != selfPubKeyHex; + }).toList(); + } + if (_typeFilter != ContactTypeFilter.all) { filtered = filtered.where(_matchesTypeFilter).toList(); } @@ -863,21 +871,30 @@ class _ContactTile extends StatelessWidget { subtitle: Text( '${contact.typeLabel} • ${contact.pathLabel} $shotPublicKey', ), - trailing: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - if (unreadCount > 0) ...[ - UnreadBadge(count: unreadCount), - const SizedBox(height: 4), - ], - Text( - _formatLastSeen(context, lastSeen), - style: TextStyle(fontSize: 12, color: Colors.grey[600]), + // Clamp text scaling in trailing section to prevent overflow while + // maintaining accessibility. Primary content (title/subtitle) scales normally. + trailing: MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaler: TextScaler.linear( + MediaQuery.textScalerOf(context).scale(1.0).clamp(1.0, 1.3), ), - if (contact.hasLocation) - Icon(Icons.location_on, size: 14, color: Colors.grey[400]), - ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (unreadCount > 0) ...[ + UnreadBadge(count: unreadCount), + const SizedBox(height: 4), + ], + Text( + _formatLastSeen(context, lastSeen), + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + if (contact.hasLocation) + Icon(Icons.location_on, size: 14, color: Colors.grey[400]), + ], + ), ), onTap: onTap, onLongPress: onLongPress, From f0d34f7503eb7775e778a3e63b671946047d454a Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 24 Jan 2026 00:27:45 -0700 Subject: [PATCH 5/5] Update Russian localization for improved pluralization and add new chat link handling messages - Enhanced pluralization rules for "hops" in various contexts to better reflect Russian grammar. - Added new localization strings for chat link handling, including error messages and confirmation prompts. - Ensured consistency in the use of plural forms across the application. --- lib/l10n/app_localizations.dart | 5 + lib/l10n/app_localizations_ru.dart | 1962 ++++++++++++++-------------- lib/l10n/app_ru.arb | 33 +- 3 files changed, 1039 insertions(+), 961 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index d52830c..5e2d8fe 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -14,6 +14,7 @@ import 'app_localizations_it.dart'; import 'app_localizations_nl.dart'; import 'app_localizations_pl.dart'; import 'app_localizations_pt.dart'; +import 'app_localizations_ru.dart'; import 'app_localizations_sk.dart'; import 'app_localizations_sl.dart'; import 'app_localizations_sv.dart'; @@ -114,6 +115,7 @@ abstract class AppLocalizations { Locale('nl'), Locale('pl'), Locale('pt'), + Locale('ru'), Locale('sk'), Locale('sl'), Locale('sv'), @@ -4705,6 +4707,7 @@ class _AppLocalizationsDelegate 'nl', 'pl', 'pt', + 'ru', 'sk', 'sl', 'sv', @@ -4736,6 +4739,8 @@ AppLocalizations lookupAppLocalizations(Locale locale) { return AppLocalizationsPl(); case 'pt': return AppLocalizationsPt(); + case 'ru': + return AppLocalizationsRu(); case 'sk': return AppLocalizationsSk(); case 'sl': diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index b1e6c1f..ae784e4 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1,2626 +1,2682 @@ // ignore: unused_import import 'package:intl/intl.dart' as intl; import 'app_localizations.dart'; + // ignore_for_file: type=lint + /// The translations for Russian (`ru`). -class AppLocalizationsEn extends AppLocalizations { +class AppLocalizationsRu extends AppLocalizations { AppLocalizationsRu([String locale = 'ru']) : super(locale); - + @override String get appTitle => 'MeshCore Open'; - + @override String get nav_contacts => 'Контакты'; - + @override String get nav_channels => 'Каналы'; - + @override String get nav_map => 'Карта'; - + @override String get common_cancel => 'Отмена'; - + @override String get common_ok => 'OK'; - + @override String get common_connect => 'Коннект'; - + @override String get common_unknownDevice => 'Неизвестное устройство'; - + @override String get common_save => 'Сохранить'; - + @override String get common_delete => 'Удалить'; - + @override String get common_close => 'Закрыть'; - + @override String get common_edit => 'Изменить'; - + @override String get common_add => 'Добавить'; - + @override String get common_settings => 'Настройки'; - + @override String get common_disconnect => 'Отключить'; - + @override String get common_connected => 'Подключено'; - + @override String get common_disconnected => 'Отключено'; - + @override String get common_create => 'Создать'; - + @override String get common_continue => 'Продолжить'; - + @override String get common_share => 'Поделиться'; - + @override String get common_copy => 'Копировать'; - + @override String get common_retry => 'Повторить'; - + @override String get common_hide => 'Скрыть'; - + @override String get common_remove => 'Убрать'; - + @override String get common_enable => 'Включить'; - + @override String get common_disable => 'Выключить'; - + @override String get common_reboot => 'Перезагрузить'; - + @override String get common_loading => 'Загрузка...'; - + @override String get common_notAvailable => '—'; - + @override String common_voltageValue(String volts) { return '$volts В'; -} - + } + @override String common_percentValue(int percent) { return '$percent%'; -} - + } + @override String get scanner_title => 'MeshCore Open'; - + @override String get scanner_scanning => 'Поиск устройств...'; - + @override String get scanner_connecting => 'Подключение...'; - + @override String get scanner_disconnecting => 'Отключение...'; - + @override String get scanner_notConnected => 'Не подключено'; - + @override String scanner_connectedTo(String deviceName) { return 'Подключено к $deviceName'; -} - + } + @override String get scanner_searchingDevices => 'Поиск устройств MeshCore...'; - + @override String get scanner_tapToScan => 'Нажмите для поиска MeshCore устройств'; - + @override String scanner_connectionFailed(String error) { return 'Подключение не удалось: $error'; -} - + } + @override String get scanner_stop => 'Стоп'; - + @override String get scanner_scan => 'Сканирование'; - + @override String get device_quickSwitch => 'Быстрое переключение'; - + @override String get device_meshcore => 'MeshCore'; - + @override String get settings_title => 'Настройки'; - + @override String get settings_deviceInfo => 'Информация об устройстве'; - + @override String get settings_appSettings => 'Настройки приложения'; - + @override String get settings_appSettingsSubtitle => 'Уведомления, сообщения и настройки карты'; - + @override String get settings_nodeSettings => 'Настройки ноды'; - + @override String get settings_nodeName => 'Имя ноды'; - + @override String get settings_nodeNameNotSet => 'Не установлено'; - + @override String get settings_nodeNameHint => 'Введите имя ноды'; - + @override String get settings_nodeNameUpdated => 'Имя обновлено'; - + @override String get settings_radioSettings => 'Настройки радио'; - + @override String get settings_radioSettingsSubtitle => 'Частота, мощность и коэффициент распространения'; - + @override String get settings_radioSettingsUpdated => 'Настройки радио обновлены'; - + @override String get settings_location => 'Позиция'; - + @override String get settings_locationSubtitle => 'Координаты GPS'; - + @override String get settings_locationUpdated => 'Позиция и настройки GPS обновлены'; - + @override - String get settings_locationBothRequired => - 'Введите широту и долготу.'; - + String get settings_locationBothRequired => 'Введите широту и долготу.'; + @override String get settings_locationInvalid => 'Неверная широта или долгота.'; - + @override String get settings_locationGPSEnable => 'Включить GPS'; - + @override String get settings_locationGPSEnableSubtitle => 'Включение GPS для автоматического обновления позиции.'; - + @override - String get settings_locationIntervalSec => 'Интервал для позиционирования GPS (секунды)'; - + String get settings_locationIntervalSec => + 'Интервал для позиционирования GPS (секунды)'; + @override String get settings_locationIntervalInvalid => 'Интервал должен составлять не менее 60 секунд и не более 86400 секунд.'; - + @override String get settings_latitude => 'Широта'; - + @override String get settings_longitude => 'Долгота'; - + @override String get settings_privacyMode => 'Режим конфиденциальности'; - + @override String get settings_privacyModeSubtitle => 'Скрыть имя/позицию в анонсировании'; - + @override String get settings_privacyModeToggle => 'Включите режим конфиденциальности, чтобы скрыть свое имя и местоположение в анонсировании.'; - + @override String get settings_privacyModeEnabled => 'Режим конфиденциальности включен'; - + @override - String get settings_privacyModeDisabled => 'Режим конфиденциальности выключен'; - + String get settings_privacyModeDisabled => + 'Режим конфиденциальности выключен'; + @override String get settings_actions => 'Действия'; - + @override String get settings_sendAdvertisement => 'Отправить анонсирование'; - + @override - String get settings_sendAdvertisementSubtitle => 'Отправить анонсирование о присутствии сейчас'; - + String get settings_sendAdvertisementSubtitle => + 'Отправить анонсирование о присутствии сейчас'; + @override String get settings_advertisementSent => 'Анонсирование отправлено'; - + @override String get settings_syncTime => 'Синхронизация времени'; - + @override String get settings_syncTimeSubtitle => 'Синхронизировать время с телефоном'; - + @override String get settings_timeSynchronized => 'Время синхронизировано'; - + @override String get settings_refreshContacts => 'Обновить контакты'; - + @override String get settings_refreshContactsSubtitle => 'Перезагрузить список контактов с устройства'; - + @override String get settings_rebootDevice => 'Перезагрузить устройство'; - + @override - String get settings_rebootDeviceSubtitle => 'Перезапустить устройство MeshCore'; - + String get settings_rebootDeviceSubtitle => + 'Перезапустить устройство MeshCore'; + @override String get settings_rebootDeviceConfirm => 'Вы уверены, что хотите перезагрузить устройство? Вы будете отключены.'; - + @override String get settings_debug => 'Отладка'; - + @override String get settings_bleDebugLog => 'Журнал отладки BLE'; - + @override String get settings_bleDebugLogSubtitle => 'Команды BLE, ответы и сырые данные'; - + @override String get settings_appDebugLog => 'Журнал отладки приложения'; - + @override String get settings_appDebugLogSubtitle => 'Сообщения отладки приложения'; - + @override String get settings_about => 'О программе'; - + @override String settings_aboutVersion(String version) { return 'MeshCore Open v$version'; -} - + } + @override String get settings_aboutLegalese => '2026 MeshCore Open Source Project'; - + @override String get settings_aboutDescription => 'Открытое клиентское приложение на Flutter для устройств MeshCore с LoRa-сетями.'; - + @override String get settings_infoName => 'Имя'; - + @override String get settings_infoId => 'ID'; - + @override String get settings_infoStatus => 'Статус'; - + @override String get settings_infoBattery => 'Батарея'; - + @override String get settings_infoPublicKey => 'Публичный ключ'; - + @override String get settings_infoContactsCount => 'Количество контактов'; - + @override String get settings_infoChannelCount => 'Количество каналов'; - + @override String get settings_presets => 'Пресеты'; - + @override String get settings_preset915Mhz => '915 МГц'; - + @override String get settings_preset868Mhz => '868 МГц'; - + @override String get settings_preset433Mhz => '433 МГц'; - + @override String get settings_frequency => 'Частота (МГц)'; - + @override String get settings_frequencyHelper => '300.0 – 2500.0'; - + @override String get settings_frequencyInvalid => 'Недопустимая частота (300–2500 МГц)'; - + @override String get settings_bandwidth => 'Полоса пропускания'; - + @override String get settings_spreadingFactor => 'Коэффициент расширения'; - + @override String get settings_codingRate => 'Коэффициент кодирования'; - + @override String get settings_txPower => 'Мощность передачи (дБм)'; - + @override String get settings_txPowerHelper => '0 – 22'; - + @override - String get settings_txPowerInvalid => 'Недопустимая мощность передачи (0–22 дБм)'; - + String get settings_txPowerInvalid => + 'Недопустимая мощность передачи (0–22 дБм)'; + @override String get settings_longRange => 'Дальний радиус'; - + @override String get settings_fastSpeed => 'Высокая скорость'; - + @override String settings_error(String message) { return 'Ошибка: $message'; -} - + } + @override String get appSettings_title => 'Настройки приложения'; - + @override String get appSettings_appearance => 'Внешний вид'; - + @override String get appSettings_theme => 'Тема'; - + @override String get appSettings_themeSystem => 'Как в системе'; - + @override String get appSettings_themeLight => 'Светлая'; - + @override String get appSettings_themeDark => 'Тёмная'; - + @override String get appSettings_language => 'Язык'; - + @override String get appSettings_languageSystem => 'Как в системе'; - + @override String get appSettings_languageEn => 'Английский'; - + @override String get appSettings_languageFr => 'Французский'; - + @override String get appSettings_languageEs => 'Испанский'; - + @override String get appSettings_languageDe => 'Немецкий'; - + @override String get appSettings_languagePl => 'Польский'; - + @override String get appSettings_languageSl => 'Словенский'; - + @override String get appSettings_languagePt => 'Португальский'; - + @override String get appSettings_languageIt => 'Итальянский'; - + @override String get appSettings_languageZh => 'Китайский'; - + @override String get appSettings_languageSv => 'Шведский'; - + @override String get appSettings_languageNl => 'Нидерландский'; - + @override String get appSettings_languageSk => 'Словацкий'; - + @override String get appSettings_languageBg => 'Болгарский'; - - @override - String get appSettings_languageRu => 'Русский'; - + @override String get appSettings_notifications => 'Уведомления'; - + @override String get appSettings_enableNotifications => 'Включить уведомления'; - + @override String get appSettings_enableNotificationsSubtitle => 'Получать уведомления о сообщениях и оповещениях'; - + @override String get appSettings_notificationPermissionDenied => - 'Разрешение на уведомления отклонено'; - + 'Разрешение на уведомления отклонено'; + @override String get appSettings_notificationsEnabled => 'Уведомления включены'; - + @override String get appSettings_notificationsDisabled => 'Уведомления отключены'; - + @override String get appSettings_messageNotifications => 'Уведомления о сообщениях'; - + @override String get appSettings_messageNotificationsSubtitle => 'Показывать уведомление при получении новых сообщений'; - + @override String get appSettings_channelMessageNotifications => 'Уведомления о сообщениях в каналах'; - + @override String get appSettings_channelMessageNotificationsSubtitle => 'Показывать уведомление при получении сообщений в каналах'; - + @override String get appSettings_advertisementNotifications => 'Уведомления об анонсированиях'; - + @override String get appSettings_advertisementNotificationsSubtitle => 'Показывать уведомление при обнаружении новых нод'; - + @override String get appSettings_messaging => 'Обмен сообщениями'; - + @override - String get appSettings_clearPathOnMaxRetry => 'Сбросить маршрут после максимального числа попыток'; - + String get appSettings_clearPathOnMaxRetry => + 'Сбросить маршрут после максимального числа попыток'; + @override String get appSettings_clearPathOnMaxRetrySubtitle => - 'Сбросить маршрут контакта после 5 неудачных попыток отправки'; - + 'Сбросить маршрут контакта после 5 неудачных попыток отправки'; + @override String get appSettings_pathsWillBeCleared => 'Маршруты будут сброшены после 5 неудачных попыток'; - + @override String get appSettings_pathsWillNotBeCleared => 'Маршруты не будут автоматически сбрасываться'; - + @override - String get appSettings_autoRouteRotation => 'Автоматическое переключение маршрутов'; - + String get appSettings_autoRouteRotation => + 'Автоматическое переключение маршрутов'; + @override String get appSettings_autoRouteRotationSubtitle => 'Циклически переключаться между лучшими маршрутами и режимом рассылки'; - + @override String get appSettings_autoRouteRotationEnabled => 'Автоматическое переключение маршрутов включено'; - + @override String get appSettings_autoRouteRotationDisabled => 'Автоматическое переключение маршрутов отключено'; - + @override String get appSettings_battery => 'Батарея'; - + @override String get appSettings_batteryChemistry => 'Химия батареи'; - + @override String appSettings_batteryChemistryPerDevice(String deviceName) { return 'Установить для устройства ($deviceName)'; -} - + } + @override String get appSettings_batteryChemistryConnectFirst => 'Подключитесь к устройству, чтобы выбрать'; - + @override String get appSettings_batteryNmc => '18650 NMC (3.0–4.2 В)'; - + @override String get appSettings_batteryLifepo4 => 'LiFePO4 (2.6–3.65 В)'; - + @override String get appSettings_batteryLipo => 'LiPo (3.0–4.2 В)'; - + @override String get appSettings_mapDisplay => 'Отображение карты'; - + @override String get appSettings_showRepeaters => 'Показывать репитеры'; - + @override String get appSettings_showRepeatersSubtitle => 'Отображать репитеры на карте'; - + @override String get appSettings_showChatNodes => 'Показывать чат-ноды'; - + @override String get appSettings_showChatNodesSubtitle => 'Отображать чат-ноды на карте'; - + @override String get appSettings_showOtherNodes => 'Показывать другие ноды'; - + @override String get appSettings_showOtherNodesSubtitle => 'Отображать другие типы нод на карте'; - + @override String get appSettings_timeFilter => 'Фильтр по времени'; - + @override String get appSettings_timeFilterShowAll => 'Показывать все ноды'; - + @override String appSettings_timeFilterShowLast(int hours) { return 'Показывать ноды за последние $hours ч'; -} - + } + @override String get appSettings_mapTimeFilter => 'Временной фильтр карты'; - + @override String get appSettings_showNodesDiscoveredWithin => 'Показывать ноды, обнаруженные за:'; - + @override String get appSettings_allTime => 'Всё время'; - + @override String get appSettings_lastHour => 'Последний час'; - + @override String get appSettings_last6Hours => 'Последние 6 часов'; - + @override String get appSettings_last24Hours => 'Последние 24 часа'; - + @override String get appSettings_lastWeek => 'Последнюю неделю'; - + @override String get appSettings_offlineMapCache => 'Кэш офлайн-карты'; - + @override String get appSettings_noAreaSelected => 'Область не выбрана'; - + @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { return 'Область выбрана (масштаб $minZoom–$maxZoom)'; -} - + } + @override String get appSettings_debugCard => 'Отладка'; - + @override String get appSettings_appDebugLogging => 'Журнал отладки приложения'; - + @override String get appSettings_appDebugLoggingSubtitle => 'Записывать отладочные сообщения приложения для диагностики'; - + @override - String get appSettings_appDebugLoggingEnabled => 'Журнал отладки приложения включён'; - + String get appSettings_appDebugLoggingEnabled => + 'Журнал отладки приложения включён'; + @override String get appSettings_appDebugLoggingDisabled => 'Журнал отладки приложения отключён'; - + @override String get contacts_title => 'Контакты'; - + @override String get contacts_noContacts => 'Контактов пока нет'; - + @override String get contacts_contactsWillAppear => 'Контакты появятся, когда устройства начнут рассылать оповещения'; - + @override String get contacts_searchContacts => 'Поиск контактов...'; - + @override String get contacts_noUnreadContacts => 'Нет непрочитанных контактов'; - + @override String get contacts_noContactsFound => 'Контакты или группы не найдены'; - + @override String get contacts_deleteContact => 'Удалить контакт'; - + @override String contacts_removeConfirm(String contactName) { return 'Удалить $contactName из контактов?'; -} - + } + @override String get contacts_manageRepeater => 'Управление репитером'; - + @override String get contacts_manageRoom => 'Управление сервером комнат'; - + @override String get contacts_roomLogin => 'Вход на сервер комнат'; - + @override String get contacts_openChat => 'Открыть чат'; - + @override String get contacts_editGroup => 'Изменить группу'; - + @override String get contacts_deleteGroup => 'Удалить группу'; - + @override String contacts_deleteGroupConfirm(String groupName) { return 'Удалить \"$groupName\"?'; -} - + } + @override String get contacts_newGroup => 'Новая группа'; - + @override String get contacts_groupName => 'Имя группы'; - + @override String get contacts_groupNameRequired => 'Имя группы обязательно'; - + @override String contacts_groupAlreadyExists(String name) { return 'Группа \"$name\" уже существует'; -} - + } + @override String get contacts_filterContacts => 'Фильтр контактов...'; - + @override - String get contacts_noContactsMatchFilter => 'Нет контактов, соответствующих фильтру'; - + String get contacts_noContactsMatchFilter => + 'Нет контактов, соответствующих фильтру'; + @override String get contacts_noMembers => 'Нет участников'; - + @override String get contacts_lastSeenNow => 'Видели только что'; - + @override String contacts_lastSeenMinsAgo(int minutes) { return 'Видели $minutes мин назад'; -} - + } + @override String get contacts_lastSeenHourAgo => 'Видели 1 час назад'; - + @override String contacts_lastSeenHoursAgo(int hours) { return 'Видели $hours ч назад'; -} - + } + @override String get contacts_lastSeenDayAgo => 'Видели 1 день назад'; - + @override String contacts_lastSeenDaysAgo(int days) { return 'Видели $days дн. назад'; -} - + } + @override String get channels_title => 'Каналы'; - + @override String get channels_noChannelsConfigured => 'Каналы не настроены'; - + @override String get channels_addPublicChannel => 'Добавить публичный канал'; - + @override String get channels_searchChannels => 'Поиск каналов...'; - + @override String get channels_noChannelsFound => 'Каналы не найдены'; - + @override String channels_channelIndex(int index) { return 'Канал $index'; -} - + } + @override String get channels_hashtagChannel => 'Хэштег-канал'; - + @override String get channels_public => 'Публичный'; - + @override String get channels_private => 'Приватный'; - + @override String get channels_publicChannel => 'Публичный канал'; - + @override String get channels_privateChannel => 'Приватный канал'; - + @override String get channels_editChannel => 'Изменить канал'; - + @override String get channels_deleteChannel => 'Удалить канал'; - + @override String channels_deleteChannelConfirm(String name) { return 'Удалить \"$name\"? Это действие нельзя отменить.'; -} - + } + @override String channels_channelDeleted(String name) { return 'Канал \"$name\" удалён'; -} - + } + @override String get channels_addChannel => 'Добавить канал'; - + @override String get channels_channelIndexLabel => 'Индекс канала'; - + @override String get channels_channelName => 'Имя канала'; - + @override String get channels_usePublicChannel => 'Использовать публичный канал'; - + @override String get channels_standardPublicPsk => 'Стандартный публичный PSK'; - + @override String get channels_pskHex => 'PSK (Hex)'; - + @override String get channels_generateRandomPsk => 'Сгенерировать случайный PSK'; - + @override String get channels_enterChannelName => 'Введите имя канала'; - + @override - String get channels_pskMustBe32Hex => 'PSK должен содержать 32 шестнадцатеричных символа'; - + String get channels_pskMustBe32Hex => + 'PSK должен содержать 32 шестнадцатеричных символа'; + @override String channels_channelAdded(String name) { return 'Канал \"$name\" добавлен'; -} - + } + @override String channels_editChannelTitle(int index) { return 'Изменить канал $index'; -} - + } + @override String get channels_smazCompression => 'Сжатие SMAZ'; - + @override String channels_channelUpdated(String name) { return 'Канал \"$name\" обновлён'; -} - + } + @override String get channels_publicChannelAdded => 'Публичный канал добавлен'; - + @override String get channels_sortBy => 'Сортировка'; - + @override String get channels_sortManual => 'Вручную'; - + @override String get channels_sortAZ => 'По алфавиту'; - + @override String get channels_sortLatestMessages => 'По последним сообщениям'; - + @override String get channels_sortUnread => 'По непрочитанным'; - + @override String get channels_createPrivateChannel => 'Создать приватный канал'; - + @override String get channels_createPrivateChannelDesc => 'Защищён секретным ключом.'; - + @override - String get channels_joinPrivateChannel => 'Присоединиться к приватному каналу'; - + String get channels_joinPrivateChannel => + 'Присоединиться к приватному каналу'; + @override - String get channels_joinPrivateChannelDesc => 'Введите секретный ключ вручную.'; - + String get channels_joinPrivateChannelDesc => + 'Введите секретный ключ вручную.'; + @override String get channels_joinPublicChannel => 'Присоединиться к публичному каналу'; - + @override - String get channels_joinPublicChannelDesc => 'К этому каналу может присоединиться любой.'; - + String get channels_joinPublicChannelDesc => + 'К этому каналу может присоединиться любой.'; + @override String get channels_joinHashtagChannel => 'Присоединиться к хэштег-каналу'; - + @override String get channels_joinHashtagChannelDesc => 'К хэштег-каналам может присоединиться любой.'; - + @override String get channels_scanQrCode => 'Сканировать QR-код'; - + @override String get channels_scanQrCodeComingSoon => 'Скоро будет'; - + @override String get channels_enterHashtag => 'Введите хэштег'; - + @override String get channels_hashtagHint => 'например, #команда'; - + @override String get chat_noMessages => 'Сообщений пока нет'; - + @override String get chat_sendMessageToStart => 'Отправьте сообщение, чтобы начать'; - + @override String get chat_originalMessageNotFound => 'Исходное сообщение не найдено'; - + @override String chat_replyingTo(String name) { return 'Ответ для $name'; -} - + } + @override String chat_replyTo(String name) { return 'Ответить $name'; -} - + } + @override String get chat_location => 'Местоположение'; - + @override String chat_sendMessageTo(String contactName) { return 'Отправить сообщение $contactName'; -} - + } + @override String get chat_typeMessage => 'Напишите сообщение...'; - + @override String chat_messageTooLong(int maxBytes) { return 'Сообщение слишком длинное (макс. $maxBytes байт).'; -} - + } + @override String get chat_messageCopied => 'Сообщение скопировано'; - + @override String get chat_messageDeleted => 'Сообщение удалено'; - + @override String get chat_retryingMessage => 'Повтор отправки сообщения'; - + @override String chat_retryCount(int current, int max) { return 'Попытка $current/$max'; -} - + } + @override String get chat_sendGif => 'Отправить GIF'; - + @override String get chat_reply => 'Ответить'; - + @override String get chat_addReaction => 'Добавить реакцию'; - + @override String get chat_me => 'Я'; - + @override String get emojiCategorySmileys => 'Смайлы'; - + @override String get emojiCategoryGestures => 'Жесты'; - + @override String get emojiCategoryHearts => 'Сердечки'; - + @override String get emojiCategoryObjects => 'Предметы'; - + @override String get gifPicker_title => 'Выберите GIF'; - + @override String get gifPicker_searchHint => 'Поиск GIF...'; - + @override String get gifPicker_poweredBy => 'Работает на GIPHY'; - + @override String get gifPicker_noGifsFound => 'GIF не найдены'; - + @override String get gifPicker_failedLoad => 'Не удалось загрузить GIF'; - + @override String get gifPicker_failedSearch => 'Не удалось выполнить поиск GIF'; - + @override String get gifPicker_noInternet => 'Нет подключения к интернету'; - + @override String get debugLog_appTitle => 'Журнал отладки приложения'; - + @override String get debugLog_bleTitle => 'Журнал отладки BLE'; - + @override String get debugLog_copyLog => 'Копировать журнал'; - + @override String get debugLog_clearLog => 'Очистить журнал'; - + @override String get debugLog_copied => 'Журнал отладки скопирован'; - + @override String get debugLog_bleCopied => 'Журнал BLE скопирован'; - + @override String get debugLog_noEntries => 'Журнал отладки пока пуст'; - + @override String get debugLog_enableInSettings => 'Включите запись журнала отладки в настройках'; - + @override String get debugLog_frames => 'Фреймы'; - + @override String get debugLog_rawLogRx => 'Сырой журнал приёма'; - + @override String get debugLog_noBleActivity => 'Активность BLE пока отсутствует'; - + @override String debugFrame_length(int count) { return 'Длина фрейма: $count байт'; -} - + } + @override String debugFrame_command(String value) { return 'Команда: 0x$value'; -} - + } + @override String get debugFrame_textMessageHeader => 'Фрейм текстового сообщения:'; - + @override String debugFrame_destinationPubKey(String pubKey) { return '- Публичный ключ получателя: $pubKey'; -} - + } + @override String debugFrame_timestamp(int timestamp) { return '- Временная метка: $timestamp'; -} - + } + @override String debugFrame_flags(String value) { return '- Флаги: 0x$value'; -} - + } + @override - String debugFrame_textType(int type, String label) { + String debugFrame_textType(int type, String label) { return '- Тип текста: $type ($label)'; -} - + } + @override String get debugFrame_textTypeCli => 'CLI'; - + @override String get debugFrame_textTypePlain => 'Обычный'; - + @override String debugFrame_text(String text) { return '- Текст: \"$text\"'; -} - + } + @override String get debugFrame_hexDump => 'Шестнадцатеричный дамп:'; - + @override String get chat_pathManagement => 'Управление маршрутами'; - + @override String get chat_routingMode => 'Режим маршрутизации'; - + @override String get chat_autoUseSavedPath => 'Авто (использовать сохранённый маршрут)'; - + @override String get chat_forceFloodMode => 'Принудительный режим рассылки'; - + @override - String get chat_recentAckPaths => 'Недавние подтверждённые маршруты (нажмите, чтобы использовать):'; - + String get chat_recentAckPaths => + 'Недавние подтверждённые маршруты (нажмите, чтобы использовать):'; + @override String get chat_pathHistoryFull => 'История маршрутов заполнена. Удалите записи, чтобы добавить новые.'; - + @override String get chat_hopSingular => 'хоп'; - + @override String get chat_hopPlural => 'хопов'; - + @override String chat_hopsCount(int count) { - String _temp0 = intl.Intl.pluralLogic( -count, -locale: localeName, -other: 'хопов', -one: 'хоп', -); + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'хопов', + many: 'хопов', + few: 'хопа', + one: 'хоп', + ); return '$count $_temp0'; -} - + } + @override String get chat_successes => 'успешно'; - + @override String get chat_removePath => 'Удалить маршрут'; - + @override String get chat_noPathHistoryYet => - 'История маршрутов пока пуста. -Отправьте сообщение, чтобы обнаружить маршруты.'; - + 'История маршрутов пока пуста.\nОтправьте сообщение, чтобы обнаружить маршруты.'; + @override String get chat_pathActions => 'Действия с маршрутом:'; - + @override String get chat_setCustomPath => 'Указать маршрут вручную'; - + @override String get chat_setCustomPathSubtitle => 'Вручную задать маршрут передачи'; - + @override String get chat_clearPath => 'Очистить маршрут'; - + @override - String get chat_clearPathSubtitle => 'Принудительно обновить маршрут при следующей отправке'; - + String get chat_clearPathSubtitle => + 'Принудительно обновить маршрут при следующей отправке'; + @override String get chat_pathCleared => 'Маршрут очищен. Следующее сообщение обновит маршрут.'; - + @override - String get chat_floodModeSubtitle => 'Используйте переключатель маршрутизации в панели приложения'; - + String get chat_floodModeSubtitle => + 'Используйте переключатель маршрутизации в панели приложения'; + @override String get chat_floodModeEnabled => 'Режим рассылки включён. Отключите через значок маршрутизации в панели приложения.'; - + @override String get chat_fullPath => 'Полный маршрут'; - + @override String get chat_pathDetailsNotAvailable => 'Детали маршрута ещё недоступны. Попробуйте отправить сообщение для обновления.'; - + @override - String chat_pathSetHops(int hopCount, String status) { - String _temp0 = intl.Intl.pluralLogic( -hopCount, -locale: localeName, -other: 'хопов', -one: 'хоп', -); + String chat_pathSetHops(int hopCount, String status) { + String _temp0 = intl.Intl.pluralLogic( + hopCount, + locale: localeName, + other: 'хопов', + many: 'хопов', + few: 'хопа', + one: 'хоп', + ); return 'Маршрут установлен: $hopCount $_temp0 — $status'; -} - + } + @override - String get chat_pathSavedLocally => 'Сохранено локально. Подключитесь для синхронизации.'; - + String get chat_pathSavedLocally => + 'Сохранено локально. Подключитесь для синхронизации.'; + @override String get chat_pathDeviceConfirmed => 'Подтверждено устройством.'; - + @override String get chat_pathDeviceNotConfirmed => 'Ещё не подтверждено устройством.'; - + @override String get chat_type => 'Тип'; - + @override String get chat_path => 'Маршрут'; - + @override String get chat_publicKey => 'Публичный ключ'; - + @override String get chat_compressOutgoingMessages => 'Сжимать исходящие сообщения'; - + @override String get chat_floodForced => 'Рассылка (принудительно)'; - + @override String get chat_directForced => 'Прямой (принудительно)'; - + @override String chat_hopsForced(int count) { return '$count хоп(ов) (принудительно)'; -} - + } + @override String get chat_floodAuto => 'Рассылка (авто)'; - + @override String get chat_direct => 'Прямой'; - + @override String get chat_poiShared => 'Точка интереса отправлена'; - + @override String chat_unread(int count) { return 'Непрочитанных: $count'; -} - + } + + @override + String get chat_openLink => 'Открыть ссылку?'; + + @override + String get chat_openLinkConfirmation => + 'Хотите открыть эту ссылку в вашем браузере?'; + + @override + String get chat_open => 'Открыть'; + + @override + String chat_couldNotOpenLink(String url) { + return 'Не удалось открыть ссылку: $url'; + } + + @override + String get chat_invalidLink => 'Неправильный формат ссылки'; + @override String get map_title => 'Карта нод'; - + @override String get map_noNodesWithLocation => 'Нет нод с данными о местоположении'; - + @override String get map_nodesNeedGps => 'Ноды должны передавать свои GPS-координаты, чтобы отображаться на карте'; - + @override String map_nodesCount(int count) { return 'Нод: $count'; -} - + } + @override String map_pinsCount(int count) { return 'Меток: $count'; -} - + } + @override String get map_chat => 'Чат'; - + @override String get map_repeater => 'Репитер'; - + @override String get map_room => 'Комната'; - + @override String get map_sensor => 'Сенсор'; - + @override String get map_pinDm => 'Метка (ЛС)'; - + @override String get map_pinPrivate => 'Метка (Приватная)'; - + @override String get map_pinPublic => 'Метка (Публичная)'; - + @override String get map_lastSeen => 'Последнее появление'; - + @override String get map_disconnectConfirm => 'Вы уверены, что хотите отключиться от этого устройства?'; - + @override String get map_from => 'От'; - + @override String get map_source => 'Источник'; - + @override String get map_flags => 'Флаги'; - + @override String get map_shareMarkerHere => 'Поделиться меткой здесь'; - + @override String get map_pinLabel => 'Метка'; - + @override String get map_label => 'Подпись'; - + @override String get map_pointOfInterest => 'Точка интереса'; - + @override String get map_sendToContact => 'Отправить контакту'; - + @override String get map_sendToChannel => 'Отправить в канал'; - + @override String get map_noChannelsAvailable => 'Нет доступных каналов'; - + @override String get map_publicLocationShare => 'Публичная передача местоположения'; - + @override String map_publicLocationShareConfirm(String channelLabel) { return 'Вы собираетесь поделиться местоположением в $channelLabel. Этот канал публичный, и любой, у кого есть PSK, сможет его увидеть.'; -} - + } + @override String get map_connectToShareMarkers => 'Подключитесь к устройству, чтобы делиться метками'; - + @override String get map_filterNodes => 'Фильтр нод'; - + @override String get map_nodeTypes => 'Типы нод'; - + @override String get map_chatNodes => 'Чат-ноды'; - + @override String get map_repeaters => 'Репитеры'; - + @override String get map_otherNodes => 'Другие ноды'; - + @override String get map_keyPrefix => 'Префикс ключа'; - + @override String get map_filterByKeyPrefix => 'Фильтр по префиксу ключа'; - + @override String get map_publicKeyPrefix => 'Префикс публичного ключа'; - + @override String get map_markers => 'Метки'; - + @override String get map_showSharedMarkers => 'Показывать общие метки'; - + @override String get map_lastSeenTime => 'Время последнего появления'; - + @override String get map_sharedPin => 'Общая метка'; - + @override String get map_joinRoom => 'Присоединиться к комнате'; - + @override String get map_manageRepeater => 'Управление репитером'; - + @override String get mapCache_title => 'Кэш офлайн-карты'; - + @override - String get mapCache_selectAreaFirst => 'Сначала выберите область для кэширования'; - + String get mapCache_selectAreaFirst => + 'Сначала выберите область для кэширования'; + @override - String get mapCache_noTilesToDownload => 'Нет плиток для загрузки в этой области'; - + String get mapCache_noTilesToDownload => + 'Нет плиток для загрузки в этой области'; + @override String get mapCache_downloadTilesTitle => 'Загрузить плитки'; - + @override String mapCache_downloadTilesPrompt(int count) { return 'Загрузить $count плиток для офлайн-использования?'; -} - + } + @override String get mapCache_downloadAction => 'Загрузить'; - + @override String mapCache_cachedTiles(int count) { return 'Закэшировано $count плиток'; -} - + } + @override String mapCache_cachedTilesWithFailed(int downloaded, int failed) { return 'Закэшировано $downloaded плиток ($failed не загружено)'; -} - + } + @override String get mapCache_clearOfflineCacheTitle => 'Очистить офлайн-кэш'; - + @override - String get mapCache_clearOfflineCachePrompt => 'Удалить все закэшированные плитки карты?'; - + String get mapCache_clearOfflineCachePrompt => + 'Удалить все закэшированные плитки карты?'; + @override String get mapCache_offlineCacheCleared => 'Офлайн-кэш очищен'; - + @override String get mapCache_noAreaSelected => 'Область не выбрана'; - + @override String get mapCache_cacheArea => 'Область кэширования'; - + @override String get mapCache_useCurrentView => 'Использовать текущий вид'; - + @override String get mapCache_zoomRange => 'Диапазон масштаба'; - + @override String mapCache_estimatedTiles(int count) { return 'Оценочное количество плиток: $count'; -} - + } + @override String mapCache_downloadedTiles(int completed, int total) { return 'Загружено $completed из $total'; -} - + } + @override String get mapCache_downloadTilesButton => 'Загрузить плитки'; - + @override String get mapCache_clearCacheButton => 'Очистить кэш'; - + @override String mapCache_failedDownloads(int count) { return 'Неудачных загрузок: $count'; -} - + } + @override String mapCache_boundsLabel( - String north, - String south, - String east, - String west, -) { + String north, + String south, + String east, + String west, + ) { return 'С $north, Ю $south, В $east, З $west'; -} - + } + @override String get time_justNow => 'Только что'; - + @override String time_minutesAgo(int minutes) { - return '${minutes} мин назад'; -} - + return '$minutes мин назад'; + } + @override String time_hoursAgo(int hours) { - return '${hours} ч назад'; -} - + return '$hours ч назад'; + } + @override String time_daysAgo(int days) { - return '${days} дн. назад'; -} - + return '$days дн. назад'; + } + @override String get time_hour => 'час'; - + @override String get time_hours => 'часов'; - + @override String get time_day => 'день'; - + @override String get time_days => 'дней'; - + @override String get time_week => 'неделя'; - + @override String get time_weeks => 'недель'; - + @override String get time_month => 'месяц'; - + @override String get time_months => 'месяцев'; - + @override String get time_minutes => 'минут'; - + @override String get time_allTime => 'Всё время'; - + @override String get dialog_disconnect => 'Отключиться'; - + @override String get dialog_disconnectConfirm => 'Вы уверены, что хотите отключиться от этого устройства?'; - + @override String get login_repeaterLogin => 'Вход в репитер'; - + @override String get login_roomLogin => 'Вход на сервер комнат'; - + @override String get login_password => 'Пароль'; - + @override String get login_enterPassword => 'Введите пароль'; - + @override String get login_savePassword => 'Сохранить пароль'; - + @override String get login_savePasswordSubtitle => 'Пароль будет надёжно сохранён на этом устройстве'; - + @override String get login_repeaterDescription => 'Введите пароль репитера для доступа к настройкам и статусу.'; - + @override String get login_roomDescription => 'Введите пароль комнаты для доступа к настройкам и статусу.'; - + @override String get login_routing => 'Маршрутизация'; - + @override String get login_routingMode => 'Режим маршрутизации'; - + @override - String get login_autoUseSavedPath => 'Авто (использовать сохранённый маршрут)'; - + String get login_autoUseSavedPath => + 'Авто (использовать сохранённый маршрут)'; + @override String get login_forceFloodMode => 'Принудительный режим рассылки'; - + @override String get login_managePaths => 'Управление маршрутами'; - + @override String get login_login => 'Войти'; - + @override String login_attempt(int current, int max) { return 'Попытка $current/$max'; -} - + } + @override String login_failed(String error) { return 'Ошибка входа: $error'; -} - + } + @override String get login_failedMessage => 'Не удалось войти. Либо пароль неверен, либо репитер недоступен.'; - + @override String get common_reload => 'Обновить'; - + @override String get common_clear => 'Очистить'; - + @override String path_currentPath(String path) { return 'Текущий маршрут: $path'; -} - + } + @override String path_usingHopsPath(int count) { - String _temp0 = intl.Intl.pluralLogic( -count, -locale: localeName, -other: 'хопов', -one: 'хоп', -); + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'хопов', + many: 'хопов', + few: 'хопа', + one: 'хоп', + ); return 'Используется маршрут из $count $_temp0'; -} - + } + @override String get path_enterCustomPath => 'Введите маршрут вручную'; - + @override String get path_currentPathLabel => 'Текущий маршрут'; - + @override String get path_hexPrefixInstructions => 'Введите 2-символьные шестнадцатеричные префиксы для каждого хопа, разделённые запятыми.'; - + @override String get path_hexPrefixExample => 'Пример: A1,F2,3C (каждый узел использует первый байт своего публичного ключа)'; - + @override String get path_labelHexPrefixes => 'Маршрут (шестнадцатеричные префиксы)'; - + @override String get path_helperMaxHops => 'Максимум 64 хопа. Каждый префикс — 2 шестнадцатеричных символа (1 байт)'; - + @override String get path_selectFromContacts => 'Или выберите из контактов:'; - + @override String get path_noRepeatersFound => 'Репитеры или серверы комнат не найдены.'; - + @override String get path_customPathsRequire => 'Пользовательские маршруты требуют промежуточных узлов, способных ретранслировать сообщения.'; - + @override String path_invalidHexPrefixes(String prefixes) { return 'Недопустимые шестнадцатеричные префиксы: $prefixes'; -} - + } + @override String get path_tooLong => 'Маршрут слишком длинный. Максимум 64 хопа.'; - + @override String get path_setPath => 'Установить маршрут'; - + @override String get repeater_management => 'Управление репитером'; - + @override String get room_management => 'Управление сервером комнат'; - + @override String get repeater_managementTools => 'Инструменты управления'; - + @override String get repeater_status => 'Статус'; - + @override String get repeater_statusSubtitle => 'Просмотр статуса, статистики и соседей репитера'; - + @override String get repeater_telemetry => 'Телеметрия'; - + @override String get repeater_telemetrySubtitle => 'Просмотр телеметрии датчиков и системной статистики'; - + @override String get repeater_cli => 'CLI'; - + @override String get repeater_cliSubtitle => 'Отправка команд репитеру'; - + @override String get repeater_neighbours => 'Соседи'; - + @override String get repeater_neighboursSubtitle => 'Просмотр соседей на нулевом хопе.'; - + @override String get repeater_settings => 'Настройки'; - + @override String get repeater_settingsSubtitle => 'Настройка параметров репитера'; - + @override String get repeater_statusTitle => 'Статус репитера'; - + @override String get repeater_routingMode => 'Режим маршрутизации'; - + @override - String get repeater_autoUseSavedPath => 'Авто (использовать сохранённый маршрут)'; - + String get repeater_autoUseSavedPath => + 'Авто (использовать сохранённый маршрут)'; + @override String get repeater_forceFloodMode => 'Принудительный режим рассылки'; - + @override String get repeater_pathManagement => 'Управление маршрутами'; - + @override String get repeater_refresh => 'Обновить'; - + @override String get repeater_statusRequestTimeout => 'Время ожидания статуса истекло.'; - + @override String repeater_errorLoadingStatus(String error) { return 'Ошибка загрузки статуса: $error'; -} - + } + @override String get repeater_systemInformation => 'Системная информация'; - + @override String get repeater_battery => 'Батарея'; - + @override String get repeater_clockAtLogin => 'Время (при входе)'; - + @override String get repeater_uptime => 'Время работы'; - + @override String get repeater_queueLength => 'Длина очереди'; - + @override String get repeater_debugFlags => 'Флаги отладки'; - + @override String get repeater_radioStatistics => 'Радиостатистика'; - + @override String get repeater_lastRssi => 'Последний RSSI'; - + @override String get repeater_lastSnr => 'Последний SNR'; - + @override String get repeater_noiseFloor => 'Уровень шума'; - + @override String get repeater_txAirtime => 'Время эфира (передача)'; - + @override String get repeater_rxAirtime => 'Время эфира (приём)'; - + @override String get repeater_packetStatistics => 'Статистика пакетов'; - + @override String get repeater_sent => 'Отправлено'; - + @override String get repeater_received => 'Получено'; - + @override String get repeater_duplicates => 'Дубликаты'; - + @override String repeater_daysHoursMinsSecs( -int days, -int hours, -int minutes, -int seconds, -) { - return '$days дн. ${hours}ч ${minutes}м ${seconds}с'; -} - + int days, + int hours, + int minutes, + int seconds, + ) { + return '$days дн. $hoursч $minutesм $secondsс'; + } + @override - String repeater_packetTxTotal(int total, String flood, String direct) { + String repeater_packetTxTotal(int total, String flood, String direct) { return 'Всего: $total, Рассылка: $flood, Прямые: $direct'; -} - + } + @override - String repeater_packetRxTotal(int total, String flood, String direct) { + String repeater_packetRxTotal(int total, String flood, String direct) { return 'Всего: $total, Рассылка: $flood, Прямые: $direct'; -} - + } + @override - String repeater_duplicatesFloodDirect(String flood, String direct) { + String repeater_duplicatesFloodDirect(String flood, String direct) { return 'Рассылка: $flood, Прямые: $direct'; -} - + } + @override String repeater_duplicatesTotal(int total) { return 'Всего: $total'; -} - + } + @override String get repeater_settingsTitle => 'Настройки репитера'; - + @override String get repeater_basicSettings => 'Основные настройки'; - + @override String get repeater_repeaterName => 'Имя репитера'; - + @override String get repeater_repeaterNameHelper => 'Отображаемое имя этого репитера'; - + @override String get repeater_adminPassword => 'Пароль администратора'; - + @override String get repeater_adminPasswordHelper => 'Пароль с полным доступом'; - + @override String get repeater_guestPassword => 'Гостевой пароль'; - + @override - String get repeater_guestPasswordHelper => 'Пароль для доступа только для чтения'; - + String get repeater_guestPasswordHelper => + 'Пароль для доступа только для чтения'; + @override String get repeater_radioSettings => 'Настройки радио'; - + @override String get repeater_frequencyMhz => 'Частота (МГц)'; - + @override String get repeater_frequencyHelper => '300–2500 МГц'; - + @override String get repeater_txPower => 'Мощность передачи'; - + @override String get repeater_txPowerHelper => '1–30 дБм'; - + @override String get repeater_bandwidth => 'Полоса пропускания'; - + @override String get repeater_spreadingFactor => 'Коэффициент расширения'; - + @override String get repeater_codingRate => 'Коэффициент кодирования'; - + @override String get repeater_locationSettings => 'Настройки местоположения'; - + @override String get repeater_latitude => 'Широта'; - + @override - String get repeater_latitudeHelper => 'В десятичных градусах (напр., 37.7749)'; - + String get repeater_latitudeHelper => + 'В десятичных градусах (напр., 37.7749)'; + @override String get repeater_longitude => 'Долгота'; - + @override - String get repeater_longitudeHelper => 'В десятичных градусах (напр., -122.4194)'; - + String get repeater_longitudeHelper => + 'В десятичных градусах (напр., -122.4194)'; + @override String get repeater_features => 'Функции'; - + @override String get repeater_packetForwarding => 'Пересылка пакетов'; - + @override String get repeater_packetForwardingSubtitle => 'Разрешить репитеру пересылать пакеты'; - + @override String get repeater_guestAccess => 'Гостевой доступ'; - + @override - String get repeater_guestAccessSubtitle => 'Разрешить гостевой доступ только для чтения'; - + String get repeater_guestAccessSubtitle => + 'Разрешить гостевой доступ только для чтения'; + @override String get repeater_privacyMode => 'Режим конфиденциальности'; - + @override String get repeater_privacyModeSubtitle => 'Скрывать имя/местоположение в оповещениях'; - + @override String get repeater_advertisementSettings => 'Настройки анонсирования'; - + @override String get repeater_localAdvertInterval => 'Интервал локальных анонсирований'; - + @override String repeater_localAdvertIntervalMinutes(int minutes) { return '$minutes минут'; -} - + } + @override - String get repeater_floodAdvertInterval => 'Интервал анонсирований рассылкой (flood)'; - + String get repeater_floodAdvertInterval => + 'Интервал анонсирований рассылкой (flood)'; + @override String repeater_floodAdvertIntervalHours(int hours) { return '$hours часов'; -} - + } + @override String get repeater_encryptedAdvertInterval => 'Интервал зашифрованных анонсирований'; - + @override String get repeater_dangerZone => 'Опасная зона'; - + @override String get repeater_rebootRepeater => 'Перезагрузить репитер'; - + @override - String get repeater_rebootRepeaterSubtitle => 'Перезапустить устройство репитера'; - + String get repeater_rebootRepeaterSubtitle => + 'Перезапустить устройство репитера'; + @override String get repeater_rebootRepeaterConfirm => 'Вы уверены, что хотите перезагрузить этот репитер?'; - + @override String get repeater_regenerateIdentityKey => 'Пересоздать ключ идентификации'; - + @override String get repeater_regenerateIdentityKeySubtitle => 'Сгенерировать новую пару публичного/приватного ключей'; - + @override String get repeater_regenerateIdentityKeyConfirm => 'Это создаст новую идентичность для репитера. Продолжить?'; - + @override String get repeater_eraseFileSystem => 'Стереть файловую систему'; - + @override String get repeater_eraseFileSystemSubtitle => 'Отформатировать файловую систему репитера'; - + @override String get repeater_eraseFileSystemConfirm => 'ВНИМАНИЕ: это удалит все данные на репитере. Действие нельзя отменить!'; - + @override String get repeater_eraseSerialOnly => 'Очистка доступна только через последовательную консоль.'; - + @override String repeater_commandSent(String command) { return 'Команда отправлена: $command'; -} - + } + @override String repeater_errorSendingCommand(String error) { return 'Ошибка отправки команды: $error'; -} - + } + @override String get repeater_confirm => 'Подтвердить'; - + @override String get repeater_settingsSaved => 'Настройки успешно сохранены'; - + @override String repeater_errorSavingSettings(String error) { return 'Ошибка сохранения настроек: $error'; -} - + } + @override String get repeater_refreshBasicSettings => 'Обновить основные настройки'; - + @override String get repeater_refreshRadioSettings => 'Обновить настройки радио'; - + @override String get repeater_refreshTxPower => 'Обновить мощность передачи'; - + @override - String get repeater_refreshLocationSettings => 'Обновить настройки местоположения'; - + String get repeater_refreshLocationSettings => + 'Обновить настройки местоположения'; + @override String get repeater_refreshPacketForwarding => 'Обновить пересылку пакетов'; - + @override String get repeater_refreshGuestAccess => 'Обновить гостевой доступ'; - + @override String get repeater_refreshPrivacyMode => 'Обновить режим конфиденциальности'; - + @override String get repeater_refreshAdvertisementSettings => 'Обновить настройки анонсирований'; - + @override String repeater_refreshed(String label) { return '$label обновлён'; -} - + } + @override String repeater_errorRefreshing(String label) { return 'Ошибка обновления $label'; -} - + } + @override String get repeater_cliTitle => 'CLI репитера'; - + @override String get repeater_debugNextCommand => 'Отладка следующей команды'; - + @override String get repeater_commandHelp => 'Справка по командам'; - + @override String get repeater_clearHistory => 'Очистить историю'; - + @override String get repeater_noCommandsSent => 'Команды ещё не отправлялись'; - + @override String get repeater_typeCommandOrUseQuick => 'Введите команду ниже или используйте быстрые команды'; - + @override String get repeater_enterCommandHint => 'Введите команду...'; - + @override String get repeater_previousCommand => 'Предыдущая команда'; - + @override String get repeater_nextCommand => 'Следующая команда'; - + @override String get repeater_enterCommandFirst => 'Сначала введите команду'; - + @override String get repeater_cliCommandFrameTitle => 'Фрейм CLI-команды'; - + @override String repeater_cliCommandError(String error) { return 'Ошибка: $error'; -} - + } + @override String get repeater_cliQuickGetName => 'Получить имя'; - + @override String get repeater_cliQuickGetRadio => 'Получить радио'; - + @override String get repeater_cliQuickGetTx => 'Получить TX'; - + @override String get repeater_cliQuickNeighbors => 'Соседи'; - + @override String get repeater_cliQuickVersion => 'Версия'; - + @override String get repeater_cliQuickAdvertise => 'Анонсировать'; - + @override String get repeater_cliQuickClock => 'Время'; - + @override String get repeater_cliHelpAdvert => 'Отправляет пакет анонсирования'; - + @override String get repeater_cliHelpReboot => 'Перезагружает устройство. (обычно вы получите «Тайм-аут» — это нормально)'; - + @override String get repeater_cliHelpClock => 'Показывает текущее время по часам устройства.'; - + @override String get repeater_cliHelpPassword => 'Устанавливает новый пароль администратора для устройства.'; - + @override String get repeater_cliHelpVersion => 'Показывает версию устройства и дату сборки прошивки.'; - + @override String get repeater_cliHelpClearStats => 'Сбрасывает различные счётчики статистики в ноль.'; - + @override - String get repeater_cliHelpSetAf => 'Устанавливает коэффициент времени в эфире.'; - + String get repeater_cliHelpSetAf => + 'Устанавливает коэффициент времени в эфире.'; + @override String get repeater_cliHelpSetTx => 'Устанавливает мощность передачи LoRa в дБм. (требуется перезагрузка)'; - + @override String get repeater_cliHelpSetRepeat => 'Включает или отключает роль репитера для этой ноды.'; - + @override String get repeater_cliHelpSetAllowReadOnly => '(Сервер комнат) Если «on», то вход без пароля разрешён, но публиковать в комнату нельзя (только чтение)'; - + @override String get repeater_cliHelpSetFloodMax => 'Устанавливает максимальное число хопов для входящих пакетов в режиме рассылки (если >= макс., пакет не пересылается)'; - + @override String get repeater_cliHelpSetIntThresh => 'Устанавливает порог интерференции (в дБ). По умолчанию 14. Установите 0, чтобы отключить обнаружение помех.'; - + @override String get repeater_cliHelpSetAgcResetInterval => 'Устанавливает интервал сброса автоматической регулировки усиления. Установите 0, чтобы отключить.'; - + @override String get repeater_cliHelpSetMultiAcks => 'Включает или отключает функцию «двойных ACK».'; - + @override String get repeater_cliHelpSetAdvertInterval => 'Устанавливает интервал (в минутах) отправки локального (нулевой хоп) анонсирования. Установите 0, чтобы отключить.'; - + @override String get repeater_cliHelpSetFloodAdvertInterval => 'Устанавливает интервал (в часах) отправки анонсирований рассылкой. Установите 0, чтобы отключить.'; - + @override String get repeater_cliHelpSetGuestPassword => 'Устанавливает/обновляет гостевой пароль. (для репитеров гости могут отправлять запрос «Get Stats»)'; - + @override String get repeater_cliHelpSetName => 'Устанавливает имя в оповещениях.'; - + @override String get repeater_cliHelpSetLat => 'Устанавливает широту для карты в оповещениях. (десятичные градусы)'; - + @override String get repeater_cliHelpSetLon => 'Устанавливает долготу для карты в оповещениях. (десятичные градусы)'; - + @override String get repeater_cliHelpSetRadio => 'Устанавливает полностью новые параметры радио и сохраняет их в настройки. Требуется команда «reboot» для применения.'; - + @override String get repeater_cliHelpSetRxDelay => 'Устанавливает (экспериментально) базовую задержку (>1 для эффекта) для принятых пакетов на основе качества сигнала. Установите 0, чтобы отключить.'; - + @override String get repeater_cliHelpSetTxDelay => 'Устанавливает множитель времени в эфире для пакета в режиме рассылки и применяет случайную задержку перед пересылкой (чтобы уменьшить коллизии).'; - + @override String get repeater_cliHelpSetDirectTxDelay => 'То же, что txdelay, но для случайной задержки пересылки пакетов в прямом режиме.'; - + @override String get repeater_cliHelpSetBridgeEnabled => 'Включить/выключить мост.'; - + @override String get repeater_cliHelpSetBridgeDelay => 'Установить задержку перед ретрансляцией пакетов.'; - + @override String get repeater_cliHelpSetBridgeSource => 'Выбрать, будет ли мост ретранслировать полученные или отправленные пакеты.'; - + @override String get repeater_cliHelpSetBridgeBaud => 'Установить скорость последовательного соединения для мостов RS232.'; - + @override String get repeater_cliHelpSetBridgeSecret => 'Установить секрет моста для мостов ESP-NOW.'; - + @override String get repeater_cliHelpSetAdcMultiplier => 'Устанавливает пользовательский коэффициент коррекции напряжения батареи (поддерживается только на некоторых платах).'; - + @override String get repeater_cliHelpTempRadio => 'Устанавливает временные параметры радио на заданное число минут, затем возвращает исходные. (НЕ сохраняется в настройки).'; - + @override String get repeater_cliHelpSetPerm => 'Изменяет ACL. Удаляет запись (по префиксу публичного ключа), если «permissions» равен нулю. Добавляет новую запись, если указан полный ключ и он отсутствует в ACL. Обновляет запись по совпадению префикса. Биты прав зависят от роли прошивки, но младшие 2 бита: 0 (Гость), 1 (Только чтение), 2 (Чтение/запись), 3 (Админ)'; - + @override String get repeater_cliHelpGetBridgeType => 'Получает тип моста: none, rs232, espnow'; - + @override String get repeater_cliHelpLogStart => 'Начинает запись пакетов в файловую систему.'; - + @override - String get repeater_cliHelpLogStop => 'Останавливает запись пакетов в файловую систему.'; - + String get repeater_cliHelpLogStop => + 'Останавливает запись пакетов в файловую систему.'; + @override String get repeater_cliHelpLogErase => 'Удаляет журналы пакетов из файловой системы.'; - + @override String get repeater_cliHelpNeighbors => 'Показывает список других репитеров, услышанных через оповещения нулевого хопа. Каждая строка: префикс-id-в-hex:временная-метка:snr×4'; - + @override String get repeater_cliHelpNeighborRemove => 'Удаляет первую подходящую запись (по префиксу публичного ключа в hex) из списка соседей.'; - + @override String get repeater_cliHelpRegion => '(только через последовательный порт) Показывает все определённые регионы и текущие права на рассылку.'; - + @override String get repeater_cliHelpRegionLoad => 'ПРИМЕЧАНИЕ: это специальная многострочная команда. Каждая следующая строка — имя региона (с отступом пробелами для указания иерархии, минимум один пробел). Завершается пустой строкой.'; - + @override String get repeater_cliHelpRegionGet => 'Ищет регион по префиксу имени (или «*» для глобальной области). Отвечает: «-> имя-региона (родитель) \'F\'»'; - + @override String get repeater_cliHelpRegionPut => 'Добавляет или обновляет определение региона с заданным именем.'; - + @override String get repeater_cliHelpRegionRemove => 'Удаляет определение региона с заданным именем. (должно точно совпадать и не иметь дочерних регионов)'; - + @override String get repeater_cliHelpRegionAllowf => 'Разрешает рассылку («F»lood) для заданного региона. («*» для глобальной/устаревшей области)'; - + @override String get repeater_cliHelpRegionDenyf => 'Запрещает рассылку («F»lood) для заданного региона. (НЕ рекомендуется для глобальной области!)'; - + @override String get repeater_cliHelpRegionHome => 'Показывает текущий «домашний» регион. (Пока не используется, зарезервировано на будущее)'; - + @override - String get repeater_cliHelpRegionHomeSet => 'Устанавливает «домашний» регион.'; - + String get repeater_cliHelpRegionHomeSet => + 'Устанавливает «домашний» регион.'; + @override String get repeater_cliHelpRegionSave => 'Сохраняет список/карту регионов в память.'; - + @override String get repeater_cliHelpGps => 'Показывает статус GPS. Если GPS выключен — отвечает только «off». Если включён — показывает статус, фиксацию, количество спутников.'; - + @override String get repeater_cliHelpGpsOnOff => 'Переключает состояние питания GPS.'; - + @override - String get repeater_cliHelpGpsSync => 'Синхронизирует время ноды с часами GPS.'; - + String get repeater_cliHelpGpsSync => + 'Синхронизирует время ноды с часами GPS.'; + @override String get repeater_cliHelpGpsSetLoc => 'Устанавливает позицию ноды по координатам GPS и сохраняет в настройки.'; - + @override String get repeater_cliHelpGpsAdvert => - 'Показывает конфигурацию передачи местоположения в анонсированиях: - - none: не включать местоположение - - share: передавать GPS-координаты (из SensorManager) - - prefs: передавать координаты из настроек'; - + 'Показывает конфигурацию передачи местоположения в анонсированиях:\n- none: не включать местоположение\n- share: передавать GPS-координаты (из SensorManager)\n- prefs: передавать координаты из настроек'; + @override String get repeater_cliHelpGpsAdvertSet => 'Устанавливает конфигурацию передачи местоположения.'; - + @override String get repeater_commandsListTitle => 'Список команд'; - + @override String get repeater_commandsListNote => 'ПРИМЕЧАНИЕ: для большинства команд «set ...» существуют соответствующие команды «get ...».'; - + @override String get repeater_general => 'Общие'; - + @override String get repeater_settingsCategory => 'Настройки'; - + @override String get repeater_bridge => 'Мост'; - + @override String get repeater_logging => 'Журналирование'; - + @override String get repeater_neighborsRepeaterOnly => 'Соседи (только для репитеров)'; - + @override String get repeater_regionManagementRepeaterOnly => 'Управление регионами (только для репитеров)'; - + @override String get repeater_regionNote => 'Команды регионов введены для управления определениями регионов и правами доступа.'; - + @override String get repeater_gpsManagement => 'Управление GPS'; - + @override String get repeater_gpsNote => 'Команда gps введена для управления параметрами, связанными с местоположением.'; - + @override String get telemetry_receivedData => 'Полученные телеметрические данные'; - + @override String get telemetry_requestTimeout => 'Время ожидания телеметрии истекло.'; - + @override String telemetry_errorLoading(String error) { return 'Ошибка загрузки телеметрии: $error'; -} - + } + @override String get telemetry_noData => 'Данные телеметрии недоступны.'; - + @override String telemetry_channelTitle(int channel) { return 'Канал $channel'; -} - + } + @override String get telemetry_batteryLabel => 'Батарея'; - + @override String get telemetry_voltageLabel => 'Напряжение'; - + @override String get telemetry_mcuTemperatureLabel => 'Температура МК'; - + @override String get telemetry_temperatureLabel => 'Температура'; - + @override String get telemetry_currentLabel => 'Ток'; - + @override - String telemetry_batteryValue(int percent, String volts) { - return '$percent% / ${volts}В'; -} - + String telemetry_batteryValue(int percent, String volts) { + return '$percent% / $voltsВ'; + } + @override String telemetry_voltageValue(String volts) { - return '${volts}В'; -} - + return '$voltsВ'; + } + @override String telemetry_currentValue(String amps) { - return '${amps}А'; -} - + return '$ampsА'; + } + @override - String telemetry_temperatureValue(String celsius, String fahrenheit) { + String telemetry_temperatureValue(String celsius, String fahrenheit) { return '$celsius°C / $fahrenheit°F'; -} - + } + @override String get neighbors_receivedData => 'Полученные данные о соседях'; - + @override - String get neighbors_requestTimedOut => 'Время ожидания данных о соседях истекло.'; - + String get neighbors_requestTimedOut => + 'Время ожидания данных о соседях истекло.'; + @override String neighbors_errorLoading(String error) { return 'Ошибка загрузки соседей: $error'; -} - + } + @override String get neighbors_repeatersNeighbours => 'Соседи репитеров'; - + @override String get neighbors_noData => 'Данные о соседях недоступны.'; - + @override String neighbors_unknownContact(String pubkey) { return 'Неизвестный $pubkey'; -} - + } + @override String neighbors_heardAgo(String time) { - return 'Слышали: $time назад'; -} - + return 'Слушал(а): $time назад'; + } + @override String get channelPath_title => 'Путь пакета'; - + @override String get channelPath_viewMap => 'Посмотреть на карте'; - + @override String get channelPath_otherObservedPaths => 'Другие наблюдаемые пути'; - + @override String get channelPath_repeaterHops => 'Хопы через репитеры'; - + @override String get channelPath_noHopDetails => 'Детали хопов для этого пакета не предоставлены.'; - + @override String get channelPath_messageDetails => 'Детали сообщения'; - + @override String get channelPath_senderLabel => 'Отправитель'; - + @override String get channelPath_timeLabel => 'Время'; - + @override String get channelPath_repeatsLabel => 'Повторы'; - + @override String channelPath_pathLabel(int index) { return 'Путь $index'; -} - + } + @override String get channelPath_observedLabel => 'Наблюдаемый'; - + @override - String channelPath_observedPathTitle(int index, String hops) { + String channelPath_observedPathTitle(int index, String hops) { return 'Наблюдаемый путь $index • $hops'; -} - + } + @override String get channelPath_noLocationData => 'Нет данных о местоположении'; - + @override - String channelPath_timeWithDate(int day, int month, String time) { + String channelPath_timeWithDate(int day, int month, String time) { return '$day/$month $time'; -} - + } + @override String channelPath_timeOnly(String time) { return '$time'; -} - + } + @override String get channelPath_unknownPath => 'Неизвестный'; - + @override String get channelPath_floodPath => 'Рассылка'; - + @override String get channelPath_directPath => 'Прямой'; - + @override String channelPath_observedZeroOf(int total) { return '0 из $total хопов'; -} - + } + @override String channelPath_observedSomeOf(int observed, int total) { return '$observed из $total хопов'; -} - + } + @override String get channelPath_mapTitle => 'Карта пути'; - + @override String get channelPath_noRepeaterLocations => 'Нет данных о местоположении репитеров для этого пути.'; - + @override String channelPath_primaryPath(int index) { return 'Путь $index (Основной)'; -} - + } + @override String get channelPath_pathLabelTitle => 'Путь'; - + @override String get channelPath_observedPathHeader => 'Наблюдаемый путь'; - + @override - String channelPath_selectedPathLabel(String label, String prefixes) { + String channelPath_selectedPathLabel(String label, String prefixes) { return '$label • $prefixes'; -} - + } + @override String get channelPath_noHopDetailsAvailable => 'Детали хопов для этого пакета недоступны.'; - + @override String get channelPath_unknownRepeater => 'Неизвестный репитер'; - + @override String get community_title => 'Сообщество'; - + @override String get community_create => 'Создать сообщество'; - + @override String get community_createDesc => 'Создать новое сообщество и поделиться через QR-код.'; - + @override String get community_join => 'Присоединиться'; - + @override String get community_joinTitle => 'Присоединиться к сообществу'; - + @override String community_joinConfirmation(String name) { return 'Вы хотите присоединиться к сообществу \"$name\"?'; -} - + } + @override String get community_scanQr => 'Сканировать QR-код сообщества'; - + @override String get community_scanInstructions => 'Наведите камеру на QR-код сообщества'; - + @override String get community_showQr => 'Показать QR-код'; - + @override String get community_publicChannel => 'Публичный канал сообщества'; - + @override String get community_hashtagChannel => 'Хэштег-канал сообщества'; - + @override String get community_name => 'Имя сообщества'; - + @override String get community_enterName => 'Введите имя сообщества'; - + @override String community_created(String name) { return 'Сообщество \"$name\" создано'; -} - + } + @override String community_joined(String name) { return 'Присоединились к сообществу \"$name\"'; -} - + } + @override String get community_qrTitle => 'Поделиться сообществом'; - + @override String community_qrInstructions(String name) { return 'Отсканируйте этот QR-код, чтобы присоединиться к \"$name\"'; -} - + } + @override String get community_hashtagPrivacyHint => 'Хэштег-каналы сообщества доступны только его участникам'; - + @override String get community_invalidQrCode => 'Недопустимый QR-код сообщества'; - + @override String get community_alreadyMember => 'Уже участник'; - + @override String community_alreadyMemberMessage(String name) { return 'Вы уже участник сообщества \"$name\".'; -} - + } + @override - String get community_addPublicChannel => 'Добавить публичный канал сообщества'; - + String get community_addPublicChannel => + 'Добавить публичный канал сообщества'; + @override String get community_addPublicChannelHint => 'Автоматически добавить публичный канал для этого сообщества'; - + @override - String get community_noCommunities => 'Вы ещё не присоединились ни к одному сообществу'; - + String get community_noCommunities => + 'Вы ещё не присоединились ни к одному сообществу'; + @override String get community_scanOrCreate => 'Отсканируйте QR-код или создайте сообщество, чтобы начать'; - + @override String get community_manageCommunities => 'Управление сообществами'; - + @override String get community_delete => 'Покинуть сообщество'; - + @override String community_deleteConfirm(String name) { return 'Покинуть \"$name\"?'; -} - + } + @override String community_deleteChannelsWarning(int count) { return 'Это также удалит $count канал(ов) и их сообщения.'; -} - + } + @override String community_deleted(String name) { return 'Покинули сообщество \"$name\"'; -} - + } + @override String get community_regenerateSecret => 'Пересоздать секрет'; - + @override String community_regenerateSecretConfirm(String name) { return 'Пересоздать секретный ключ для \"$name\"? Все участники должны будут отсканировать новый QR-код для продолжения общения.'; -} - + } + @override String get community_regenerate => 'Пересоздать'; - + @override String community_secretRegenerated(String name) { return 'Секрет пересоздан для \"$name\"'; -} - + } + @override String get community_updateSecret => 'Обновить секрет'; - + @override String community_secretUpdated(String name) { return 'Секрет обновлён для \"$name\"'; -} - + } + @override String community_scanToUpdateSecret(String name) { return 'Отсканируйте новый QR-код, чтобы обновить секрет для \"$name\"'; -} - + } + @override String get community_addHashtagChannel => 'Добавить хэштег-канал сообщества'; - + @override String get community_addHashtagChannelDesc => 'Добавить хэштег-канал для этого сообщества'; - + @override String get community_selectCommunity => 'Выбрать сообщество'; - + @override String get community_regularHashtag => 'Обычный хэштег'; - + @override - String get community_regularHashtagDesc => 'Публичный хэштег (любой может присоединиться)'; - + String get community_regularHashtagDesc => + 'Публичный хэштег (любой может присоединиться)'; + @override String get community_communityHashtag => 'Хэштег сообщества'; - + @override - String get community_communityHashtagDesc => 'Доступен только участникам сообщества'; - + String get community_communityHashtagDesc => + 'Доступен только участникам сообщества'; + @override String community_forCommunity(String name) { - return 'Для $name'; -} - + return 'Для $name'; + } + @override String get listFilter_tooltip => 'Фильтр и сортировка'; - + @override String get listFilter_sortBy => 'Сортировка по'; - + @override String get listFilter_latestMessages => 'Последние сообщения'; - + @override String get listFilter_heardRecently => 'Слышали недавно'; - + @override String get listFilter_az => 'По алфавиту'; - + @override String get listFilter_filters => 'Фильтры'; - + @override String get listFilter_all => 'Все'; - + @override String get listFilter_users => 'Пользователи'; - + @override String get listFilter_repeaters => 'Репитеры'; - + @override String get listFilter_roomServers => 'Серверы комнат'; - + @override String get listFilter_unreadOnly => 'Только непрочитанные'; - + @override String get listFilter_newGroup => 'Новая группа'; -} \ No newline at end of file +} diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 7ac1e85..e0c2cbe 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1,12 +1,9 @@ { "@@locale": "ru", - "appTitle": "MeshCore Open", - "nav_contacts": "Контакты", "nav_channels": "Каналы", "nav_map": "Карта", - "common_cancel": "Отмена", "common_ok": "OK", "common_connect": "Коннект", @@ -326,7 +323,7 @@ "chat_pathHistoryFull": "История маршрутов заполнена. Удалите записи, чтобы добавить новые.", "chat_hopSingular": "хоп", "chat_hopPlural": "хопов", - "chat_hopsCount": "{count} {plural, select, one {хоп} other {хопов}}", + "chat_hopsCount": "{count} {count, plural, one{хоп} few{хопа} many{хопов} other{хопов}}", "chat_successes": "успешно", "chat_removePath": "Удалить маршрут", "chat_noPathHistoryYet": "История маршрутов пока пуста.\nОтправьте сообщение, чтобы обнаружить маршруты.", @@ -340,7 +337,7 @@ "chat_floodModeEnabled": "Режим рассылки включён. Отключите через значок маршрутизации в панели приложения.", "chat_fullPath": "Полный маршрут", "chat_pathDetailsNotAvailable": "Детали маршрута ещё недоступны. Попробуйте отправить сообщение для обновления.", - "chat_pathSetHops": "Маршрут установлен: {hopCount} {plural, select, one {хоп} other {хопов}} — {status}", + "chat_pathSetHops": "Маршрут установлен: {hopCount} {hopCount, plural, one{хоп} few{хопа} many{хопов} other{хопов}} — {status}", "chat_pathSavedLocally": "Сохранено локально. Подключитесь для синхронизации.", "chat_pathDeviceConfirmed": "Подтверждено устройством.", "chat_pathDeviceNotConfirmed": "Ещё не подтверждено устройством.", @@ -453,7 +450,7 @@ "common_reload": "Обновить", "common_clear": "Очистить", "path_currentPath": "Текущий маршрут: {path}", - "path_usingHopsPath": "Используется маршрут из {count} {plural, select, one {хоп} other {хопов}}", + "path_usingHopsPath": "Используется маршрут из {count} {count, plural, one{хоп} few{хопа} many{хопов} other{хопов}}", "path_enterCustomPath": "Введите маршрут вручную", "path_currentPathLabel": "Текущий маршрут", "path_hexPrefixInstructions": "Введите 2-символьные шестнадцатеричные префиксы для каждого хопа, разделённые запятыми.", @@ -757,5 +754,25 @@ "listFilter_repeaters": "Репитеры", "listFilter_roomServers": "Серверы комнат", "listFilter_unreadOnly": "Только непрочитанные", - "listFilter_newGroup": "Новая группа" -} \ No newline at end of file + "listFilter_newGroup": "Новая группа", + "@chat_couldNotOpenLink": { + "placeholders": { + "url": { + "type": "String" + } + } + }, + "@neighbors_heardAgo": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "chat_open": "Открыть", + "chat_couldNotOpenLink": "Не удалось открыть ссылку: {url}", + "chat_openLink": "Открыть ссылку?", + "chat_openLinkConfirmation": "Хотите открыть эту ссылку в вашем браузере?", + "neighbors_heardAgo": "Слушал(а): {time} назад", + "chat_invalidLink": "Неправильный формат ссылки" +}