From 27d93ecaf9bb034007366361f79854ce72dd1f5c Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 5 Oct 2026 16:55:43 +0200 Subject: [PATCH] Add English and Russian ChatGUI interface translations --- tools/chatgui/CMakeLists.txt | 45 + tools/chatgui/build_nix.sh | 4 +- tools/chatgui/chatgui_doc.md | 12 + tools/chatgui/src/accountlist.cpp | 39 +- tools/chatgui/src/audiodevicesettingspage.cpp | 67 +- tools/chatgui/src/callwindow.cpp | 49 +- tools/chatgui/src/channellist.cpp | 15 +- tools/chatgui/src/channelsettingsdialog.cpp | 27 +- tools/chatgui/src/chatview.cpp | 3 +- tools/chatgui/src/connmonitorwindow.cpp | 7 +- tools/chatgui/src/creategroupdialog.cpp | 11 +- tools/chatgui/src/databasesettingspage.cpp | 71 +- tools/chatgui/src/dminvitedialog.cpp | 35 +- tools/chatgui/src/emoji.cpp | 9 +- tools/chatgui/src/gui_language.h | 15 + tools/chatgui/src/imageviewer_window.cpp | 9 +- tools/chatgui/src/inputbar.cpp | 35 +- tools/chatgui/src/invite_link.cpp | 29 +- tools/chatgui/src/inviteby.cpp | 41 +- tools/chatgui/src/invitedialog.cpp | 22 +- tools/chatgui/src/joindialog.cpp | 44 +- tools/chatgui/src/main.cpp | 41 +- tools/chatgui/src/mainwindow.cpp | 43 +- tools/chatgui/src/memberlistmodel.cpp | 7 +- tools/chatgui/src/memberpropsdialog.cpp | 39 +- tools/chatgui/src/messagedelegate.cpp | 19 +- tools/chatgui/src/messagelist.cpp | 45 +- tools/chatgui/src/networksettingspage.cpp | 44 +- tools/chatgui/src/nodespage.cpp | 36 +- tools/chatgui/src/ptt_hotkey_manager.cpp | 5 +- tools/chatgui/src/ptt_portal.cpp | 16 +- tools/chatgui/src/radio_audio_engine.cpp | 13 +- tools/chatgui/src/radiopanel.cpp | 39 +- tools/chatgui/src/radiosettingspage.cpp | 62 +- tools/chatgui/src/renamedialog.cpp | 11 +- tools/chatgui/src/settingsdialog.cpp | 88 +- tools/chatgui/src/settingsdialog.h | 1 + tools/chatgui/src/soundsettingspage.cpp | 25 +- tools/chatgui/src/statuspage.cpp | 13 +- tools/chatgui/src/storagesettingspage.cpp | 13 +- tools/chatgui/src/videoplayer_engine.cpp | 17 +- tools/chatgui/src/videoplayer_window.cpp | 11 +- tools/chatgui/tests/test_gui_language.cpp | 153 ++ tools/chatgui/tests/test_ptt_portal.cpp | 2 +- tools/chatgui/tests/test_radio_panel.cpp | 2 +- tools/chatgui/translations/chatgui_en.ts | 2058 +++++++++++++++++ tools/chatgui/translations/chatgui_ru.ts | 2058 +++++++++++++++++ .../chatgui/translations/translations.qrc.in | 7 + tools/chatgui/transport/node_config.cpp | 10 +- tools/chatgui/transport/node_config.h | 2 + 50 files changed, 4978 insertions(+), 491 deletions(-) create mode 100644 tools/chatgui/src/gui_language.h create mode 100644 tools/chatgui/tests/test_gui_language.cpp create mode 100644 tools/chatgui/translations/chatgui_en.ts create mode 100644 tools/chatgui/translations/chatgui_ru.ts create mode 100644 tools/chatgui/translations/translations.qrc.in diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index ebe3716c..7ae97e18 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -15,12 +15,47 @@ if(NOT Qt6_FOUND) set(QT_LIBS Qt5::Widgets Qt5::Network) set(QT_CORE Qt5::Core) set(QT_TEST_LIB Qt5::Test) + set(QT_PACKAGE Qt5) else() set(QT_LIBS Qt6::Widgets Qt6::Network) set(QT_CORE Qt6::Core) set(QT_TEST_LIB Qt6::Test) + set(QT_PACKAGE Qt6) endif() +# Каталоги в ресурсах: перевод работает и в переносимой сборке без файлов рядом с exe. +get_target_property(QT_QMAKE ${QT_PACKAGE}::qmake IMPORTED_LOCATION) +execute_process(COMMAND "${QT_QMAKE}" -query QT_HOST_BINS + OUTPUT_VARIABLE QT_BINARY_DIR OUTPUT_STRIP_TRAILING_WHITESPACE RESULT_VARIABLE QT_BINARY_RESULT) +if(NOT QT_BINARY_RESULT EQUAL 0) + message(FATAL_ERROR "Cannot query Qt host tools directory") +endif() +find_program(QT_GUI_LRELEASE NAMES lrelease HINTS "${QT_BINARY_DIR}" NO_DEFAULT_PATH) +if(NOT QT_GUI_LRELEASE) + message(FATAL_ERROR "Qt lrelease is required to build GUI translations (install Qt Linguist tools)") +endif() +execute_process(COMMAND "${QT_QMAKE}" -query QT_INSTALL_TRANSLATIONS + OUTPUT_VARIABLE QT_TRANSLATIONS_DIR OUTPUT_STRIP_TRAILING_WHITESPACE RESULT_VARIABLE QT_TRANSLATIONS_RESULT) +if(NOT QT_TRANSLATIONS_RESULT EQUAL 0) + message(FATAL_ERROR "Cannot query Qt translations directory") +endif() +set(QT_RUSSIAN_CATALOG "${QT_TRANSLATIONS_DIR}/qtbase_ru.qm") +if(NOT EXISTS "${QT_RUSSIAN_CATALOG}") + message(FATAL_ERROR "Qt Russian translations missing: ${QT_RUSSIAN_CATALOG}") +endif() +file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/translations") +set(GUI_QM_FILES) +foreach(LANGUAGE en ru) + set(QM_FILE "${CMAKE_CURRENT_BINARY_DIR}/translations/chatgui_${LANGUAGE}.qm") + add_custom_command(OUTPUT "${QM_FILE}" + COMMAND "${QT_GUI_LRELEASE}" -nounfinished "${CMAKE_CURRENT_SOURCE_DIR}/translations/chatgui_${LANGUAGE}.ts" -qm "${QM_FILE}" + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/translations/chatgui_${LANGUAGE}.ts" + VERBATIM) + list(APPEND GUI_QM_FILES "${QM_FILE}") +endforeach() +add_custom_target(chatgui_translations DEPENDS ${GUI_QM_FILES}) +configure_file(translations/translations.qrc.in "${CMAKE_CURRENT_BINARY_DIR}/translations.qrc" @ONLY) + find_package(rlottie REQUIRED HINTS "${CMAKE_CURRENT_SOURCE_DIR}/rlottie_build") message(STATUS "rlottie package: ${rlottie_DIR}") find_package(ZLIB REQUIRED) @@ -160,7 +195,9 @@ add_executable(vibechat db/db_manager.cpp ../../lib/sqlite3.c resources/chatgui.qrc + "${CMAKE_CURRENT_BINARY_DIR}/translations.qrc" ) +add_dependencies(vibechat chatgui_translations) target_include_directories(vibechat PRIVATE ${CMAKE_SOURCE_DIR}/../../lib ${CMAKE_SOURCE_DIR}/../../src ${CMAKE_SOURCE_DIR}/db ${CMAKE_SOURCE_DIR}/../../lib/libopus/include ${FFMPEG_INCLUDE_DIRS}) target_compile_definitions(vibechat PRIVATE SQLITE_THREADSAFE=1 USE_SQLITE) @@ -228,6 +265,14 @@ endif() enable_testing() +add_executable(test_gui_language tests/test_gui_language.cpp src/networksettingspage.cpp + transport/node_config.cpp transport/config_updater.cpp "${CMAKE_CURRENT_BINARY_DIR}/translations.qrc") +add_dependencies(test_gui_language chatgui_translations) +target_compile_definitions(test_gui_language PRIVATE GUI_TS_DIR="${CMAKE_CURRENT_SOURCE_DIR}/translations") +target_link_libraries(test_gui_language PRIVATE ${QT_LIBS} ${QT_TEST_LIB} OpenSSL::Crypto utun pthread) +add_test(NAME test_gui_language COMMAND test_gui_language) +set_tests_properties(test_gui_language PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen" TIMEOUT 30) + add_executable(test_emoji_playback tests/test_emoji_playback.cpp src/animtimer.cpp src/lottieicon.cpp src/emojipanel.cpp src/emojitabbar.cpp src/emoji.cpp src/chatview.cpp) target_link_libraries(test_emoji_playback PRIVATE ${QT_LIBS} ${QT_TEST_LIB} rlottie::rlottie ZLIB::ZLIB utun) diff --git a/tools/chatgui/build_nix.sh b/tools/chatgui/build_nix.sh index 782e4454..8734ce28 100755 --- a/tools/chatgui/build_nix.sh +++ b/tools/chatgui/build_nix.sh @@ -55,10 +55,10 @@ install_deps() { echo "[deps] installing build dependencies..." if [ "$OS_ID" = linux ]; then sudo apt-get update - sudo apt-get install -y build-essential cmake qt6-base-dev librlottie-dev zlib1g-dev libssl-dev \ + sudo apt-get install -y build-essential cmake qt6-base-dev qt6-tools-dev-tools qt6-translations-l10n librlottie-dev zlib1g-dev libssl-dev \ libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev else - sudo pkg install -y cmake qt6-base rlottie openssl ffmpeg + sudo pkg install -y cmake qt6-base qt6-tools qt6-translations rlottie openssl ffmpeg fi } diff --git a/tools/chatgui/chatgui_doc.md b/tools/chatgui/chatgui_doc.md index 32a15189..e7489775 100644 --- a/tools/chatgui/chatgui_doc.md +++ b/tools/chatgui/chatgui_doc.md @@ -4,6 +4,18 @@ Десктопный GUI-чат (Telegram-подобный) со встроенным P2P uTun-узлом. Приложение объединяет полнофункциональный интерфейс обмена сообщениями и децентрализованный транспортный слой в едином процессе. Рассчитан на работу без центральных серверов — вся маршрутизация, синхронизация и доставка сообщений идут напрямую между узлами по ETCP-протоколу. +Язык интерфейса: русский или английский. По умолчанию используется первый поддерживаемый язык из списка языков интерфейса ОС; +если поддерживаемых языков нет, используется английский. Выбор в «Настройки → Профиль → Язык интерфейса»: +«Язык системы», English или Русский. Смена языка применяется после перезапуска, настройка хранится в `[gui] language=system|en|ru`. +Региональный формат дат и чисел остаётся системным. Диагностические дампы ядра сохраняют исходные имена полей. + +Каталоги `translations/chatgui_en.ts` и `translations/chatgui_ru.ts` компилируются `lrelease` и встраиваются в приложение вместе +с `qtbase_ru.qm` для стандартных виджетов Qt. Для сборки нужны инструменты Qt Linguist и пакет переводов Qt +(Debian/Ubuntu: `qt6-tools-dev-tools`, `qt6-translations-l10n`; для Qt 5: `qttools5-dev-tools`, `qttranslations5-l10n`). +После добавления строк обновить каталоги из `tools/chatgui`: +`lupdate src -no-obsolete -locations relative -ts translations/chatgui_en.ts translations/chatgui_ru.ts`, +заполнить новые переводы и пересобрать. Проверка: `ctest --test-dir build -R test_gui_language --output-on-failure`. + ## 2. Архитектура ### 2.1. Уровни приложения diff --git a/tools/chatgui/src/accountlist.cpp b/tools/chatgui/src/accountlist.cpp index 7138d98b..0f1b8dd0 100644 --- a/tools/chatgui/src/accountlist.cpp +++ b/tools/chatgui/src/accountlist.cpp @@ -1,3 +1,4 @@ +#include #include "accountlist.h" #include "accountdelegate.h" #include "memberlistmodel.h" @@ -57,10 +58,10 @@ QString AccountList::relativeTimeStr(qint64 timestamp) { if (timestamp <= 0) return ""; qint64 diff = QDateTime::currentSecsSinceEpoch() - timestamp; if (diff < 0) diff = 0; - if (diff < 60) return QString("%1s").arg(diff); - if (diff < 3600) return QString("%1m").arg(diff / 60); - if (diff < 86400) return QString("%1h").arg(diff / 3600); - return QString("%1d").arg(diff / 86400); + if (diff < 60) return QCoreApplication::translate("ChatGui", "%1s").arg(diff); + if (diff < 3600) return QCoreApplication::translate("ChatGui", "%1m").arg(diff / 60); + if (diff < 86400) return QCoreApplication::translate("ChatGui", "%1h").arg(diff / 3600); + return QCoreApplication::translate("ChatGui", "%1d").arg(diff / 86400); } AccountList::AccountList(DbManager* db, QWidget *parent) @@ -75,7 +76,7 @@ AccountList::AccountList(DbManager* db, QWidget *parent) layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); - m_header = new QLabel("Members", this); + m_header = new QLabel(QCoreApplication::translate("ChatGui", "Members"), this); m_header->setAlignment(Qt::AlignCenter); m_header->setStyleSheet("font-weight: bold; padding: 8px; background: palette(window);"); layout->addWidget(m_header); @@ -104,7 +105,7 @@ AccountList::AccountList(DbManager* db, QWidget *parent) detailLayout->setContentsMargins(0, 0, 0, 0); detailLayout->setSpacing(0); - m_detailHeader = new QLabel("Select a member", m_detailPanel); + m_detailHeader = new QLabel(QCoreApplication::translate("ChatGui", "Select a member"), m_detailPanel); m_detailHeader->setWordWrap(true); m_detailHeader->setStyleSheet("font-weight: bold; padding: 4px 8px; background: palette(window);"); detailLayout->addWidget(m_detailHeader); @@ -177,11 +178,11 @@ void AccountList::setChannel(const QString& channelId) { m_detailTimer->stop(); m_linkTimer->stop(); m_detailNodeId = 0; - m_detailHeader->setText("Select a member"); + m_detailHeader->setText(QCoreApplication::translate("ChatGui", "Select a member")); m_detailText->clear(); - m_header->setText(isOwner ? QString::fromUtf8("Members \xF0\x9F\x94\x91") : QStringLiteral("Members")); - m_header->setToolTip(isOwner ? QStringLiteral("Channel owner (has private key)") : QString()); + m_header->setText(isOwner ? QCoreApplication::translate("ChatGui", "Members 🔑") : QCoreApplication::translate("ChatGui", "Members")); + m_header->setToolTip(isOwner ? QCoreApplication::translate("ChatGui", "Channel owner (has private key)") : QString()); } void AccountList::refresh() { m_model->refresh(); } @@ -371,9 +372,9 @@ void AccountList::updateDetailDisplay() { hdr.append(QString("● %2 %3") .arg(dotColor, m_detailName.toHtmlEscaped(), nodeHex)); if (m_detailLastSeen > 0) - hdr.append(QString("seen %1").arg(relativeTimeStr(m_detailLastSeen))); + hdr.append(QCoreApplication::translate("ChatGui", "seen %1").arg(relativeTimeStr(m_detailLastSeen))); if (m_detailCreated > 0) - hdr.append(QString("created %1").arg(relativeTimeStr(m_detailCreated))); + hdr.append(QCoreApplication::translate("ChatGui", "created %1").arg(relativeTimeStr(m_detailCreated))); m_detailHeader->setText(hdr.join(" ")); /* ── Body ── */ @@ -385,7 +386,7 @@ void AccountList::updateDetailDisplay() { }; if (!m_detailAddrs.isEmpty()) { - lines.append("── Addresses ──"); + lines.append(QCoreApplication::translate("ChatGui", "── Addresses ──")); for (const auto& a : m_detailAddrs) { lines.append(QString(" [sock%1] %2 %3 %4:%5 %6") .arg(a.socket_id) @@ -398,7 +399,7 @@ void AccountList::updateDetailDisplay() { } if (isSelf) { - lines.append("── Listen ──"); + lines.append(QCoreApplication::translate("ChatGui", "── Listen ──")); for (const auto& s : m_snapSocks) { const DetailAddress* a = nullptr; for (const auto& da : m_detailAddrs) { @@ -413,7 +414,7 @@ void AccountList::updateDetailDisplay() { } } else { if (!m_snapLinks.isEmpty()) { - lines.append("── Connections ──"); + lines.append(QCoreApplication::translate("ChatGui", "── Connections ──")); for (const auto& lk : m_snapLinks) { bool up = (lk.linkState == 3 && lk.linkStatus == 1); QString c = up ? "#4CAF50" : "#E53935"; @@ -482,15 +483,15 @@ void AccountList::onMemberContextMenu(const QPoint& pos) { QString name = m_model->data(index, Qt::DisplayRole).toString(); QMenu menu(m_listView); - QAction* pmAction = menu.addAction(QString::fromUtf8("Написать сообщение")); + QAction* pmAction = menu.addAction(QCoreApplication::translate("ChatGui", "Send message")); if (nodeId == m_db->myNodeId()) pmAction->setEnabled(false); - QAction* callAction = menu.addAction(QString::fromUtf8("Позвонить")); + QAction* callAction = menu.addAction(QCoreApplication::translate("ChatGui", "Call peer")); if (nodeId == m_db->myNodeId() || !m_model->isOnlineAt(index.row())) callAction->setEnabled(false); - QAction* connectAction = menu.addAction("Connect"); + QAction* connectAction = menu.addAction(QCoreApplication::translate("ChatGui", "Connect")); if (nodeId == m_db->myNodeId()) connectAction->setEnabled(false); - QAction* renameAction = menu.addAction("Rename"); + QAction* renameAction = menu.addAction(QCoreApplication::translate("ChatGui", "Rename")); if (nodeId == m_db->myNodeId()) renameAction->setEnabled(false); - QAction* editAction = menu.addAction("Edit"); + QAction* editAction = menu.addAction(QCoreApplication::translate("ChatGui", "Edit")); QAction* chosen = menu.exec(m_listView->viewport()->mapToGlobal(pos)); if (chosen == pmAction) { diff --git a/tools/chatgui/src/audiodevicesettingspage.cpp b/tools/chatgui/src/audiodevicesettingspage.cpp index a72e9489..2eb0ca0b 100644 --- a/tools/chatgui/src/audiodevicesettingspage.cpp +++ b/tools/chatgui/src/audiodevicesettingspage.cpp @@ -1,3 +1,4 @@ +#include #include "audiodevicesettingspage.h" #include "sound_manager.h" #include "audiorecorder.h" @@ -33,14 +34,14 @@ AudioDeviceSettingsPage::AudioDeviceSettingsPage(DbManager* db, AudioRecorder* r formLayout->setContentsMargins(0, 0, 0, 0); formLayout->setSpacing(10); - auto* outLabel = new QLabel("Playback device:", this); + auto* outLabel = new QLabel(QCoreApplication::translate("ChatGui", "Playback device:"), this); m_outputCombo = new QComboBox(this); m_outputCombo->setStyleSheet( "QComboBox { border: 1px solid palette(mid); border-radius: 4px;" " padding: 6px 10px; font-size: 13px; background: palette(base); }"); formLayout->addRow(outLabel, m_outputCombo); - auto* inLabel = new QLabel("Recording device:", this); + auto* inLabel = new QLabel(QCoreApplication::translate("ChatGui", "Recording device:"), this); m_inputCombo = new QComboBox(this); m_inputCombo->setStyleSheet(m_outputCombo->styleSheet()); formLayout->addRow(inLabel, m_inputCombo); @@ -54,7 +55,7 @@ AudioDeviceSettingsPage::AudioDeviceSettingsPage(DbManager* db, AudioRecorder* r layout->addWidget(sep1); /* --- Microphone test --- */ - auto* micLabel = new QLabel("Microphone test", this); + auto* micLabel = new QLabel(QCoreApplication::translate("ChatGui", "Microphone test"), this); micLabel->setStyleSheet("font-size: 13px; font-weight: bold;"); layout->addWidget(micLabel); @@ -67,7 +68,7 @@ AudioDeviceSettingsPage::AudioDeviceSettingsPage(DbManager* db, AudioRecorder* r m_micLevelBar->setFixedHeight(16); micRow->addWidget(m_micLevelBar, 1); - m_testMicBtn = new QPushButton("Test Microphone", this); + m_testMicBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Test Microphone"), this); m_testMicBtn->setCheckable(true); m_testMicBtn->setStyleSheet( "QPushButton { border: 1px solid palette(mid); border-radius: 4px;" @@ -84,11 +85,11 @@ AudioDeviceSettingsPage::AudioDeviceSettingsPage(DbManager* db, AudioRecorder* r layout->addWidget(sep2); /* --- Speaker test --- */ - auto* spkLabel = new QLabel("Speaker test", this); + auto* spkLabel = new QLabel(QCoreApplication::translate("ChatGui", "Speaker test"), this); spkLabel->setStyleSheet("font-size: 13px; font-weight: bold;"); layout->addWidget(spkLabel); - m_testSpeakerBtn = new QPushButton("Test Speaker (440Hz)", this); + m_testSpeakerBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Test Speaker (440Hz)"), this); m_testSpeakerBtn->setStyleSheet( "QPushButton { border: 1px solid palette(mid); border-radius: 4px;" " padding: 6px 16px; font-size: 13px; background: palette(button); }" @@ -103,14 +104,14 @@ AudioDeviceSettingsPage::AudioDeviceSettingsPage(DbManager* db, AudioRecorder* r layout->addWidget(sep3); /* --- Codec quality --- */ - auto* codecLabel = new QLabel("Voice codec quality", this); + auto* codecLabel = new QLabel(QCoreApplication::translate("ChatGui", "Voice codec quality"), this); codecLabel->setStyleSheet("font-size: 13px; font-weight: bold;"); layout->addWidget(codecLabel); m_codecPresetCombo = new QComboBox(this); - m_codecPresetCombo->addItem("Low (16 kbps, fast)", 0); - m_codecPresetCombo->addItem("Standard (32 kbps)", 1); - m_codecPresetCombo->addItem("High (64 kbps, best)", 2); + m_codecPresetCombo->addItem(QCoreApplication::translate("ChatGui", "Low (16 kbps, fast)"), 0); + m_codecPresetCombo->addItem(QCoreApplication::translate("ChatGui", "Standard (32 kbps)"), 1); + m_codecPresetCombo->addItem(QCoreApplication::translate("ChatGui", "High (64 kbps, best)"), 2); m_codecPresetCombo->setStyleSheet(m_outputCombo->styleSheet()); layout->addWidget(m_codecPresetCombo); @@ -121,7 +122,7 @@ AudioDeviceSettingsPage::AudioDeviceSettingsPage(DbManager* db, AudioRecorder* r layout->addWidget(sep5); /* --- Radio (PTT) Opus --- */ - auto* radioLabel = new QLabel("Radio (PTT) — Opus", this); + auto* radioLabel = new QLabel(QCoreApplication::translate("ChatGui", "Radio (PTT) — Opus"), this); radioLabel->setStyleSheet("font-size: 13px; font-weight: bold;"); layout->addWidget(radioLabel); @@ -130,20 +131,20 @@ AudioDeviceSettingsPage::AudioDeviceSettingsPage(DbManager* db, AudioRecorder* r radioForm->setSpacing(10); m_radioOpusModeCombo = new QComboBox(this); - m_radioOpusModeCombo->addItem("Voice (VOIP)", 0); - m_radioOpusModeCombo->addItem("Music (AUDIO)", 1); + m_radioOpusModeCombo->addItem(QCoreApplication::translate("ChatGui", "Voice (VOIP)"), 0); + m_radioOpusModeCombo->addItem(QCoreApplication::translate("ChatGui", "Music (AUDIO)"), 1); m_radioOpusModeCombo->setStyleSheet(m_outputCombo->styleSheet()); - radioForm->addRow(new QLabel("Mode:", this), m_radioOpusModeCombo); + radioForm->addRow(new QLabel(QCoreApplication::translate("ChatGui", "Mode:"), this), m_radioOpusModeCombo); m_radioOpusBitrateCombo = new QComboBox(this); for (int br : {16, 32, 64, 96, 128}) - m_radioOpusBitrateCombo->addItem(QString::number(br) + " kbps", br); + m_radioOpusBitrateCombo->addItem(QString::number(br) + QCoreApplication::translate("ChatGui", " kbps"), br); m_radioOpusBitrateCombo->setStyleSheet(m_outputCombo->styleSheet()); - radioForm->addRow(new QLabel("Bitrate:", this), m_radioOpusBitrateCombo); + radioForm->addRow(new QLabel(QCoreApplication::translate("ChatGui", "Bitrate:"), this), m_radioOpusBitrateCombo); layout->addLayout(radioForm); - m_radioCaptureStereoCb = new QCheckBox("Capture stereo (playback is always stereo)", this); + m_radioCaptureStereoCb = new QCheckBox(QCoreApplication::translate("ChatGui", "Capture stereo (playback is always stereo)"), this); m_radioCaptureStereoCb->setStyleSheet("font-size: 12px;"); layout->addWidget(m_radioCaptureStereoCb); @@ -159,9 +160,9 @@ AudioDeviceSettingsPage::AudioDeviceSettingsPage(DbManager* db, AudioRecorder* r m_jitterReserveSpin->setDecimals(1); m_jitterReserveSpin->setSingleStep(0.1); m_jitterReserveSpin->setValue(1.0); - m_jitterReserveSpin->setSuffix(" s"); - m_jitterReserveSpin->setToolTip("Maximum adaptive jitter reserve for calls. Applies to the next call."); - callForm->addRow("Call: maximum jitter reserve", m_jitterReserveSpin); + m_jitterReserveSpin->setSuffix(QCoreApplication::translate("ChatGui", " s")); + m_jitterReserveSpin->setToolTip(QCoreApplication::translate("ChatGui", "Maximum adaptive jitter reserve for calls. Applies to the next call.")); + callForm->addRow(QCoreApplication::translate("ChatGui", "Call: maximum jitter reserve"), m_jitterReserveSpin); layout->addLayout(callForm); buildCompressorUI(layout); @@ -183,19 +184,19 @@ void AudioDeviceSettingsPage::loadFromDb() { /* Output devices */ m_outputCombo->clear(); - m_outputCombo->addItem("Default (auto)", QString()); + m_outputCombo->addItem(QCoreApplication::translate("ChatGui", "Default (auto)"), QString()); for (int i = 0; i < playbackDevs.size(); i++) { QString label = playbackDevs[i].name; - if (playbackDevs[i].isDefault) label += " (default)"; + if (playbackDevs[i].isDefault) label += QCoreApplication::translate("ChatGui", " (default)"); m_outputCombo->addItem(label, playbackDevs[i].id); } /* Input devices */ m_inputCombo->clear(); - m_inputCombo->addItem("Default (auto)", QString()); + m_inputCombo->addItem(QCoreApplication::translate("ChatGui", "Default (auto)"), QString()); for (int i = 0; i < captureDevs.size(); i++) { QString label = captureDevs[i].name; - if (captureDevs[i].isDefault) label += " (default)"; + if (captureDevs[i].isDefault) label += QCoreApplication::translate("ChatGui", " (default)"); m_inputCombo->addItem(label, captureDevs[i].id); } @@ -366,11 +367,11 @@ void AudioDeviceSettingsPage::onTestMicToggled() { m_testRecorder->startRecording(true); m_micTestActive = true; m_levelTimer->start(); - m_testMicBtn->setText("Stop Test"); + m_testMicBtn->setText(QCoreApplication::translate("ChatGui", "Stop Test")); m_micLevelBar->setValue(0); } else { stopMicTest(); - m_testMicBtn->setText("Test Microphone"); + m_testMicBtn->setText(QCoreApplication::translate("ChatGui", "Test Microphone")); } } @@ -415,15 +416,15 @@ void AudioDeviceSettingsPage::onTestSpeaker() { } void AudioDeviceSettingsPage::buildCompressorUI(QVBoxLayout* layout) { - auto* compLabel = new QLabel("Voice Compressor", this); + auto* compLabel = new QLabel(QCoreApplication::translate("ChatGui", "Voice Compressor"), this); compLabel->setStyleSheet("font-size: 13px; font-weight: bold;"); layout->addWidget(compLabel); - m_compressorEnabledCb = new QCheckBox("Enable compressor (leveler/normalizer)", this); + m_compressorEnabledCb = new QCheckBox(QCoreApplication::translate("ChatGui", "Enable compressor (leveler/normalizer)"), this); m_compressorEnabledCb->setStyleSheet("font-size: 12px;"); layout->addWidget(m_compressorEnabledCb); - m_aecEnabledCb = new QCheckBox("Enable echo cancellation (AEC) in calls", this); + m_aecEnabledCb = new QCheckBox(QCoreApplication::translate("ChatGui", "Enable echo cancellation (AEC) in calls"), this); m_aecEnabledCb->setStyleSheet("font-size: 12px;"); layout->addWidget(m_aecEnabledCb); @@ -432,7 +433,7 @@ void AudioDeviceSettingsPage::buildCompressorUI(QVBoxLayout* layout) { auto* row = new QHBoxLayout(); row->setSpacing(6); auto* textLabel = new QLabel(name, this); - textLabel->setFixedWidth(180); + textLabel->setMinimumWidth(180); textLabel->setStyleSheet("font-size: 12px;"); row->addWidget(textLabel); slider = new QSlider(Qt::Horizontal, this); @@ -450,9 +451,9 @@ void AudioDeviceSettingsPage::buildCompressorUI(QVBoxLayout* layout) { layout->addLayout(row); }; - makeSliderRow("Max gain:", m_compressorMaxGainSlider, m_compressorMaxGainLabel, + makeSliderRow(QCoreApplication::translate("ChatGui", "Max gain:"), m_compressorMaxGainSlider, m_compressorMaxGainLabel, 1, 50, 1); - makeSliderRow("Rise rate (x/sec):", m_compressorRiseRateSlider, m_compressorRiseRateLabel, + makeSliderRow(QCoreApplication::translate("ChatGui", "Rise rate (x/sec):"), m_compressorRiseRateSlider, m_compressorRiseRateLabel, 2, 10, 1); auto connectSlider = [this](QSlider* slider) { @@ -464,7 +465,7 @@ void AudioDeviceSettingsPage::buildCompressorUI(QVBoxLayout* layout) { void AudioDeviceSettingsPage::updateCompressorLabels() { if (m_compressorMaxGainLabel) - m_compressorMaxGainLabel->setText(QString::number(m_compressorMaxGainSlider->value()) + " dB"); + m_compressorMaxGainLabel->setText(QString::number(m_compressorMaxGainSlider->value()) + QCoreApplication::translate("ChatGui", " dB")); if (m_compressorRiseRateLabel) { int v = m_compressorRiseRateSlider->value(); m_compressorRiseRateLabel->setText(QString::number(v) + "x"); diff --git a/tools/chatgui/src/callwindow.cpp b/tools/chatgui/src/callwindow.cpp index 8eaa770a..6c3bbb2a 100644 --- a/tools/chatgui/src/callwindow.cpp +++ b/tools/chatgui/src/callwindow.cpp @@ -1,3 +1,4 @@ +#include #include "callwindow.h" #include "../transport/gui_bridge.h" #include "call_audio_engine.h" @@ -136,7 +137,7 @@ static QWidget* wrapWithCaption(QWidget* btn, const QString& caption, QLabel** c CallWindow::CallWindow(QWidget* parent) : QWidget(parent, Qt::Window) { - setWindowTitle(QStringLiteral("Звонок")); + setWindowTitle(QCoreApplication::translate("ChatGui", "Call")); setFixedSize(360, 480); setAttribute(Qt::WA_DeleteOnClose, false); @@ -171,10 +172,10 @@ CallWindow::CallWindow(QWidget* parent) m_statsLabel->setStyleSheet("color: gray; font-size: 11px;"); m_statsLabel->hide(); - m_acceptBtn = makeRoundBtn(makeRoundIcon(QColor("#27ae60"), Glyph::Phone, 96), QStringLiteral("Принять"), this); - m_declineBtn = makeRoundBtn(makeRoundIcon(QColor("#e74c3c"), Glyph::PhoneDown, 96), QStringLiteral("Отклонить"), this); - m_hangupBtn = makeRoundBtn(makeRoundIcon(QColor("#e74c3c"), Glyph::PhoneDown, 96), QStringLiteral("Завершить"), this); - m_muteBtn = makeRoundBtn(makeRoundIcon(QColor("#7f8c8d"), Glyph::Mic, 96), QStringLiteral("Mute"), this); + m_acceptBtn = makeRoundBtn(makeRoundIcon(QColor("#27ae60"), Glyph::Phone, 96), QCoreApplication::translate("ChatGui", "Accept"), this); + m_declineBtn = makeRoundBtn(makeRoundIcon(QColor("#e74c3c"), Glyph::PhoneDown, 96), QCoreApplication::translate("ChatGui", "Decline"), this); + m_hangupBtn = makeRoundBtn(makeRoundIcon(QColor("#e74c3c"), Glyph::PhoneDown, 96), QCoreApplication::translate("ChatGui", "End call"), this); + m_muteBtn = makeRoundBtn(makeRoundIcon(QColor("#7f8c8d"), Glyph::Mic, 96), QCoreApplication::translate("ChatGui", "Mute"), this); m_muteBtn->setCheckable(true); m_deviceCombo = new QComboBox(this); @@ -194,10 +195,10 @@ CallWindow::CallWindow(QWidget* parent) lay->addWidget(m_statsLabel); lay->addStretch(1); - m_acceptCell = wrapWithCaption(m_acceptBtn, QStringLiteral("Принять")); - m_declineCell = wrapWithCaption(m_declineBtn, QStringLiteral("Отклонить")); - m_hangupCell = wrapWithCaption(m_hangupBtn, QStringLiteral("Завершить")); - m_muteCell = wrapWithCaption(m_muteBtn, QStringLiteral("Mute"), &m_muteCaption); + m_acceptCell = wrapWithCaption(m_acceptBtn, QCoreApplication::translate("ChatGui", "Accept")); + m_declineCell = wrapWithCaption(m_declineBtn, QCoreApplication::translate("ChatGui", "Decline")); + m_hangupCell = wrapWithCaption(m_hangupBtn, QCoreApplication::translate("ChatGui", "End call")); + m_muteCell = wrapWithCaption(m_muteBtn, QCoreApplication::translate("ChatGui", "Mute"), &m_muteCaption); QHBoxLayout* btn = new QHBoxLayout(); btn->setSpacing(10); @@ -237,29 +238,29 @@ void CallWindow::setBgpPath(const QString& path) { } void CallWindow::setConnectionType(uint8_t type, quint64 nextHop) { - const QString names[] = { QStringLiteral("нет подключения"), QStringLiteral("прямое"), - QStringLiteral("reverse"), QStringLiteral("indirect") }; - m_connectionLabel->setText(QStringLiteral("Подключение: %1").arg(names[type <= 3 ? type : 0])); - m_connectionLabel->setToolTip(type == 3 ? QStringLiteral("Посредник: 0x%1").arg(nextHop, 16, 16, QChar('0')) : QString()); + const QString names[] = { QCoreApplication::translate("ChatGui", "not connected"), QCoreApplication::translate("ChatGui", "direct"), + QCoreApplication::translate("ChatGui", "reverse"), QCoreApplication::translate("ChatGui", "indirect") }; + m_connectionLabel->setText(QCoreApplication::translate("ChatGui", "Connection: %1").arg(names[type <= 3 ? type : 0])); + m_connectionLabel->setToolTip(type == 3 ? QCoreApplication::translate("ChatGui", "Relay: 0x%1").arg(nextHop, 16, 16, QChar('0')) : QString()); } void CallWindow::setState(State s) { m_state = s; switch (s) { case Outgoing: - m_statusLabel->setText(QStringLiteral("Пытаемся связаться…")); + m_statusLabel->setText(QCoreApplication::translate("ChatGui", "Connecting…")); m_durationLabel->hide(); break; case Ringing: - m_statusLabel->setText(QStringLiteral("Звоним…")); + m_statusLabel->setText(QCoreApplication::translate("ChatGui", "Calling…")); m_durationLabel->hide(); break; case Incoming: - m_statusLabel->setText(QStringLiteral("Входящий звонок…")); + m_statusLabel->setText(QCoreApplication::translate("ChatGui", "Incoming call…")); m_durationLabel->hide(); break; case Active: - m_statusLabel->setText(QStringLiteral("Разговор")); + m_statusLabel->setText(QCoreApplication::translate("ChatGui", "In call")); m_durationLabel->setText(QStringLiteral("00:00")); m_durationLabel->show(); m_startMs = QDateTime::currentMSecsSinceEpoch(); @@ -269,7 +270,7 @@ void CallWindow::setState(State s) { break; case Ended: setConnectionType(0, 0); - m_statusLabel->setText(QStringLiteral("Завершён")); + m_statusLabel->setText(QCoreApplication::translate("ChatGui", "Ended")); m_durationTimer->stop(); break; } @@ -279,11 +280,11 @@ void CallWindow::setState(State s) { void CallWindow::populateDeviceCombo() { m_deviceCombo->blockSignals(true); m_deviceCombo->clear(); - m_deviceCombo->addItem(QStringLiteral("Default (auto)"), QString()); + m_deviceCombo->addItem(QCoreApplication::translate("ChatGui", "Default (auto)"), QString()); const auto devs = SoundManager::enumeratePlaybackDevices(); for (int i = 0; i < devs.size(); i++) { QString label = devs[i].name; - if (devs[i].isDefault) label += QStringLiteral(" (default)"); + if (devs[i].isDefault) label += QCoreApplication::translate("ChatGui", " (default)"); m_deviceCombo->addItem(label, devs[i].id); } QString cur = CallAudioEngine::instance()->playbackDevice(); @@ -328,7 +329,7 @@ void CallWindow::onMuteToggled(bool checked) { m_muteBtn->setIcon(makeRoundIcon(checked ? QColor("#e74c3c") : QColor("#7f8c8d"), checked ? Glyph::MicOff : Glyph::Mic, 96)); if (m_muteCaption) - m_muteCaption->setText(checked ? QStringLiteral("Вкл. микрофон") : QStringLiteral("Mute")); + m_muteCaption->setText(checked ? QCoreApplication::translate("ChatGui", "Unmute") : QCoreApplication::translate("ChatGui", "Mute")); } void CallWindow::onDeviceChanged(int index) { @@ -354,9 +355,9 @@ void CallWindow::updateDuration() { void CallWindow::setAudioStats(int rtt, int buffer, int tempo, int dropped, int underruns, int minimum, int maximum, int reserve) { - m_statsLabel->setText(QStringLiteral("Буфер: %1 мс · Запас: %2 мс\n" - "За 10 с: минимум %3 · максимум %4 мс\n" - "Скорость: %5× · RTT: %6 мс\nПотери буфера: %7 · Недостачи: %8") + m_statsLabel->setText(QCoreApplication::translate("ChatGui", + "Buffer: %1 ms · Reserve: %2 ms\nOver 10 s: minimum %3 · maximum %4 ms\nSpeed: %5× · RTT: %6 " + "ms\nBuffer drops: %7 · Underruns: %8") .arg(buffer).arg(reserve).arg(minimum).arg(maximum) .arg(tempo / 100.0, 0, 'f', 2).arg(rtt).arg(dropped).arg(underruns)); m_statsLabel->show(); diff --git a/tools/chatgui/src/channellist.cpp b/tools/chatgui/src/channellist.cpp index 3d2164c2..2d68cca4 100644 --- a/tools/chatgui/src/channellist.cpp +++ b/tools/chatgui/src/channellist.cpp @@ -1,3 +1,4 @@ +#include #include "channellist.h" #include "channeldelegate.h" #include "../db/db_manager.h" @@ -146,7 +147,7 @@ ChannelList::ChannelList(DbManager* db, QWidget *parent) layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); - auto *header = new QLabel("Channels", this); + auto *header = new QLabel(QCoreApplication::translate("ChatGui", "Channels"), this); header->setAlignment(Qt::AlignCenter); header->setStyleSheet("font-weight: bold; padding: 8px; background: palette(window);"); layout->addWidget(header); @@ -167,7 +168,7 @@ ChannelList::ChannelList(DbManager* db, QWidget *parent) auto *gearBtn = new QPushButton(QString::fromUtf8("\xE2\x9A\x99"), footer); gearBtn->setFixedSize(28, 28); - gearBtn->setToolTip(QString::fromUtf8("Настройки")); + gearBtn->setToolTip(QCoreApplication::translate("ChatGui", "Settings")); gearBtn->setStyleSheet( "QPushButton { border: none; font-size: 18px; background: transparent; color: #666; }" "QPushButton:hover { color: #3390EC; background: rgba(51,144,236,20); border-radius: 14px; }"); @@ -177,7 +178,7 @@ ChannelList::ChannelList(DbManager* db, QWidget *parent) auto *monitorBtn = new QPushButton(QString::fromUtf8("\xF0\x9F\x94\x8D"), footer); /* 🔍 */ monitorBtn->setFixedSize(28, 28); - monitorBtn->setToolTip(QString::fromUtf8("Connection Monitor")); + monitorBtn->setToolTip(QCoreApplication::translate("ChatGui", "Connection Monitor")); monitorBtn->setStyleSheet( "QPushButton { border: none; font-size: 16px; background: transparent; color: #666; }" "QPushButton:hover { color: #3390EC; background: rgba(51,144,236,20); border-radius: 14px; }"); @@ -186,9 +187,9 @@ ChannelList::ChannelList(DbManager* db, QWidget *parent) fl->addStretch(); - auto *createBtn = new QPushButton("GRP", footer); + auto *createBtn = new QPushButton(QCoreApplication::translate("ChatGui", "GRP"), footer); createBtn->setFixedSize(36, 28); - createBtn->setToolTip(QString::fromUtf8("Создать группу")); + createBtn->setToolTip(QCoreApplication::translate("ChatGui", "Create group")); createBtn->setStyleSheet( "QPushButton { border: none; border-radius: 6px; font-size: 11px; font-weight: bold;" " color: white; background: #27AE60; }" @@ -198,7 +199,7 @@ ChannelList::ChannelList(DbManager* db, QWidget *parent) auto *plusBtn = new QPushButton("+", footer); plusBtn->setFixedSize(28, 28); - plusBtn->setToolTip(QString::fromUtf8("Присоединиться")); + plusBtn->setToolTip(QCoreApplication::translate("ChatGui", "Join")); plusBtn->setStyleSheet( "QPushButton { border: none; border-radius: 14px; font-size: 18px; font-weight: bold;" " color: white; background: #3390EC; }" @@ -294,7 +295,7 @@ void ChannelList::loadChannels() { auto dms = m_db->getDmConversations(); if (!dms.isEmpty()) { auto *section = new QStandardItem(); - section->setData(QStringLiteral("Личные"), ChannelNameRole); + section->setData(QCoreApplication::translate("ChatGui", "Direct messages"), ChannelNameRole); section->setData(true, ChannelIsSectionRole); section->setFlags(Qt::NoItemFlags); m_model->appendRow(section); diff --git a/tools/chatgui/src/channelsettingsdialog.cpp b/tools/chatgui/src/channelsettingsdialog.cpp index 4cb8194b..69c1dadd 100644 --- a/tools/chatgui/src/channelsettingsdialog.cpp +++ b/tools/chatgui/src/channelsettingsdialog.cpp @@ -1,3 +1,4 @@ +#include #include "channelsettingsdialog.h" #include "../db/db_manager.h" #include "../transport/gui_bridge.h" @@ -16,7 +17,7 @@ ChannelSettingsDialog::ChannelSettingsDialog(const QString& channelId, DbManager , m_channelId(channelId) , m_db(db) { - setWindowTitle(QStringLiteral("Channel Settings")); + setWindowTitle(QCoreApplication::translate("ChatGui", "Channel Settings")); setMinimumWidth(340); setModal(true); @@ -24,7 +25,7 @@ ChannelSettingsDialog::ChannelSettingsDialog(const QString& channelId, DbManager layout->setContentsMargins(20, 16, 20, 16); layout->setSpacing(12); - auto* titleLabel = new QLabel(QStringLiteral("Settings for channel"), this); + auto* titleLabel = new QLabel(QCoreApplication::translate("ChatGui", "Settings for channel"), this); titleLabel->setStyleSheet("font-weight: bold; font-size: 14px;"); layout->addWidget(titleLabel); @@ -35,16 +36,18 @@ ChannelSettingsDialog::ChannelSettingsDialog(const QString& channelId, DbManager layout->addSpacing(8); - m_autoPlayVoice = new QCheckBox(QStringLiteral("Auto-play voice messages on receive"), this); - m_autoPlayVoice->setToolTip(QStringLiteral("Voice messages will play automatically when received. Multiple messages queue up and play sequentially without interrupting current playback.")); + m_autoPlayVoice = new QCheckBox(QCoreApplication::translate("ChatGui", "Auto-play voice messages on receive"), this); + m_autoPlayVoice->setToolTip(QCoreApplication::translate("ChatGui", + "Voice messages will play automatically when received. Multiple messages queue up and play " + "sequentially without interrupting current playback.")); layout->addWidget(m_autoPlayVoice); - m_soundOnMessage = new QCheckBox(QStringLiteral("Play sound on incoming message"), this); - m_soundOnMessage->setToolTip(QStringLiteral("Play a notification sound when a message arrives in this channel.")); + m_soundOnMessage = new QCheckBox(QCoreApplication::translate("ChatGui", "Play sound on incoming message"), this); + m_soundOnMessage->setToolTip(QCoreApplication::translate("ChatGui", "Play a notification sound when a message arrives in this channel.")); layout->addWidget(m_soundOnMessage); - m_pttMode = new QCheckBox(QStringLiteral("Push-To-Talk (PTT) mode"), this); - m_pttMode->setToolTip(QStringLiteral("Record voice messages with push-to-talk. (Coming soon)")); + m_pttMode = new QCheckBox(QCoreApplication::translate("ChatGui", "Push-To-Talk (PTT) mode"), this); + m_pttMode->setToolTip(QCoreApplication::translate("ChatGui", "Record voice messages with push-to-talk. (Coming soon)")); m_pttMode->setEnabled(false); layout->addWidget(m_pttMode); @@ -54,8 +57,8 @@ ChannelSettingsDialog::ChannelSettingsDialog(const QString& channelId, DbManager btnLayout->setSpacing(8); btnLayout->addStretch(); - auto* cancelBtn = new QPushButton(QStringLiteral("Cancel"), this); - cancelBtn->setFixedWidth(90); + auto* cancelBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Cancel"), this); + cancelBtn->setMinimumWidth(90); cancelBtn->setStyleSheet( "QPushButton { border: 1px solid palette(mid); border-radius: 4px;" " padding: 6px 16px; font-size: 13px; background: palette(button); }" @@ -63,8 +66,8 @@ ChannelSettingsDialog::ChannelSettingsDialog(const QString& channelId, DbManager connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject); btnLayout->addWidget(cancelBtn); - auto* saveBtn = new QPushButton(QStringLiteral("Save"), this); - saveBtn->setFixedWidth(90); + auto* saveBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Save"), this); + saveBtn->setMinimumWidth(90); saveBtn->setDefault(true); saveBtn->setStyleSheet( "QPushButton { border: none; border-radius: 4px;" diff --git a/tools/chatgui/src/chatview.cpp b/tools/chatgui/src/chatview.cpp index ac0bd7e5..9ceb718f 100644 --- a/tools/chatgui/src/chatview.cpp +++ b/tools/chatgui/src/chatview.cpp @@ -1,3 +1,4 @@ +#include #include "chatview.h" #include "messagedelegate.h" #include "emoji.h" @@ -44,7 +45,7 @@ ChatView::ChatView(QWidget *parent) " background: rgba(90,90,90,220);" "}" ); - m_scrollDownBtn->setToolTip(tr("Scroll to bottom")); + m_scrollDownBtn->setToolTip(QCoreApplication::translate("ChatGui", "Scroll to bottom")); m_scrollDownBtn->setVisible(false); connect(m_scrollDownBtn, &QPushButton::clicked, this, [this]() { diff --git a/tools/chatgui/src/connmonitorwindow.cpp b/tools/chatgui/src/connmonitorwindow.cpp index 9bcbfd68..5fd87a31 100644 --- a/tools/chatgui/src/connmonitorwindow.cpp +++ b/tools/chatgui/src/connmonitorwindow.cpp @@ -1,3 +1,4 @@ +#include // connmonitorwindow.cpp — ETCP connection monitor (standalone, non-modal window) #include "connmonitorwindow.h" #include "../transport/gui_bridge.h" @@ -31,7 +32,7 @@ static void connMetricsCb(const uint8_t* data, int len) { ConnMonitorWindow::ConnMonitorWindow(QWidget* parent) : QDialog(parent) { - setWindowTitle("Connection Monitor"); + setWindowTitle(QCoreApplication::translate("ChatGui", "Connection Monitor")); resize(900, 550); setAttribute(Qt::WA_DeleteOnClose); setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); @@ -40,7 +41,7 @@ ConnMonitorWindow::ConnMonitorWindow(QWidget* parent) mainLayout->setSpacing(4); mainLayout->setContentsMargins(8, 8, 8, 8); - auto* title = new QLabel("Active ETCP Connections (10 fps)", this); + auto* title = new QLabel(QCoreApplication::translate("ChatGui", "Active ETCP Connections (10 fps)"), this); title->setStyleSheet("font-weight: bold; font-size: 13px;"); mainLayout->addWidget(title); @@ -63,7 +64,7 @@ ConnMonitorWindow::ConnMonitorWindow(QWidget* parent) m_details->setStyleSheet( "QPlainTextEdit { border: 1px solid palette(mid); border-radius: 4px;" " padding: 8px; background: palette(base); }"); - m_details->setPlaceholderText("Select a connection to view metrics"); + m_details->setPlaceholderText(QCoreApplication::translate("ChatGui", "Select a connection to view metrics")); splitter->addWidget(m_connList); splitter->addWidget(m_details); diff --git a/tools/chatgui/src/creategroupdialog.cpp b/tools/chatgui/src/creategroupdialog.cpp index 06fa52d8..1dd11ee3 100644 --- a/tools/chatgui/src/creategroupdialog.cpp +++ b/tools/chatgui/src/creategroupdialog.cpp @@ -1,3 +1,4 @@ +#include #include "creategroupdialog.h" #include #include @@ -8,16 +9,16 @@ CreateGroupDialog::CreateGroupDialog(QWidget* parent) : QDialog(parent) , m_nameEdit(new QLineEdit(this)) { - setWindowTitle("Создать группу"); + setWindowTitle(QCoreApplication::translate("ChatGui", "Create group")); setMinimumWidth(350); setModal(true); auto* layout = new QVBoxLayout(this); - auto* nameLabel = new QLabel("Название группы:", this); + auto* nameLabel = new QLabel(QCoreApplication::translate("ChatGui", "Group name:"), this); layout->addWidget(nameLabel); - m_nameEdit->setPlaceholderText("Введите название..."); + m_nameEdit->setPlaceholderText(QCoreApplication::translate("ChatGui", "Enter a name...")); m_nameEdit->setMaxLength(128); layout->addWidget(m_nameEdit); @@ -26,11 +27,11 @@ CreateGroupDialog::CreateGroupDialog(QWidget* parent) auto* btnLayout = new QHBoxLayout(); btnLayout->addStretch(); - auto* cancelBtn = new QPushButton("Отмена", this); + auto* cancelBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Cancel"), this); connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject); btnLayout->addWidget(cancelBtn); - auto* createBtn = new QPushButton("Создать", this); + auto* createBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Create"), this); createBtn->setDefault(true); connect(createBtn, &QPushButton::clicked, this, [this]() { if (m_nameEdit->text().trimmed().isEmpty()) { diff --git a/tools/chatgui/src/databasesettingspage.cpp b/tools/chatgui/src/databasesettingspage.cpp index 7ed7149a..b5c5747a 100644 --- a/tools/chatgui/src/databasesettingspage.cpp +++ b/tools/chatgui/src/databasesettingspage.cpp @@ -1,3 +1,4 @@ +#include // databasesettingspage.cpp — Database maintenance page in settings #include "databasesettingspage.h" #include "../db/db_manager.h" @@ -17,7 +18,7 @@ DatabaseSettingsPage::DatabaseSettingsPage(DbManager* db, QWidget* parent) layout->setSpacing(10); layout->setContentsMargins(0, 0, 0, 0); - auto* title = new QLabel("Database Maintenance", this); + auto* title = new QLabel(QCoreApplication::translate("ChatGui", "Database Maintenance"), this); title->setStyleSheet("font-weight: bold; font-size: 13px;"); layout->addWidget(title); @@ -35,29 +36,29 @@ DatabaseSettingsPage::DatabaseSettingsPage(DbManager* db, QWidget* parent) QFileInfo fi(dbPath); if (fi.exists()) { qint64 sz = fi.size(); - if (sz < 1024) dbSize = QString("%1 B").arg(sz); - else if (sz < 1024 * 1024) dbSize = QString("%1 KB").arg(sz / 1024); - else dbSize = QString("%1 MB").arg(sz / (1024.0 * 1024.0), 0, 'f', 2); + if (sz < 1024) dbSize = QCoreApplication::translate("ChatGui", "%1 B").arg(sz); + else if (sz < 1024 * 1024) dbSize = QCoreApplication::translate("ChatGui", "%1 KB").arg(sz / 1024); + else dbSize = QCoreApplication::translate("ChatGui", "%1 MB").arg(sz / (1024.0 * 1024.0), 0, 'f', 2); } QFileInfo walFi(dbPath + "-wal"); if (walFi.exists()) { qint64 wsz = walFi.size(); - if (wsz < 1024) walSize = QString("%1 B").arg(wsz); - else if (wsz < 1024 * 1024) walSize = QString("%1 KB").arg(wsz / 1024); - else walSize = QString("%1 MB").arg(wsz / (1024.0 * 1024.0), 0, 'f', 2); + if (wsz < 1024) walSize = QCoreApplication::translate("ChatGui", "%1 B").arg(wsz); + else if (wsz < 1024 * 1024) walSize = QCoreApplication::translate("ChatGui", "%1 KB").arg(wsz / 1024); + else walSize = QCoreApplication::translate("ChatGui", "%1 MB").arg(wsz / (1024.0 * 1024.0), 0, 'f', 2); } else { - walSize = "n/a"; + walSize = QCoreApplication::translate("ChatGui", "n/a"); } } } m_filePathLabel = new QLabel(dbPath, this); m_filePathLabel->setStyleSheet("font-size: 11px; color: gray;"); m_filePathLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); - infoLayout->addRow("File:", m_filePathLabel); + infoLayout->addRow(QCoreApplication::translate("ChatGui", "File:"), m_filePathLabel); m_fileSizeLabel = new QLabel(dbSize, this); m_fileSizeLabel->setStyleSheet("font-size: 11px;"); - infoLayout->addRow("Size:", m_fileSizeLabel); + infoLayout->addRow(QCoreApplication::translate("ChatGui", "Size:"), m_fileSizeLabel); m_walSizeLabel = new QLabel(walSize, this); m_walSizeLabel->setStyleSheet("font-size: 11px;"); @@ -76,34 +77,34 @@ DatabaseSettingsPage::DatabaseSettingsPage(DbManager* db, QWidget* parent) btnLayout->setSpacing(6); auto* quickRow = new QHBoxLayout(); - m_quickBtn = new QPushButton("Quick Check", this); + m_quickBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Quick Check"), this); m_quickBtn->setStyleSheet( "QPushButton { border: 1px solid palette(mid); border-radius: 4px;" " padding: 5px 14px; font-size: 12px; background: palette(button); }" "QPushButton:hover { background: palette(light); }"); connect(m_quickBtn, &QPushButton::clicked, this, &DatabaseSettingsPage::onQuickCheck); quickRow->addWidget(m_quickBtn); - auto* quickDesc = new QLabel("Scans structure without verifying indexes (fast)", this); + auto* quickDesc = new QLabel(QCoreApplication::translate("ChatGui", "Scans structure without verifying indexes (fast)"), this); quickDesc->setStyleSheet("color: gray; font-size: 11px;"); quickRow->addWidget(quickDesc, 1); btnLayout->addLayout(quickRow); auto* fullRow = new QHBoxLayout(); - m_fullBtn = new QPushButton("Full Check", this); + m_fullBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Full Check"), this); m_fullBtn->setStyleSheet(m_quickBtn->styleSheet()); connect(m_fullBtn, &QPushButton::clicked, this, &DatabaseSettingsPage::onFullCheck); fullRow->addWidget(m_fullBtn); - auto* fullDesc = new QLabel("Verifies all data structures and indexes (thorough)", this); + auto* fullDesc = new QLabel(QCoreApplication::translate("ChatGui", "Verifies all data structures and indexes (thorough)"), this); fullDesc->setStyleSheet("color: gray; font-size: 11px;"); fullRow->addWidget(fullDesc, 1); btnLayout->addLayout(fullRow); auto* optRow = new QHBoxLayout(); - m_optimizeBtn = new QPushButton("Optimize", this); + m_optimizeBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Optimize"), this); m_optimizeBtn->setStyleSheet(m_quickBtn->styleSheet()); connect(m_optimizeBtn, &QPushButton::clicked, this, &DatabaseSettingsPage::onOptimize); optRow->addWidget(m_optimizeBtn); - auto* optDesc = new QLabel("Update query planner statistics for better performance", this); + auto* optDesc = new QLabel(QCoreApplication::translate("ChatGui", "Update query planner statistics for better performance"), this); optDesc->setStyleSheet("color: gray; font-size: 11px;"); optRow->addWidget(optDesc, 1); btnLayout->addLayout(optRow); @@ -123,13 +124,13 @@ DatabaseSettingsPage::DatabaseSettingsPage(DbManager* db, QWidget* parent) "QPlainTextEdit { font-family: monospace; font-size: 11px;" " background: palette(base); border: 1px solid palette(mid); border-radius: 4px;" " padding: 4px; }"); - m_resultText->setPlaceholderText("Run a check to see results..."); + m_resultText->setPlaceholderText(QCoreApplication::translate("ChatGui", "Run a check to see results...")); layout->addWidget(m_resultText); layout->addStretch(); if (!m_db || !m_db->m_db) { - m_statusLabel->setText("Database not connected"); + m_statusLabel->setText(QCoreApplication::translate("ChatGui", "Database not connected")); m_statusLabel->setStyleSheet("font-size: 12px; font-weight: bold; color: red;"); m_quickBtn->setEnabled(false); m_fullBtn->setEnabled(false); @@ -140,7 +141,7 @@ DatabaseSettingsPage::DatabaseSettingsPage(DbManager* db, QWidget* parent) void DatabaseSettingsPage::refreshStatus() { if (!m_db || !m_db->m_db) { - m_statusLabel->setText("Database not connected"); + m_statusLabel->setText(QCoreApplication::translate("ChatGui", "Database not connected")); m_statusLabel->setStyleSheet("font-size: 12px; font-weight: bold; color: red;"); m_quickBtn->setEnabled(false); m_fullBtn->setEnabled(false); @@ -157,16 +158,16 @@ void DatabaseSettingsPage::refreshStatus() { QFileInfo fi(dbPath); if (fi.exists()) { qint64 sz = fi.size(); - if (sz < 1024) m_fileSizeLabel->setText(QString("%1 B").arg(sz)); - else if (sz < 1024 * 1024) m_fileSizeLabel->setText(QString("%1 KB").arg(sz / 1024)); - else m_fileSizeLabel->setText(QString("%1 MB").arg(sz / (1024.0 * 1024.0), 0, 'f', 2)); + if (sz < 1024) m_fileSizeLabel->setText(QCoreApplication::translate("ChatGui", "%1 B").arg(sz)); + else if (sz < 1024 * 1024) m_fileSizeLabel->setText(QCoreApplication::translate("ChatGui", "%1 KB").arg(sz / 1024)); + else m_fileSizeLabel->setText(QCoreApplication::translate("ChatGui", "%1 MB").arg(sz / (1024.0 * 1024.0), 0, 'f', 2)); QFileInfo walFi(dbPath + "-wal"); if (walFi.exists()) { qint64 wsz = walFi.size(); - if (wsz < 1024) m_walSizeLabel->setText(QString("%1 B").arg(wsz)); - else if (wsz < 1024 * 1024) m_walSizeLabel->setText(QString("%1 KB").arg(wsz / 1024)); - else m_walSizeLabel->setText(QString("%1 MB").arg(wsz / (1024.0 * 1024.0), 0, 'f', 2)); - } else { m_walSizeLabel->setText("n/a"); } + if (wsz < 1024) m_walSizeLabel->setText(QCoreApplication::translate("ChatGui", "%1 B").arg(wsz)); + else if (wsz < 1024 * 1024) m_walSizeLabel->setText(QCoreApplication::translate("ChatGui", "%1 KB").arg(wsz / 1024)); + else m_walSizeLabel->setText(QCoreApplication::translate("ChatGui", "%1 MB").arg(wsz / (1024.0 * 1024.0), 0, 'f', 2)); + } else { m_walSizeLabel->setText(QCoreApplication::translate("ChatGui", "n/a")); } } } } @@ -176,47 +177,47 @@ void DatabaseSettingsPage::setBusy(bool busy) { m_fullBtn->setEnabled(!busy); m_optimizeBtn->setEnabled(!busy); if (busy) { - m_statusLabel->setText("Checking..."); + m_statusLabel->setText(QCoreApplication::translate("ChatGui", "Checking...")); m_statusLabel->setStyleSheet("font-size: 12px; margin-top: 4px; color: gray;"); } } void DatabaseSettingsPage::onQuickCheck() { if (!m_db || !m_db->m_db) { - m_statusLabel->setText("Database not open"); + m_statusLabel->setText(QCoreApplication::translate("ChatGui", "Database not open")); m_statusLabel->setStyleSheet("font-size: 12px; color: red;"); return; } setBusy(true); QTimer::singleShot(50, this, [this]() { IntegrityResult r = m_db->integrityCheckQuick(); - showResult("Quick Check", r); + showResult(QCoreApplication::translate("ChatGui", "Quick Check"), r); }); } void DatabaseSettingsPage::onFullCheck() { if (!m_db || !m_db->m_db) { - m_statusLabel->setText("Database not open"); + m_statusLabel->setText(QCoreApplication::translate("ChatGui", "Database not open")); m_statusLabel->setStyleSheet("font-size: 12px; color: red;"); return; } setBusy(true); QTimer::singleShot(50, this, [this]() { IntegrityResult r = m_db->integrityCheckFull(); - showResult("Full Check", r); + showResult(QCoreApplication::translate("ChatGui", "Full Check"), r); }); } void DatabaseSettingsPage::onOptimize() { if (!m_db || !m_db->m_db) { - m_statusLabel->setText("Database not open"); + m_statusLabel->setText(QCoreApplication::translate("ChatGui", "Database not open")); m_statusLabel->setStyleSheet("font-size: 12px; color: red;"); return; } setBusy(true); QTimer::singleShot(50, this, [this]() { IntegrityResult r = m_db->integrityOptimize(); - showResult("Optimize", r); + showResult(QCoreApplication::translate("ChatGui", "Optimize"), r); }); } @@ -226,11 +227,11 @@ void DatabaseSettingsPage::showResult(const QString& checkName, const IntegrityR m_optimizeBtn->setEnabled(true); if (result.ok) { - m_statusLabel->setText(QString("%1: OK (%2 ms)") + m_statusLabel->setText(QCoreApplication::translate("ChatGui", "%1: OK (%2 ms)") .arg(checkName).arg(result.elapsedMs)); m_statusLabel->setStyleSheet("font-size: 12px; font-weight: bold; color: green;"); } else { - m_statusLabel->setText(QString("%1: FAILED (%2 ms)") + m_statusLabel->setText(QCoreApplication::translate("ChatGui", "%1: FAILED (%2 ms)") .arg(checkName).arg(result.elapsedMs)); m_statusLabel->setStyleSheet("font-size: 12px; font-weight: bold; color: red;"); } diff --git a/tools/chatgui/src/dminvitedialog.cpp b/tools/chatgui/src/dminvitedialog.cpp index 7cb75a6d..d3955b87 100644 --- a/tools/chatgui/src/dminvitedialog.cpp +++ b/tools/chatgui/src/dminvitedialog.cpp @@ -1,3 +1,4 @@ +#include #include "dminvitedialog.h" #include "debug_ui.h" #include "../db/db_manager.h" @@ -18,28 +19,29 @@ extern "C" { DmInviteDialog::DmInviteDialog(DbManager* db, quint64 peer, const QString& name, const QString& source, QWidget* parent) : QDialog(parent), m_peer(peer), m_name(name), m_source(source) { - setWindowTitle(QStringLiteral("Пригласить в группу")); + setWindowTitle(QCoreApplication::translate("ChatGui", "Invite to group")); setMinimumWidth(400); auto* layout = new QVBoxLayout(this); - layout->addWidget(new QLabel(QStringLiteral("Получатель: %1").arg(name), this)); + layout->addWidget(new QLabel(QCoreApplication::translate("ChatGui", "Recipient: %1").arg(name), this)); m_groups = new QComboBox(this); for (const auto& group : db->getChannels()) { if (db->isChannelMember(group.channelId, db->myNodeId())) m_groups->addItem(group.name, group.channelId); } m_groups->setCurrentIndex(-1); - m_groups->setPlaceholderText(QStringLiteral("Выберите группу")); + m_groups->setPlaceholderText(QCoreApplication::translate("ChatGui", "Select a group")); layout->addWidget(m_groups); - auto* expiry = new QLabel(QStringLiteral("Ссылка действует %1 минут с момента создания.\n" - "При поздней доставке PM потребуется новое приглашение.").arg(JOIN_KEY_TTL_SECONDS / 60), this); + auto* expiry = new QLabel(QCoreApplication::translate("ChatGui", + "The link expires %1 minutes after creation.\nIf the direct message arrives late, a new " + "invitation will be needed.").arg(JOIN_KEY_TTL_SECONDS / 60), this); expiry->setWordWrap(true); layout->addWidget(expiry); m_status = new QLabel(this); m_status->setWordWrap(true); - if (m_groups->count() == 0) m_status->setText(QStringLiteral("Нет групп для приглашения.")); + if (m_groups->count() == 0) m_status->setText(QCoreApplication::translate("ChatGui", "No groups available to invite to.")); layout->addWidget(m_status); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Cancel, this); - buttons->button(QDialogButtonBox::Cancel)->setText(QStringLiteral("Отмена")); - m_send = buttons->addButton(QStringLiteral("Пригласить"), QDialogButtonBox::AcceptRole); + buttons->button(QDialogButtonBox::Cancel)->setText(QCoreApplication::translate("ChatGui", "Cancel")); + m_send = buttons->addButton(QCoreApplication::translate("ChatGui", "Invite"), QDialogButtonBox::AcceptRole); m_send->setEnabled(false); layout->addWidget(buttons); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); @@ -70,10 +72,11 @@ void DmInviteDialog::sendInvite() { if (m_pending || m_groups->currentIndex() < 0) return; if (!gui_bridge_is_uasync_ready() || !gui_bridge_get_inst()) { GUI_ERROR("invite PM: core unavailable"); - m_status->setText(QStringLiteral("Ядро не запущено.")); return; + m_status->setText(QCoreApplication::translate("ChatGui", "The core is not running.")); return; } auto* req = (struct dm_invite_req*)u_calloc(1, sizeof(struct dm_invite_req)); - if (!req) { GUI_ERROR("invite PM: request allocation failed"); m_status->setText(QStringLiteral("Недостаточно памяти.")); return; } + if (!req) { GUI_ERROR("invite PM: request allocation failed"); m_status->setText(QCoreApplication::translate("ChatGui", + "Not enough memory.")); return; } static uint64_t nextRequestId = 0; req->inst = gui_bridge_get_inst(); req->request_id = m_requestId = ++nextRequestId; req->peer_node_id = m_peer; req->channel_id = m_groups->currentData().toString().toULongLong(); @@ -81,7 +84,7 @@ void DmInviteDialog::sendInvite() { snprintf(req->peer_name, sizeof(req->peer_name), "%s", m_name.toUtf8().constData()); m_pending = true; m_groups->setEnabled(false); m_send->setEnabled(false); - m_status->setText(QStringLiteral("Создание и отправка приглашения…")); + m_status->setText(QCoreApplication::translate("ChatGui", "Creating and sending invitation…")); gui_bridge_post_uasync_fn(dm_invite_trampoline, req); } @@ -93,11 +96,11 @@ void DmInviteDialog::onResult(const uint8_t* data, int len) { if (!m_pending || request != m_requestId || peer != m_peer) return; m_pending = false; if (result == DM_INVITE_OK) { m_convId = QString::number(conv); accept(); return; } - const QString error = result == DM_INVITE_START_ERROR ? QStringLiteral("Не удалось создать личную беседу.") - : result == DM_INVITE_SEND_ERROR ? QStringLiteral("Не удалось отправить приглашение.") - : result == DM_INVITE_BUSY ? QStringLiteral("Другое приглашение ещё создаётся. Повторите попытку.") - : result == DM_INVITE_CANCELLED ? QStringLiteral("Создание приглашения отменено.") - : QStringLiteral("Не удалось создать приглашение. Проверьте подключение и повторите попытку."); + const QString error = result == DM_INVITE_START_ERROR ? QCoreApplication::translate("ChatGui", "Failed to create a direct conversation.") + : result == DM_INVITE_SEND_ERROR ? QCoreApplication::translate("ChatGui", "Failed to send the invitation.") + : result == DM_INVITE_BUSY ? QCoreApplication::translate("ChatGui", "Another invitation is still being created. Try again.") + : result == DM_INVITE_CANCELLED ? QCoreApplication::translate("ChatGui", "Invitation creation cancelled.") + : QCoreApplication::translate("ChatGui", "Failed to create an invitation. Check the connection and try again."); GUI_ERROR("invite PM dialog: failed request=%llu result=%d", (unsigned long long)request, result); m_status->setText(error); m_groups->setEnabled(true); m_send->setEnabled(m_groups->currentIndex() >= 0); } diff --git a/tools/chatgui/src/emoji.cpp b/tools/chatgui/src/emoji.cpp index 8e71bd34..f8cb8c86 100644 --- a/tools/chatgui/src/emoji.cpp +++ b/tools/chatgui/src/emoji.cpp @@ -1,3 +1,4 @@ +#include #include "emoji.h" QHash g_animatedEmojiMap; @@ -133,10 +134,10 @@ static QVector animations() { QVector builtinEmojiSet() { return { - {"Smileys & People", smileys()}, - {"Gestures", gestures()}, - {"Hearts & Symbols", hearts()}, - {"Animations", animations()}, + {QCoreApplication::translate("ChatGui", "Smileys & People"), smileys()}, + {QCoreApplication::translate("ChatGui", "Gestures"), gestures()}, + {QCoreApplication::translate("ChatGui", "Hearts & Symbols"), hearts()}, + {QCoreApplication::translate("ChatGui", "Animations"), animations()}, }; } diff --git a/tools/chatgui/src/gui_language.h b/tools/chatgui/src/gui_language.h new file mode 100644 index 00000000..2fff8c5a --- /dev/null +++ b/tools/chatgui/src/gui_language.h @@ -0,0 +1,15 @@ +/* Выбор языка интерфейса независимо от регионального формата дат и чисел. */ +#pragma once + +#include +#include +#include + +inline QString resolveGuiLanguage(const QString& preference, const QStringList& systemLanguages = QLocale::system().uiLanguages()) { + if (preference == QStringLiteral("en") || preference == QStringLiteral("ru")) return preference; + for (const QString& language : systemLanguages) { + const QString code = language.section('-', 0, 0).section('_', 0, 0).toLower(); + if (code == QStringLiteral("en") || code == QStringLiteral("ru")) return code; + } + return QStringLiteral("en"); +} diff --git a/tools/chatgui/src/imageviewer_window.cpp b/tools/chatgui/src/imageviewer_window.cpp index 858596ca..3dd80c1b 100644 --- a/tools/chatgui/src/imageviewer_window.cpp +++ b/tools/chatgui/src/imageviewer_window.cpp @@ -1,3 +1,4 @@ +#include #include "imageviewer_window.h" #include "../../lib/debug_config.h" #include @@ -34,14 +35,14 @@ static QPointF wheelPos(const QWheelEvent* e) { ImageViewerWindow::ImageViewerWindow(QWidget *parent) : QWidget(parent) { - setWindowTitle(QStringLiteral("Фото")); + setWindowTitle(QCoreApplication::translate("ChatGui", "Photo")); setAttribute(Qt::WA_DeleteOnClose, false); setMinimumSize(320, 240); resize(800, 600); setMouseTracking(true); m_closeBtn = new QPushButton(QStringLiteral("✕"), this); - m_closeBtn->setToolTip(QStringLiteral("Закрыть")); + m_closeBtn->setToolTip(QCoreApplication::translate("ChatGui", "Close")); m_closeBtn->setCursor(Qt::ArrowCursor); m_closeBtn->setStyleSheet( "QPushButton { background: rgba(0,0,0,0.5); color: white; border: none; " @@ -56,7 +57,7 @@ ImageViewerWindow::ImageViewerWindow(QWidget *parent) m_closeFade = new QPropertyAnimation(m_closeEffect, "opacity", this); m_closeFade->setEasingCurve(QEasingCurve::InOutQuad); - m_hintLabel = new QLabel(QStringLiteral("Ctrl+колесо — зум"), this); + m_hintLabel = new QLabel(QCoreApplication::translate("ChatGui", "Ctrl+wheel — zoom"), this); m_hintLabel->setAlignment(Qt::AlignCenter); m_hintLabel->setAttribute(Qt::WA_TransparentForMouseEvents); m_hintLabel->setStyleSheet( @@ -126,7 +127,7 @@ void ImageViewerWindow::paintEvent(QPaintEvent*) { if (m_image.isNull()) { p.setPen(QColor(150, 150, 150)); - p.drawText(rect(), Qt::AlignCenter, QStringLiteral("Не удалось загрузить фото")); + p.drawText(rect(), Qt::AlignCenter, QCoreApplication::translate("ChatGui", "Failed to load photo")); return; } diff --git a/tools/chatgui/src/inputbar.cpp b/tools/chatgui/src/inputbar.cpp index 872c80a8..db918a6b 100644 --- a/tools/chatgui/src/inputbar.cpp +++ b/tools/chatgui/src/inputbar.cpp @@ -1,3 +1,4 @@ +#include #include "inputbar.h" #include "emojipanel.h" #include "audiorecorder.h" @@ -61,13 +62,13 @@ static QWidget *wrapBottomBtn(QPushButton *btn, int pad) { } QString InputBar::formatFileSize(qint64 bytes) { - if (bytes < 1024) return QString("%1B").arg(bytes); + if (bytes < 1024) return QCoreApplication::translate("ChatGui", "%1B").arg(bytes); double kb = bytes / 1024.0; - if (kb < 1024.0) return QString("%1KB").arg(kb, 0, 'f', kb < 10.0 ? 1 : 0); + if (kb < 1024.0) return QCoreApplication::translate("ChatGui", "%1KB").arg(kb, 0, 'f', kb < 10.0 ? 1 : 0); double mb = kb / 1024.0; - if (mb < 1024.0) return QString("%1MB").arg(mb, 0, 'f', mb < 10.0 ? 1 : 0); + if (mb < 1024.0) return QCoreApplication::translate("ChatGui", "%1MB").arg(mb, 0, 'f', mb < 10.0 ? 1 : 0); double gb = mb / 1024.0; - return QString("%1GB").arg(gb, 0, 'f', 1); + return QCoreApplication::translate("ChatGui", "%1GB").arg(gb, 0, 'f', 1); } InputBar::InputBar(EmojiPanel *emojiPanel, AudioRecorder *recorder, QWidget *parent) @@ -108,7 +109,7 @@ InputBar::InputBar(EmojiPanel *emojiPanel, AudioRecorder *recorder, QWidget *par layout->setSpacing(4); m_attachBtn = makeIconBtn(QIcon(":/icons/attach.png"), - QString::fromUtf8("Прикрепить"), this); + QCoreApplication::translate("ChatGui", "Attach"), this); connect(m_attachBtn, &QPushButton::clicked, this, &InputBar::onAttachClicked); layout->addWidget(wrapBottomBtn(m_attachBtn, 3), 0, Qt::AlignBottom); @@ -126,15 +127,15 @@ InputBar::InputBar(EmojiPanel *emojiPanel, AudioRecorder *recorder, QWidget *par layout->addWidget(m_input, 1); m_emojiBtn = makeIconBtn(QIcon(":/icons/emoji.png"), - QString::fromUtf8("Смайлики"), this); + QCoreApplication::translate("ChatGui", "Emoji"), this); layout->addWidget(wrapBottomBtn(m_emojiBtn, 3), 0, Qt::AlignBottom); m_pttBtn = makeIconBtn(QIcon(":/icons/mic.png"), - QString::fromUtf8("Голосовое сообщение (удерживайте)"), this); + QCoreApplication::translate("ChatGui", "Voice message (hold)"), this); layout->addWidget(wrapBottomBtn(m_pttBtn, 3), 0, Qt::AlignBottom); auto *sendBtn = makeIconBtn(QIcon(":/icons/send.png"), - QString::fromUtf8("Отправить"), this); + QCoreApplication::translate("ChatGui", "Send"), this); sendBtn->setStyleSheet(sendBtn->styleSheet() + "QPushButton { border: 1px solid palette(highlight); }"); layout->addWidget(sendBtn, 0, Qt::AlignBottom); @@ -331,7 +332,7 @@ void InputBar::onPttReleased() { void InputBar::onAttachClicked() { if (m_channelIdForRecord.isEmpty()) return; - QString srcPath = QFileDialog::getOpenFileName(this, QString::fromUtf8("Прикрепить файл")); + QString srcPath = QFileDialog::getOpenFileName(this, QCoreApplication::translate("ChatGui", "Attach file")); if (srcPath.isEmpty()) return; QFileInfo fi(srcPath); @@ -360,10 +361,10 @@ void InputBar::onAttachClicked() { bool asVideo = false; if (!asImage && QStringList{"mp4", "mov", "mkv", "avi", "webm", "m4v"}.contains(fi.suffix().toLower())) { QMessageBox box(this); - box.setWindowTitle(QString::fromUtf8("Отправить видео")); - box.setText(QString::fromUtf8("Файл содержит видео. Как отправить?")); - QPushButton *fileBtn = box.addButton(QString::fromUtf8("Как файл"), QMessageBox::AcceptRole); - QPushButton *videoBtn = box.addButton(QString::fromUtf8("Как видео"), QMessageBox::DestructiveRole); + box.setWindowTitle(QCoreApplication::translate("ChatGui", "Send video")); + box.setText(QCoreApplication::translate("ChatGui", "The file contains video. How would you like to send it?")); + QPushButton *fileBtn = box.addButton(QCoreApplication::translate("ChatGui", "As a file"), QMessageBox::AcceptRole); + QPushButton *videoBtn = box.addButton(QCoreApplication::translate("ChatGui", "As a video"), QMessageBox::DestructiveRole); box.exec(); asVideo = (box.clickedButton() == videoBtn); if (!box.clickedButton() || (!asVideo && box.clickedButton() != fileBtn)) { @@ -421,8 +422,8 @@ void InputBar::updatePreparation(const QString& target, const QByteArray& job, i void InputBar::showPreparation() { QString status; - if (!m_preparations.value(m_channelIdForRecord).isEmpty()) status = QString::fromUtf8("Подготовка медиа…"); - else if (m_preparationErrors.contains(m_channelIdForRecord)) status = QString::fromUtf8("Не удалось подготовить вложение"); + if (!m_preparations.value(m_channelIdForRecord).isEmpty()) status = QCoreApplication::translate("ChatGui", "Preparing media…"); + else if (m_preparationErrors.contains(m_channelIdForRecord)) status = QCoreApplication::translate("ChatGui", "Failed to prepare attachment"); m_preparationLabel->setText(status); m_preparationLabel->setVisible(!status.isEmpty()); } @@ -442,7 +443,7 @@ void InputBar::showRecordOverlay() { m_recordOverlay->setGeometry(m_input->geometry().adjusted(4, 4, -4, -4)); m_recordOverlay->show(); m_recordOverlay->raise(); - m_recordLabel->setText(QString::fromUtf8("\xF0\x9F\x94\xB4 Recording 0:00")); + m_recordLabel->setText(QCoreApplication::translate("ChatGui", "🔴 Recording 0:00")); if (m_levelBar) m_levelBar->setValue(0); } } @@ -454,7 +455,7 @@ void InputBar::hideRecordOverlay() { void InputBar::updateRecordingUI(int ms) { if (m_recordLabel) { int secs = ms / 1000; - m_recordLabel->setText(QString::fromUtf8("\xF0\x9F\x94\xB4 Recording %1:%2") + m_recordLabel->setText(QCoreApplication::translate("ChatGui", "🔴 Recording %1:%2") .arg(secs / 60).arg(secs % 60, 2, 10, QChar('0'))); } if (m_levelBar && m_recorder) { diff --git a/tools/chatgui/src/invite_link.cpp b/tools/chatgui/src/invite_link.cpp index 8bd9af6b..8c6cd43e 100644 --- a/tools/chatgui/src/invite_link.cpp +++ b/tools/chatgui/src/invite_link.cpp @@ -1,3 +1,4 @@ +#include #include "invite_link.h" #include "debug_ui.h" #include @@ -61,13 +62,13 @@ QString encodeInviteLink(uint64_t channelId, const QByteArray& pubkey, InviteData decodeInviteLink(const QString& link) { InviteData d; if (!link.startsWith(QLatin1String("utun://"))) { - d.error = QStringLiteral("invalid prefix (expected utun://)"); + d.error = QCoreApplication::translate("ChatGui", "invalid prefix (expected utun://)"); GUI_ERROR("decodeInviteLink: invalid prefix"); return d; } QByteArray blob = QByteArray::fromBase64(link.mid(7).toLatin1()); if (blob.isEmpty()) { - d.error = QStringLiteral("base64 decode failed"); + d.error = QCoreApplication::translate("ChatGui", "base64 decode failed"); GUI_ERROR("decodeInviteLink: base64 decode failed"); return d; } @@ -75,7 +76,7 @@ InviteData decodeInviteLink(const QString& link) { int len = blob.size(); if (len < 11) { - d.error = QStringLiteral("blob too short (%1 bytes)").arg(len); + d.error = QCoreApplication::translate("ChatGui", "blob too short (%1 bytes)").arg(len); GUI_ERROR("decodeInviteLink: blob too short (%d bytes)", len); return d; } @@ -83,7 +84,7 @@ InviteData decodeInviteLink(const QString& link) { uint8_t ver = data[off++]; if (ver != INVITE_LINK_VERSION && ver != 0x02 && ver != 0x01) { - d.error = QStringLiteral("unsupported version 0x%1").arg(ver, 2, 16, QLatin1Char('0')); + d.error = QCoreApplication::translate("ChatGui", "unsupported version 0x%1").arg(ver, 2, 16, QLatin1Char('0')); GUI_ERROR("decodeInviteLink: unsupported version %d", ver); return d; } @@ -91,13 +92,13 @@ InviteData decodeInviteLink(const QString& link) { /* v2/v3: password field before channel_id */ if (ver == 0x02 || ver == INVITE_LINK_VERSION) { if (off >= len) { - d.error = QStringLiteral("truncated at pass_len"); + d.error = QCoreApplication::translate("ChatGui", "truncated at pass_len"); return d; } uint8_t plen = data[off++]; if (plen > 0) { if (off + plen > len) { - d.error = QStringLiteral("truncated at password"); + d.error = QCoreApplication::translate("ChatGui", "truncated at password"); return d; } d.password = QString::fromUtf8(reinterpret_cast(data + off), plen); @@ -107,7 +108,7 @@ InviteData decodeInviteLink(const QString& link) { /* channel_id: uint64 big-endian */ if (off + 8 > len) { - d.error = QStringLiteral("truncated at channel_id"); + d.error = QCoreApplication::translate("ChatGui", "truncated at channel_id"); GUI_ERROR("decodeInviteLink: truncated at channel_id"); return d; } @@ -118,7 +119,7 @@ InviteData decodeInviteLink(const QString& link) { /* join_key: uint64 big-endian */ if (off + 8 > len) { - d.error = QStringLiteral("truncated at join_key"); + d.error = QCoreApplication::translate("ChatGui", "truncated at join_key"); GUI_ERROR("decodeInviteLink: truncated at join_key"); return d; } @@ -129,15 +130,15 @@ InviteData decodeInviteLink(const QString& link) { /* v3: reality-блок (node-global параметры) */ if (ver == INVITE_LINK_VERSION) { - if (off >= len) { d.error = QStringLiteral("truncated at reality"); return d; } + if (off >= len) { d.error = QCoreApplication::translate("ChatGui", "truncated at reality"); return d; } if (data[off++] != 0) { - if (off + 8 + 32 + 3 + 1 > len) { d.error = QStringLiteral("truncated at reality params"); return d; } + if (off + 8 + 32 + 3 + 1 > len) { d.error = QCoreApplication::translate("ChatGui", "truncated at reality params"); return d; } d.reality.shortId = QByteArray(reinterpret_cast(data + off), 8); off += 8; d.reality.serverPubkey = QByteArray(reinterpret_cast(data + off), 32); off += 32; d.reality.version = QByteArray(reinterpret_cast(data + off), 3); off += 3; uint8_t snlen = data[off++]; if (snlen > INVITE_SNAME_MAX - 1) snlen = INVITE_SNAME_MAX - 1; - if (off + snlen > len) { d.error = QStringLiteral("truncated at reality server_name"); return d; } + if (off + snlen > len) { d.error = QCoreApplication::translate("ChatGui", "truncated at reality server_name"); return d; } d.reality.serverName = QString::fromUtf8(reinterpret_cast(data + off), snlen); off += snlen; d.reality.has = true; @@ -149,12 +150,12 @@ InviteData decodeInviteLink(const QString& link) { uint8_t header = data[off++]; int cnt = (header & 0x03) + 1; if (cnt < 1 || cnt > 4) { - d.error = QStringLiteral("invalid addr count %1").arg(cnt); + d.error = QCoreApplication::translate("ChatGui", "invalid addr count %1").arg(cnt); GUI_ERROR("decodeInviteLink: invalid addr count %d", cnt); return d; } if (off + 32 > len) { - d.error = QStringLiteral("truncated at pubkey"); + d.error = QCoreApplication::translate("ChatGui", "truncated at pubkey"); GUI_ERROR("decodeInviteLink: truncated at pubkey"); return d; } @@ -168,7 +169,7 @@ InviteData decodeInviteLink(const QString& link) { bool isV6 = header & (1 << (2 + j)); int ipLen = isV6 ? 16 : 4; if (off + 2 + ipLen + 2 > len) { - d.error = QStringLiteral("truncated at addr %1 of block").arg(j); + d.error = QCoreApplication::translate("ChatGui", "truncated at addr %1 of block").arg(j); GUI_ERROR("decodeInviteLink: truncated at addr %d of block", j); return d; } diff --git a/tools/chatgui/src/inviteby.cpp b/tools/chatgui/src/inviteby.cpp index 56241941..04ec2f78 100644 --- a/tools/chatgui/src/inviteby.cpp +++ b/tools/chatgui/src/inviteby.cpp @@ -1,3 +1,4 @@ +#include #include "inviteby.h" #include "invite_link.h" #include "debug_ui.h" @@ -21,14 +22,14 @@ extern "C" { InviteByLinkDialog::InviteByLinkDialog(const QString& channelId, UtunNode* node, QWidget* parent) : QDialog(parent), m_channelId(channelId), m_node(node) { - setWindowTitle(QString::fromUtf8("Пригласить в канал %1").arg(channelId)); + setWindowTitle(QCoreApplication::translate("ChatGui", "Invite to channel %1").arg(channelId)); setMinimumWidth(420); setModal(true); auto *layout = new QVBoxLayout(this); layout->setSpacing(12); - auto *title = new QLabel(QString::fromUtf8("Вставьте utun:// ссылку приглашаемого узла:"), this); + auto *title = new QLabel(QCoreApplication::translate("ChatGui", "Paste the invited node's utun:// link:"), this); title->setStyleSheet("font-weight: bold; font-size: 12px;"); layout->addWidget(title); @@ -46,7 +47,7 @@ InviteByLinkDialog::InviteByLinkDialog(const QString& channelId, UtunNode* node, auto *btnLayout = new QHBoxLayout(); btnLayout->addStretch(); - m_inviteSendBtn = new QPushButton(QString::fromUtf8("Пригласить"), this); + m_inviteSendBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Invite"), this); m_inviteSendBtn->setStyleSheet( "QPushButton { border: none; border-radius: 4px;" " padding: 5px 14px; font-size: 12px; font-weight: bold;" @@ -83,14 +84,14 @@ void InviteByLinkDialog::onInviteLinkTextChanged() { } if (!link.startsWith(QLatin1String("utun://"))) { m_inviteStatusLabel->setStyleSheet("color: gray; font-size: 11px; padding: 2px 0;"); - m_inviteStatusLabel->setText(QString::fromUtf8("Ожидается utun:// ссылка...")); + m_inviteStatusLabel->setText(QCoreApplication::translate("ChatGui", "Expected a utun:// link...")); return; } InviteData d = decodeInviteLink(link); if (!d.error.isEmpty()) { m_inviteStatusLabel->setStyleSheet("color: gray; font-size: 11px; padding: 2px 0;"); - m_inviteStatusLabel->setText(QString::fromUtf8("Неверная ссылка: ") + d.error); + m_inviteStatusLabel->setText(QCoreApplication::translate("ChatGui", "Invalid link: ") + d.error); return; } @@ -112,9 +113,9 @@ void InviteByLinkDialog::onInviteLinkTextChanged() { QString::fromLatin1(d.addrs[i].address.left(4).toHex())).arg(d.addrs[i].port); } } - if (d.addrs.size() > 3) addrInfo += QString(" +%1 more").arg(d.addrs.size() - 3); + if (d.addrs.size() > 3) addrInfo += QCoreApplication::translate("ChatGui", " +%1 more").arg(d.addrs.size() - 3); - QString diag = QString::fromUtf8("Ссылка: узел 0x%1 ключ %2, %3 адр. (%4)\nПриглашение в канал: %5") + QString diag = QCoreApplication::translate("ChatGui", "Link: node 0x%1 key %2, %3 addresses (%4)\nInvitation to channel: %5") .arg(targetNodeId, 16, 16, QChar('0')) .arg(pkShort) .arg(d.addrs.size()) @@ -123,7 +124,7 @@ void InviteByLinkDialog::onInviteLinkTextChanged() { if (myNodeId != 0 && myNodeId == targetNodeId) { m_inviteStatusLabel->setStyleSheet("color: #E67E22; font-size: 11px; padding: 2px 0;"); - m_inviteStatusLabel->setText(diag + QString::fromUtf8(" ⚠ Это ваша собственная ссылка")); + m_inviteStatusLabel->setText(diag + QCoreApplication::translate("ChatGui", " ⚠ This is your own link")); } else { m_inviteStatusLabel->setStyleSheet("color: gray; font-size: 11px; padding: 2px 0;"); m_inviteStatusLabel->setText(diag); @@ -142,21 +143,21 @@ void InviteByLinkDialog::onSendInvite() { if (m_inviteSending) return; QString link = m_inviteLinkEdit->text().trimmed(); if (link.isEmpty()) { - setInviteStatus(QString::fromUtf8("Вставьте utun:// ссылку узла."), true); + setInviteStatus(QCoreApplication::translate("ChatGui", "Paste the node's utun:// link."), true); return; } InviteData d = decodeInviteLink(link); if (!d.error.isEmpty()) { - setInviteStatus(QString::fromUtf8("Неверная ссылка: ") + d.error, true); + setInviteStatus(QCoreApplication::translate("ChatGui", "Invalid link: ") + d.error, true); return; } if (d.pubkey.size() != 32) { - setInviteStatus(QString::fromUtf8("Pubkey не 32 байта."), true); + setInviteStatus(QCoreApplication::translate("ChatGui", "The public key must be 32 bytes."), true); return; } if (d.addrs.isEmpty()) { - setInviteStatus(QString::fromUtf8("Нет адресов."), true); + setInviteStatus(QCoreApplication::translate("ChatGui", "No addresses."), true); return; } @@ -166,7 +167,7 @@ void InviteByLinkDialog::onSendInvite() { uint64_t myNodeId = m_node ? m_node->nodeIdHex().toULongLong(nullptr, 16) : 0; if (myNodeId != 0 && myNodeId == targetNodeId) { GUI_WARN("invite_by_link: self-invite rejected — link contains own pubkey, node=0x%016llx", (unsigned long long)myNodeId); - setInviteStatus(QString::fromUtf8("Ошибка: это ваша собственная ссылка (нельзя пригласить себя)"), true); + setInviteStatus(QCoreApplication::translate("ChatGui", "Error: this is your own link (you cannot invite yourself)"), true); return; } } @@ -186,27 +187,27 @@ void InviteByLinkDialog::onSendInvite() { (const uint8_t*)d.pubkey.constData(), (const uint8_t*)addrsBuf.constData(), d.addrs.size(), addrsBuf.size()); - setInviteStatus(QString::fromUtf8("Приглашение отправлено, ожидание ответа...")); + setInviteStatus(QCoreApplication::translate("ChatGui", "Invitation sent, waiting for a response...")); } void InviteByLinkDialog::onConnectResult(uint64_t nodeId, uint64_t channelId, int result) { if (!m_inviteSending || nodeId != m_inviteTargetNodeId) return; if (result == 0) { - setInviteStatus(QString::fromUtf8("Узел присоединился к каналу."), false); + setInviteStatus(QCoreApplication::translate("ChatGui", "The node has joined the channel."), false); QTimer::singleShot(400, this, &QDialog::accept); } else { m_inviteSending = false; m_inviteSendBtn->setEnabled(true); m_inviteSendBtn->setFocus(); - const char* errMsg = "Неизвестная ошибка."; + QString errMsg = QCoreApplication::translate("ChatGui", "Unknown error."); switch (result) { - case -1: errMsg = "Канал не найден."; break; - case 2: errMsg = "Таймаут — узел не ответил."; break; - case -7: errMsg = "Узел не инициализирован."; break; + case -1: errMsg = QCoreApplication::translate("ChatGui", "Channel not found."); break; + case 2: errMsg = QCoreApplication::translate("ChatGui", "Timed out — the node did not respond."); break; + case -7: errMsg = QCoreApplication::translate("ChatGui", "Node not initialized."); break; default: break; } - setInviteStatus(QString::fromUtf8("Ошибка: %1").arg(QString::fromUtf8(errMsg)), true); + setInviteStatus(QCoreApplication::translate("ChatGui", "Error: %1").arg(errMsg), true); } } diff --git a/tools/chatgui/src/invitedialog.cpp b/tools/chatgui/src/invitedialog.cpp index 9b5ff533..450b3169 100644 --- a/tools/chatgui/src/invitedialog.cpp +++ b/tools/chatgui/src/invitedialog.cpp @@ -1,3 +1,4 @@ +#include #include "invitedialog.h" #include "qrcode_utils.h" #include "../db/db_manager.h" @@ -26,17 +27,18 @@ ShareInviteDialog::ShareInviteDialog(const QString& channelId, DbManager* db, QW , m_channelId(channelId) , m_db(db) { - setWindowTitle(QString::fromUtf8("Пригласительная ссылка")); + setWindowTitle(QCoreApplication::translate("ChatGui", "Invite link")); setMinimumSize(420, 560); setModal(true); auto *layout = new QVBoxLayout(this); layout->setSpacing(12); - auto* expiry = new QLabel(QStringLiteral("Ссылка действует %1 минут с момента создания.").arg(JOIN_KEY_TTL_SECONDS / 60), this); + auto* expiry = new QLabel(QCoreApplication::translate("ChatGui", + "The link expires %1 minutes after creation.").arg(JOIN_KEY_TTL_SECONDS / 60), this); expiry->setWordWrap(true); layout->addWidget(expiry); - auto *nodeLabel = new QLabel(QString::fromUtf8("Узел входа:"), this); + auto *nodeLabel = new QLabel(QCoreApplication::translate("ChatGui", "Entry node:"), this); nodeLabel->setStyleSheet("font-size: 12px; color: palette(window-text);"); m_nodeCombo = new QComboBox(this); @@ -52,10 +54,10 @@ ShareInviteDialog::ShareInviteDialog(const QString& channelId, DbManager* db, QW m_linkEdit = new QLineEdit(this); m_linkEdit->setReadOnly(true); - m_linkEdit->setPlaceholderText(QString::fromUtf8("Генерация ссылки...")); + m_linkEdit->setPlaceholderText(QCoreApplication::translate("ChatGui", "Generating link...")); m_linkEdit->setStyleSheet("padding: 6px; font-family: monospace;"); - auto *copyBtn = new QPushButton(QString::fromUtf8("Копировать"), this); + auto *copyBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Copy"), this); connect(copyBtn, &QPushButton::clicked, this, [this]() { QClipboard* cb = QApplication::clipboard(); cb->setText(m_linkEdit->text(), QClipboard::Clipboard); @@ -125,7 +127,7 @@ void ShareInviteDialog::requestLink(uint64_t targetNodeId) { req->request_id = m_requestId = ++nextRequestId; req->target_node_id = targetNodeId; m_linkEdit->setText(QString()); - m_linkEdit->setPlaceholderText(QString::fromUtf8("Генерация ссылки...")); + m_linkEdit->setPlaceholderText(QCoreApplication::translate("ChatGui", "Generating link...")); m_qrLabel->clear(); gui_bridge_post_uasync_fn(chat_invite_build_link_trampoline, req); } @@ -151,7 +153,7 @@ void ShareInviteDialog::onCandidates(const char* chId, int chIdLen, const uint8_ if (autoId == 0) { m_linkEdit->setText(QString()); - m_linkEdit->setPlaceholderText(QString::fromUtf8("Не удалось определить узел (см. лог)")); + m_linkEdit->setPlaceholderText(QCoreApplication::translate("ChatGui", "Failed to identify the node (see log)")); return; } @@ -167,7 +169,7 @@ void ShareInviteDialog::onCandidates(const char* chId, int chIdLen, const uint8_ m_nodeCombo->clear(); /* пункт 0 — узел, найденный авто-выбором */ - m_nodeCombo->addItem(nodeLabel(autoId) + QString::fromUtf8(" (auto)"), + m_nodeCombo->addItem(nodeLabel(autoId) + QCoreApplication::translate("ChatGui", " (auto)"), QVariant::fromValue((qulonglong)autoId)); /* остальные узлы (пропуская авто), свой помечаем (self) */ @@ -179,7 +181,7 @@ void ShareInviteDialog::onCandidates(const char* chId, int chIdLen, const uint8_ if (nid == autoId) continue; QString label = nodeLabel(nid); if (m_db && nid == m_db->myNodeId()) - label += QString::fromUtf8(" (self)"); + label += QCoreApplication::translate("ChatGui", " (self)"); m_nodeCombo->addItem(label, QVariant::fromValue((qulonglong)nid)); } @@ -193,7 +195,7 @@ void ShareInviteDialog::onLinkReady(const char* chId, int chIdLen, uint64_t requ if (cid != m_channelId) return; if (!link || link[0] == '\0') { m_linkEdit->setText(QString()); - m_linkEdit->setPlaceholderText(QString::fromUtf8("Не удалось собрать ссылку (см. лог)")); + m_linkEdit->setPlaceholderText(QCoreApplication::translate("ChatGui", "Failed to build the link (see log)")); m_qrLabel->clear(); return; } diff --git a/tools/chatgui/src/joindialog.cpp b/tools/chatgui/src/joindialog.cpp index f058efe0..f4ed3325 100644 --- a/tools/chatgui/src/joindialog.cpp +++ b/tools/chatgui/src/joindialog.cpp @@ -1,3 +1,4 @@ +#include #include "joindialog.h" #include "invite_link.h" #include "debug_ui.h" @@ -23,7 +24,7 @@ JoinDialog::JoinDialog(UtunNode* node, DbManager* db, QWidget* parent, , m_node(node) , m_db(db) { - setWindowTitle("Join Channel"); + setWindowTitle(QCoreApplication::translate("ChatGui", "Join Channel")); setMinimumWidth(420); setModal(true); @@ -31,7 +32,7 @@ JoinDialog::JoinDialog(UtunNode* node, DbManager* db, QWidget* parent, layout->setSpacing(12); layout->setContentsMargins(20, 16, 20, 16); - auto *titleLabel = new QLabel("Paste invite link:", this); + auto *titleLabel = new QLabel(QCoreApplication::translate("ChatGui", "Paste invite link:"), this); titleLabel->setStyleSheet("font-weight: bold; font-size: 13px;"); layout->addWidget(titleLabel); @@ -41,8 +42,9 @@ JoinDialog::JoinDialog(UtunNode* node, DbManager* db, QWidget* parent, "QLineEdit { border: 1px solid palette(mid); border-radius: 6px;" " padding: 8px 12px; font-size: 13px; background: palette(base); }"); layout->addWidget(m_linkEdit); - auto* expiry = new QLabel(QStringLiteral("Приглашение действует 10 минут с момента создания.\n" - "Если срок истёк, попросите отправителя создать новую ссылку."), this); + auto* expiry = new QLabel(QCoreApplication::translate("ChatGui", + "The invitation expires 10 minutes after creation.\nIf it has expired, ask the sender to " + "create a new link."), this); expiry->setWordWrap(true); layout->addWidget(expiry); @@ -57,8 +59,8 @@ JoinDialog::JoinDialog(UtunNode* node, DbManager* db, QWidget* parent, btnLayout->setSpacing(8); btnLayout->addStretch(); - m_cancelBtn = new QPushButton("Cancel", this); - m_cancelBtn->setFixedWidth(90); + m_cancelBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Cancel"), this); + m_cancelBtn->setMinimumWidth(90); m_cancelBtn->setStyleSheet( "QPushButton { border: 1px solid palette(mid); border-radius: 4px;" " padding: 6px 16px; font-size: 13px; background: palette(button); }" @@ -66,8 +68,8 @@ JoinDialog::JoinDialog(UtunNode* node, DbManager* db, QWidget* parent, connect(m_cancelBtn, &QPushButton::clicked, this, &QDialog::reject); btnLayout->addWidget(m_cancelBtn); - m_connectBtn = new QPushButton("Connect", this); - m_connectBtn->setFixedWidth(90); + m_connectBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Connect"), this); + m_connectBtn->setMinimumWidth(90); m_connectBtn->setDefault(true); m_connectBtn->setStyleSheet( "QPushButton { border: none; border-radius: 4px;" @@ -113,7 +115,7 @@ JoinDialog::JoinDialog(UtunNode* node, DbManager* db, QWidget* parent, : QString("?%1").arg(a.proto); addrList.append(QString("%1:%2 (%3)").arg(addrStr).arg(a.port).arg(protoName)); } - setStatus(QString("Node: 0x%1 | %2") + setStatus(QCoreApplication::translate("ChatGui", "Node: 0x%1 %2") .arg(nodeId, 16, 16, QLatin1Char('0')) .arg(addrList.join(", "))); }); @@ -149,22 +151,22 @@ void JoinDialog::onConnectClicked() { if (m_connecting) return; QString link = m_linkEdit->text().trimmed(); if (link.isEmpty()) { - setStatus("Enter an invite link.", true); + setStatus(QCoreApplication::translate("ChatGui", "Enter an invite link."), true); return; } - setStatus("Parsing link..."); + setStatus(QCoreApplication::translate("ChatGui", "Parsing link...")); InviteData d = decodeInviteLink(link); if (!d.error.isEmpty()) { - setStatus("Invalid invite link: " + d.error, true); + setStatus(QCoreApplication::translate("ChatGui", "Invalid invite link: ") + d.error, true); return; } if (d.pubkey.size() != 32) { - setStatus(QString("Invalid invite link: pubkey is %1 bytes (need 32)").arg(d.pubkey.size()), true); + setStatus(QCoreApplication::translate("ChatGui", "Invalid invite link: pubkey is %1 bytes (need 32)").arg(d.pubkey.size()), true); return; } if (d.addrs.isEmpty()) { - setStatus("No peer addresses in the invite link.", true); + setStatus(QCoreApplication::translate("ChatGui", "No peer addresses in the invite link."), true); return; } @@ -190,7 +192,7 @@ void JoinDialog::onConnectClicked() { m_connectBtn->setEnabled(false); m_linkEdit->setReadOnly(true); - setStatus("Connecting to peer..."); + setStatus(QCoreApplication::translate("ChatGui", "Connecting to peer...")); QByteArray addrsBuf = serializeInviteAddrs(d); @@ -206,7 +208,7 @@ void JoinDialog::onConnectResult(uint64_t nodeId, uint64_t channelId, int result if (!m_connecting || nodeId != m_pendingNodeId || channelId != m_pendingChannelId) return; if (result == 0) { - setStatus("Connected!"); + setStatus(QCoreApplication::translate("ChatGui", "Connected!")); QString cid = QString::number(m_pendingChannelId); emit joined(cid); accept(); @@ -216,13 +218,13 @@ void JoinDialog::onConnectResult(uint64_t nodeId, uint64_t channelId, int result m_linkEdit->setReadOnly(false); m_connectBtn->setFocus(); - const char* errMsg = "Unknown error."; + QString errMsg = QCoreApplication::translate("ChatGui", "Unknown error."); switch (result) { - case -1: errMsg = "Channel not found."; break; - case 2: errMsg = "Connection timed out."; break; - case -7: errMsg = "Node not initialized."; break; + case -1: errMsg = QCoreApplication::translate("ChatGui", "Channel not found."); break; + case 2: errMsg = QCoreApplication::translate("ChatGui", "Connection timed out."); break; + case -7: errMsg = QCoreApplication::translate("ChatGui", "Node not initialized."); break; default: break; } - setStatus(QString("Connection failed: %1").arg(errMsg), true); + setStatus(QCoreApplication::translate("ChatGui", "Connection failed: %1").arg(errMsg), true); } } diff --git a/tools/chatgui/src/main.cpp b/tools/chatgui/src/main.cpp index 82287ba6..88e1ad6a 100644 --- a/tools/chatgui/src/main.cpp +++ b/tools/chatgui/src/main.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -6,6 +7,8 @@ #include #include #include +#include +#include "gui_language.h" #include #ifdef _WIN32 #include @@ -75,13 +78,11 @@ int main(int argc, char *argv[]) { app.setDesktopFileName("org.utun.vibechat"); app.setWindowIcon(QIcon(":/icons/app_icon.png")); app.setQuitOnLastWindowClosed(false); + debug_config_init(); - /* single-instance: лог/БД/сокеты конфликтуют при втором запуске */ + /* Блокировка до любых изменений конфига/БД; предупреждение после загрузки языка. */ QLockFile lock(QCoreApplication::applicationDirPath() + "/vibechat.lock"); - if (!lock.tryLock()) { - QMessageBox::warning(nullptr, "vibechat", "vibechat is already running."); - return 1; - } + const bool locked = lock.tryLock(); app.setStyle(QStyleFactory::create("Fusion")); QPalette p; @@ -105,10 +106,31 @@ int main(int argc, char *argv[]) { QString cfgDir = QCoreApplication::applicationDirPath(); QString cfgPath = cfgDir + "/vibechat.cfg"; /* миграция: старый конфиг chatgui.cfg → vibechat.cfg */ - if (!QFile::exists(cfgPath) && QFile::exists(cfgDir + "/chatgui.cfg")) + if (locked && !QFile::exists(cfgPath) && QFile::exists(cfgDir + "/chatgui.cfg")) QFile::rename(cfgDir + "/chatgui.cfg", cfgPath); NodeConfig cfg(cfgPath); + if (cfg.exists() && !cfg.load()) { + qCritical("Cannot open/read config file: %s", qPrintable(cfgPath)); + return 1; + } + + QTranslator guiTranslator, qtTranslator; + const QString language = resolveGuiLanguage(cfg.language()); + if (!guiTranslator.load(":/translations/chatgui_" + language + ".qm") || !app.installTranslator(&guiTranslator)) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "GUI language: failed to load embedded catalog for %s", qPrintable(language)); + return 1; + } + if (language == "ru") { + if (!qtTranslator.load(QStringLiteral(":/translations/qtbase_ru.qm")) || !app.installTranslator(&qtTranslator)) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "GUI language: failed to load embedded Qt Russian catalog"); + return 1; + } + } + if (!locked) { + QMessageBox::warning(nullptr, "vibechat", QCoreApplication::translate("ChatGui", "vibechat is already running.")); + return 1; + } bool needSave = false; if (!cfg.exists()) { @@ -117,10 +139,6 @@ int main(int argc, char *argv[]) { return 1; } } else { - if (!cfg.load()) { - qCritical("Cannot open/read config file: %s", qPrintable(cfgPath)); - return 1; - } if (cfg.hasErrors()) { for (const auto& err : cfg.errors()) fprintf(stderr, "CONFIG ERROR: %s\n", qPrintable(err)); @@ -154,12 +172,13 @@ int main(int argc, char *argv[]) { /* ранняя инициализация лога в GUI-потоке: SoundManager::init() вызывается в * конструкторе MainWindow ДО старта utun-потока, поэтому без этого его диагностика * (ma_engine_init и т.п.) в лог-файл не попадёт. */ - debug_config_init(); debug_enable_function_name(0); debug_enable_file_output(debugFile.toUtf8().constData(), 0); QString lvl = cfg.debugLevel().trimmed(); if (lvl.isEmpty()) lvl = QStringLiteral("info"); debug_apply_global_level(lvl.toUtf8().constData()); + DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "GUI language: preference=%s system=%s selected=%s", + qPrintable(cfg.language()), qPrintable(QLocale::system().uiLanguages().join(',')), qPrintable(language)); DbManager db; MainWindow w(nullptr, &db, cfgPath, dbPath, debugFile, diff --git a/tools/chatgui/src/mainwindow.cpp b/tools/chatgui/src/mainwindow.cpp index 881113ba..f087b4ed 100644 --- a/tools/chatgui/src/mainwindow.cpp +++ b/tools/chatgui/src/mainwindow.cpp @@ -1,3 +1,4 @@ +#include #include "mainwindow.h" #include "channellist.h" #include "messagelist.h" @@ -437,7 +438,7 @@ void MainWindow::setupBridgeCallbacks() { uint64_t from; memcpy(&from, data + 1 + chLen, 8); s_mainWindow->m_trayIcon->showMessage( "vibechat", - QStringLiteral("You are now a channel co-owner.\nReceived admin key for channel %1 from 0x%2") + QCoreApplication::translate("ChatGui", "You are now a channel co-owner.\nReceived admin key for channel %1 from 0x%2") .arg(chId).arg(QString::number(from, 16)), QSystemTrayIcon::Information, 5000); if (s_mainWindow->m_currentChannelId == chId) @@ -604,7 +605,7 @@ void MainWindow::onCallIncoming(const QString& chId, quint64 caller, quint64 cal SoundManager::instance()->playLoop("call_incoming"); if (!isVisible() || isMinimized()) { - m_trayIcon->showMessage("vibechat", QStringLiteral("Incoming call from %1").arg(name), + m_trayIcon->showMessage("vibechat", QCoreApplication::translate("ChatGui", "Incoming call from %1").arg(name), QSystemTrayIcon::Information, 5000); } @@ -687,7 +688,9 @@ void MainWindow::onDmConvUpdated(const QString& convId) { void MainWindow::onInviteToPmRequested(const QString& source, quint64 peer, const QString& name) { if (!m_db || !m_db->isOpen() || !m_node || !m_node->isRunning()) { GUI_ERROR("invite PM: node unavailable"); - QMessageBox::warning(this, QStringLiteral("Приглашение"), QStringLiteral("Ядро не запущено.")); return; + QMessageBox::warning(this, QCoreApplication::translate("ChatGui", + "Invitation"), QCoreApplication::translate("ChatGui", + "The core is not running.")); return; } if (!peer || peer == m_db->myNodeId()) { GUI_ERROR("invite PM: invalid recipient"); return; } DmInviteDialog dialog(m_db, peer, name, source, this); @@ -702,11 +705,15 @@ void MainWindow::onMessageLinkActivated(const QString& link) { const QUrl url(link, QUrl::StrictMode); if (!url.isValid() || url.host().isEmpty() || (url.scheme() != "http" && url.scheme() != "https")) { GUI_ERROR("message link: invalid web address bytes=%d", (int)link.size()); - QMessageBox::warning(this, tr("Ссылка"), tr("Некорректная ссылка.")); return; + QMessageBox::warning(this, QCoreApplication::translate("ChatGui", + "Link"), QCoreApplication::translate("ChatGui", + "Invalid link.")); return; } if (!QDesktopServices::openUrl(url)) { GUI_ERROR("message link: external open failed scheme=%s host=%s", qPrintable(url.scheme()), qPrintable(url.host())); - QMessageBox::warning(this, tr("Ссылка"), tr("Не удалось открыть ссылку. Проверьте наличие браузера.")); return; + QMessageBox::warning(this, QCoreApplication::translate("ChatGui", + "Link"), QCoreApplication::translate("ChatGui", + "Failed to open the link. Check that a browser is installed.")); return; } GUI_INFO("message link: external open requested scheme=%s host=%s", qPrintable(url.scheme()), qPrintable(url.host())); return; @@ -714,11 +721,15 @@ void MainWindow::onMessageLinkActivated(const QString& link) { const auto invite = decodeInviteLink(link); if (!invite.error.isEmpty()) { GUI_ERROR("invite link: invalid error=%s", qPrintable(invite.error)); - QMessageBox::warning(this, QStringLiteral("Приглашение"), QStringLiteral("Некорректная ссылка: ") + invite.error); return; + QMessageBox::warning(this, QCoreApplication::translate("ChatGui", + "Invitation"), QCoreApplication::translate("ChatGui", + "Invalid link: ") + invite.error); return; } if (!m_db || !m_db->isOpen() || !m_node || !m_node->isRunning()) { GUI_ERROR("invite link: node unavailable"); - QMessageBox::warning(this, QStringLiteral("Приглашение"), QStringLiteral("Ядро не запущено.")); return; + QMessageBox::warning(this, QCoreApplication::translate("ChatGui", + "Invitation"), QCoreApplication::translate("ChatGui", + "The core is not running.")); return; } const QString channelId = QString::number(invite.channelId); for (const auto& channel : m_db->getChannels()) { @@ -751,19 +762,19 @@ void MainWindow::setupTray() { m_trayIcon->setToolTip("vibechat"); m_trayMenu = new QMenu(this); - QAction *settingsAction = m_trayMenu->addAction("Settings"); + QAction *settingsAction = m_trayMenu->addAction(QCoreApplication::translate("ChatGui", "Settings")); connect(settingsAction, &QAction::triggered, this, &MainWindow::showSettings); m_trayMenu->addSeparator(); - QAction *showAction = m_trayMenu->addAction("Show"); + QAction *showAction = m_trayMenu->addAction(QCoreApplication::translate("ChatGui", "Show")); connect(showAction, &QAction::triggered, this, [this]() { show(); raise(); activateWindow(); }); - QAction *quitAction = m_trayMenu->addAction("Quit"); + QAction *quitAction = m_trayMenu->addAction(QCoreApplication::translate("ChatGui", "Quit")); connect(quitAction, &QAction::triggered, this, &MainWindow::requestShutdown); m_trayIcon->setContextMenu(m_trayMenu); @@ -778,7 +789,7 @@ void MainWindow::closeEvent(QCloseEvent *event) { DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "MainWindow: closeEvent — hiding to tray"); saveWindowGeometry(); hide(); - m_trayIcon->showMessage("vibechat", "Application minimized to tray.", + m_trayIcon->showMessage("vibechat", QCoreApplication::translate("ChatGui", "Application minimized to tray."), QSystemTrayIcon::Information, 2000); event->ignore(); } @@ -814,7 +825,7 @@ void MainWindow::showShareInviteDialog(const QString& channelId) { void MainWindow::showInviteByLinkDialog(const QString& channelId) { if (!m_node || !m_node->isRunning()) { - QMessageBox::warning(this, "Error", "Node is not running."); + QMessageBox::warning(this, QCoreApplication::translate("ChatGui", "Error"), QCoreApplication::translate("ChatGui", "Node is not running.")); return; } InviteByLinkDialog dlg(channelId, m_node, this); @@ -823,7 +834,7 @@ void MainWindow::showInviteByLinkDialog(const QString& channelId) { void MainWindow::showJoinDialog() { if (!m_node || !m_node->isRunning()) { - QMessageBox::warning(this, "Error", "Node is not running."); + QMessageBox::warning(this, QCoreApplication::translate("ChatGui", "Error"), QCoreApplication::translate("ChatGui", "Node is not running.")); return; } @@ -953,11 +964,13 @@ void MainWindow::reloadChannels() { void MainWindow::onCreateGroupRequested() { if (!m_node || !m_node->isRunning()) { - QMessageBox::warning(this, "Error", "Node is not running."); + QMessageBox::warning(this, QCoreApplication::translate("ChatGui", "Error"), QCoreApplication::translate("ChatGui", "Node is not running.")); return; } if (!gui_bridge_is_uasync_ready()) { - QMessageBox::warning(this, "Error", "uTun thread is not ready yet. Please wait and try again."); + QMessageBox::warning(this, QCoreApplication::translate("ChatGui", + "Error"), QCoreApplication::translate("ChatGui", + "uTun thread is not ready yet. Please wait and try again.")); return; } diff --git a/tools/chatgui/src/memberlistmodel.cpp b/tools/chatgui/src/memberlistmodel.cpp index 89300a19..ce5677b6 100644 --- a/tools/chatgui/src/memberlistmodel.cpp +++ b/tools/chatgui/src/memberlistmodel.cpp @@ -1,3 +1,4 @@ +#include #include "memberlistmodel.h" #include "../transport/gui_bridge.h" #include @@ -47,9 +48,11 @@ QVariant MemberListModel::data(const QModelIndex& index, int role) const { return sub; } if (role == Qt::ToolTipRole) - return QString(it->online ? "online" : "offline") + " | 0x" + + return (it->online ? QCoreApplication::translate("ChatGui", + "online") : QCoreApplication::translate("ChatGui", + "offline")) + " | 0x" + QString("%1").arg(it->nodeId, 16, 16, QChar('0')) + - ((it->flags & CHAT_MEMBER_FLAG_RADIO) ? tr(" | Radio enabled") : QString()); + ((it->flags & CHAT_MEMBER_FLAG_RADIO) ? QCoreApplication::translate("ChatGui", " · Radio enabled") : QString()); if (role == Qt::UserRole + 2) return it->isSelf; if (role == Qt::UserRole + 3) return it->flags; if (role == Qt::UserRole + 4) return (bool)it->storage; diff --git a/tools/chatgui/src/memberpropsdialog.cpp b/tools/chatgui/src/memberpropsdialog.cpp index c5a43a52..67568845 100644 --- a/tools/chatgui/src/memberpropsdialog.cpp +++ b/tools/chatgui/src/memberpropsdialog.cpp @@ -1,3 +1,4 @@ +#include #include "memberpropsdialog.h" #include "../db/db_manager.h" #include "../transport/gui_bridge.h" @@ -25,7 +26,7 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId, , m_flags(flags) , m_transferBtn(nullptr) { - setWindowTitle("Member Properties"); + setWindowTitle(QCoreApplication::translate("ChatGui", "Member Properties")); setMinimumWidth(340); setModal(true); @@ -80,52 +81,52 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId, layout->setContentsMargins(20, 16, 20, 16); auto* title = new QLabel( - QString("Node: 0x%1").arg(nodeId, 16, 16, QChar('0')) + + QCoreApplication::translate("ChatGui", "Node: 0x%1").arg(nodeId, 16, 16, QChar('0')) + (memberName.isEmpty() ? "" : QString(" (%1)").arg(memberName)), this); title->setStyleSheet("font-weight: bold; font-size: 13px;"); layout->addWidget(title); m_nickEdit = new QLineEdit(this); - m_nickEdit->setPlaceholderText("Local nickname..."); + m_nickEdit->setPlaceholderText(QCoreApplication::translate("ChatGui", "Local nickname...")); m_nickEdit->setText(curNick); m_nickEdit->setEnabled(!isOwn); layout->addWidget(m_nickEdit); - m_adminCb = new QCheckBox("Admin", this); + m_adminCb = new QCheckBox(QCoreApplication::translate("ChatGui", "Admin"), this); m_adminCb->setChecked(isAdmin); m_adminCb->setEnabled(haveKey); layout->addWidget(m_adminCb); - m_superCb = new QCheckBox("Supernode", this); + m_superCb = new QCheckBox(QCoreApplication::translate("ChatGui", "Supernode"), this); m_superCb->setChecked(isSuper); m_superCb->setEnabled(haveKey); layout->addWidget(m_superCb); - m_storageCb = new QCheckBox("Storage (priority attachment keeper)", this); + m_storageCb = new QCheckBox(QCoreApplication::translate("ChatGui", "Storage (priority attachment keeper)"), this); m_storageCb->setChecked(curStorage); m_storageCb->setEnabled(haveKey); layout->addWidget(m_storageCb); - m_verifiedCb = new QCheckBox("Verified", this); + m_verifiedCb = new QCheckBox(QCoreApplication::translate("ChatGui", "Verified"), this); m_verifiedCb->setChecked(curVerified); m_verifiedCb->setEnabled(haveKey); layout->addWidget(m_verifiedCb); - m_deletedCb = new QCheckBox("Deleted", this); + m_deletedCb = new QCheckBox(QCoreApplication::translate("ChatGui", "Deleted"), this); m_deletedCb->setChecked(curDeleted); m_deletedCb->setEnabled(haveKey); layout->addWidget(m_deletedCb); if (!haveKey) { - auto* hint = new QLabel("(Admin/Super require channel private key)", this); + auto* hint = new QLabel(QCoreApplication::translate("ChatGui", "(Admin/Super require channel private key)"), this); hint->setStyleSheet("color: gray; font-size: 11px;"); layout->addWidget(hint); } - m_transferBtn = new QPushButton("Transfer admin rights", this); + m_transferBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Transfer admin rights"), this); m_transferBtn->setEnabled(haveKey && !isOwn); - m_transferBtn->setToolTip(QStringLiteral("Send the channel private key to this member (encrypted + signed)")); + m_transferBtn->setToolTip(QCoreApplication::translate("ChatGui", "Send the channel private key to this member (encrypted + signed)")); m_transferBtn->setStyleSheet( "QPushButton { border: 1px solid palette(mid); border-radius: 4px;" " padding: 6px 16px; font-size: 13px; background: palette(button); }" @@ -139,8 +140,8 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId, auto* btnLayout = new QHBoxLayout(); btnLayout->addStretch(); - auto* cancelBtn = new QPushButton("Cancel", this); - cancelBtn->setFixedWidth(90); + auto* cancelBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Cancel"), this); + cancelBtn->setMinimumWidth(90); cancelBtn->setStyleSheet( "QPushButton { border: 1px solid palette(mid); border-radius: 4px;" " padding: 6px 16px; font-size: 13px; background: palette(button); }" @@ -148,8 +149,8 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId, connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject); btnLayout->addWidget(cancelBtn); - auto* saveBtn = new QPushButton("Save", this); - saveBtn->setFixedWidth(90); + auto* saveBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Save"), this); + saveBtn->setMinimumWidth(90); saveBtn->setDefault(true); saveBtn->setStyleSheet( "QPushButton { border: none; border-radius: 4px;" @@ -214,10 +215,10 @@ void MemberPropsDialog::onTransferAdmin() { QString name = m_memberName.isEmpty() ? QString("0x%1").arg(m_nodeId, 16, 16, QChar('0')) : m_memberName; QMessageBox::StandardButton answer = QMessageBox::question( this, - QStringLiteral("Transfer admin rights"), - QStringLiteral("Send the channel private key to \"%1\"?\n\n" - "The key is transferred encrypted and signed. The recipient will become a co-owner " - "and will be able to manage members.").arg(name), + QCoreApplication::translate("ChatGui", "Transfer admin rights"), + QCoreApplication::translate("ChatGui", + "Send the channel private key to \"%1\"?\n\nThe key is transferred encrypted and signed. The " + "recipient will become a co-owner and will be able to manage members.").arg(name), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); diff --git a/tools/chatgui/src/messagedelegate.cpp b/tools/chatgui/src/messagedelegate.cpp index 9cd49be5..23dd7b52 100644 --- a/tools/chatgui/src/messagedelegate.cpp +++ b/tools/chatgui/src/messagedelegate.cpp @@ -1,3 +1,4 @@ +#include #include "messagedelegate.h" #include "message_links.h" #include "chatview.h" @@ -334,20 +335,20 @@ static int measureTextHeight(const QFont &font, const QString &text, int width, } static QString formatFileSize(qint64 bytes) { - if (bytes < 1024) return QString("%1B").arg(bytes); + if (bytes < 1024) return QCoreApplication::translate("ChatGui", "%1B").arg(bytes); double kb = bytes / 1024.0; - if (kb < 1024.0) return QString("%1KB").arg(kb, 0, 'f', kb < 10.0 ? 1 : 0); + if (kb < 1024.0) return QCoreApplication::translate("ChatGui", "%1KB").arg(kb, 0, 'f', kb < 10.0 ? 1 : 0); double mb = kb / 1024.0; - if (mb < 1024.0) return QString("%1MB").arg(mb, 0, 'f', mb < 10.0 ? 1 : 0); + if (mb < 1024.0) return QCoreApplication::translate("ChatGui", "%1MB").arg(mb, 0, 'f', mb < 10.0 ? 1 : 0); double gb = mb / 1024.0; - return QString("%1GB").arg(gb, 0, 'f', 1); + return QCoreApplication::translate("ChatGui", "%1GB").arg(gb, 0, 'f', 1); } static QString formatBitrate(qint64 bytes, qint64 durMs) { if (bytes <= 0 || durMs <= 0) return QString(); qint64 kbps = bytes * 8 / durMs; - if (kbps >= 1000) return QString("%1Mbps").arg(kbps / 1000.0, 0, 'f', 1); - return QString("%1kbps").arg(kbps); + if (kbps >= 1000) return QCoreApplication::translate("ChatGui", "%1Mbps").arg(kbps / 1000.0, 0, 'f', 1); + return QCoreApplication::translate("ChatGui", "%1kbps").arg(kbps); } static int calcMaxLineWidth(const QFont &font, const QString &text, int availW) { @@ -1308,7 +1309,9 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio painter->setPen(QColor(0x80, 0x80, 0x80)); qint64 fsz = index.data(MsgFileSizeRole).toLongLong(); QString sizeStr = formatFileSize(fsz); - QString stateStr = (dlState == 2) ? QString::fromUtf8("\xE2\x9C\x85") : (dlState == 1) ? "Downloading..." : "Download"; + QString stateStr = (dlState == 2) ? QString::fromUtf8("\xE2\x9C\x85") : (dlState == 1) ? QCoreApplication::translate("ChatGui", + "Downloading...") : QCoreApplication::translate("ChatGui", + "Download"); QString deliveryState = index.data(MsgDeliveryStateRole).toString(); if (!deliveryState.isEmpty()) stateStr = deliveryState; QString info = sizeStr + " \xC2\xB7 " + stateStr; @@ -1582,7 +1585,7 @@ void MessageDelegate::drawStatusBar(QPainter *p, const Layout &L, QString statusText; if (index.data(MsgEditedRole).toBool()) { - statusText = QString::fromUtf8("изменено "); + statusText = QCoreApplication::translate("ChatGui", "edited "); } statusText += index.data(MsgTimeRole).toString(); p->drawText(QRect(r.left(), r.top(), r.width(), r.height()), diff --git a/tools/chatgui/src/messagelist.cpp b/tools/chatgui/src/messagelist.cpp index d1524c2e..f53c8d3a 100644 --- a/tools/chatgui/src/messagelist.cpp +++ b/tools/chatgui/src/messagelist.cpp @@ -1,3 +1,4 @@ +#include #include "messagelist.h" #include "debug_ui.h" #include "chatview.h" @@ -353,7 +354,7 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent hl->addWidget(m_peerStatus); m_channelTitle = new QLabel(header); m_channelTitle->setStyleSheet("font-weight: bold; font-size: 14px;"); - m_channelTitle->setText(QStringLiteral("Канал")); + m_channelTitle->setText(QCoreApplication::translate("ChatGui", "Channel")); hl->addWidget(m_channelTitle, 1); m_talkingLabel = new QLabel(header); @@ -362,8 +363,8 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent hl->addWidget(m_talkingLabel); m_inviteBtn = new QPushButton(QStringLiteral("+"), header); - m_inviteBtn->setAccessibleName(QStringLiteral("Пригласить в группу")); - m_inviteBtn->setToolTip(QStringLiteral("Пригласить в группу")); + m_inviteBtn->setAccessibleName(QCoreApplication::translate("ChatGui", "Invite to group")); + m_inviteBtn->setToolTip(QCoreApplication::translate("ChatGui", "Invite to group")); m_inviteBtn->setFixedSize(34, 30); m_inviteBtn->setCursor(Qt::PointingHandCursor); m_inviteBtn->hide(); @@ -380,7 +381,7 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent }); m_callBtn = new QPushButton(QStringLiteral("☎"), header); - m_callBtn->setAccessibleName(QStringLiteral("Позвонить")); + m_callBtn->setAccessibleName(QCoreApplication::translate("ChatGui", "Call peer")); m_callBtn->setFixedSize(34, 30); m_callBtn->setCursor(Qt::PointingHandCursor); m_callBtn->hide(); @@ -394,11 +395,11 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent m_radioBtn = new QPushButton(header); m_radioBtn->setIcon(makeRadioIcon(header->palette().color(QPalette::ButtonText))); m_radioBtn->setIconSize(QSize(24, 24)); - m_radioBtn->setAccessibleName(QStringLiteral("Рация")); + m_radioBtn->setAccessibleName(QCoreApplication::translate("ChatGui", "Radio")); m_radioBtn->setCheckable(true); m_radioBtn->setFixedSize(34, 30); m_radioBtn->setCursor(Qt::PointingHandCursor); - m_radioBtn->setToolTip(QStringLiteral("Рация: включить/выключить прослушивание канала")); + m_radioBtn->setToolTip(QCoreApplication::translate("ChatGui", "Radio: toggle channel listening")); m_radioBtn->setStyleSheet( "QPushButton { border: 1px solid palette(mid); border-radius: 6px; background: palette(base); font-size: 15px; }" "QPushButton:checked { background: #27ae60; border-color: #27ae60; }"); @@ -1023,17 +1024,17 @@ void MessageList::onMessageContextMenu(const QPoint& pos) { bool downloaded = !filePath.isEmpty() && QFile::exists(filePath); QMenu menu(m_view); - QAction* saveAct = menu.addAction(QString::fromUtf8("Сохранить в файл")); + QAction* saveAct = menu.addAction(QCoreApplication::translate("ChatGui", "Save to file")); saveAct->setEnabled(downloaded); QAction* dlAct = nullptr; - if (!downloaded && !isDm) dlAct = menu.addAction(QString::fromUtf8("Скачать")); + if (!downloaded && !isDm) dlAct = menu.addAction(QCoreApplication::translate("ChatGui", "Download")); QAction* chosen = menu.exec(m_view->viewport()->mapToGlobal(pos)); if (chosen == saveAct) { QString base = displayName; if (base.isEmpty()) base = QFileInfo(filePath).fileName(); - QString dest = QFileDialog::getSaveFileName(this, QString::fromUtf8("Сохранить файл"), base); + QString dest = QFileDialog::getSaveFileName(this, QCoreApplication::translate("ChatGui", "Save file"), base); if (dest.isEmpty()) return; std::thread([filePath, dest]() { if (!QFile::copy(filePath, dest)) @@ -1063,9 +1064,9 @@ void MessageList::onMessageContextMenu(const QPoint& pos) { void MessageList::clearDmCallRoute() { m_callPeer = 0; m_callGroup = 0; m_callBtn->setEnabled(false); - m_callBtn->setToolTip(QStringLiteral("Собеседник недоступен")); + m_callBtn->setToolTip(QCoreApplication::translate("ChatGui", "Peer unavailable")); m_peerStatus->setStyleSheet("background: #E53935; border-radius: 6px;"); - m_peerStatus->setToolTip(QStringLiteral("Не в сети")); + m_peerStatus->setToolTip(QCoreApplication::translate("ChatGui", "Offline")); } void MessageList::refreshDmCallRoute() { @@ -1095,8 +1096,8 @@ void MessageList::onDmCallRoute(const uint8_t* data, int len) { m_callPeer = peer; m_callGroup = group; m_callBtn->setEnabled(peer && group); m_peerStatus->setStyleSheet(group ? "background: #4CAF50; border-radius: 6px;" : "background: #E53935; border-radius: 6px;"); - m_peerStatus->setToolTip(group ? QStringLiteral("В сети") : QStringLiteral("Не в сети")); - m_callBtn->setToolTip(group ? QStringLiteral("Позвонить") : QStringLiteral("Собеседник недоступен")); + m_peerStatus->setToolTip(group ? QCoreApplication::translate("ChatGui", "Online") : QCoreApplication::translate("ChatGui", "Offline")); + m_callBtn->setToolTip(group ? QCoreApplication::translate("ChatGui", "Call peer") : QCoreApplication::translate("ChatGui", "Peer unavailable")); } bool MessageList::isDmChannel(const QString& channelId) { @@ -1155,7 +1156,7 @@ void MessageList::onDmMessages(const uint8_t* data, int len) { const QString inviteLink = invite ? text : QString(); if (invite) { const QString groupName = message.value("tags").toObject().value("group_name").toString(); - text = tr("Приглашение в группу «%1»").arg(groupName) + '\n' + inviteLink; + text = QCoreApplication::translate("ChatGui", "Invitation to group “%1”").arg(groupName) + '\n' + inviteLink; GUI_DEBUG("invite PM: rendered conv=%s seq=%lld group_name_bytes=%d link_bytes=%d", qPrintable(m_currentChannelId), (long long)seq, (int)groupName.toUtf8().size(), (int)inviteLink.size()); } @@ -1163,12 +1164,14 @@ void MessageList::onDmMessages(const uint8_t* data, int len) { QString state = message.value("media_status").toString(); QString status; if (file) { - if (state == "delivered") status = QString::fromUtf8("Доставлено"); - else if (state == "expired") status = QString::fromUtf8("Срок хранения истёк"); - else if (dir == 1) status = QString::fromUtf8("Ожидает доставки"); - else if (state == "ready") status = QString::fromUtf8("Получено"); - else status = QString::fromUtf8("Загрузка…"); - } else if (dir == 1) status = message.value("pending").toBool() ? QString::fromUtf8("Ожидает доставки") : QString::fromUtf8("Доставлено"); + if (state == "delivered") status = QCoreApplication::translate("ChatGui", "Delivered"); + else if (state == "expired") status = QCoreApplication::translate("ChatGui", "Storage period expired"); + else if (dir == 1) status = QCoreApplication::translate("ChatGui", "Awaiting delivery"); + else if (state == "ready") status = QCoreApplication::translate("ChatGui", "Received"); + else status = QCoreApplication::translate("ChatGui", "Downloading…"); + } else if (dir == 1) status = message.value("pending").toBool() ? QCoreApplication::translate("ChatGui", + "Awaiting delivery") : QCoreApplication::translate("ChatGui", + "Delivered"); AccountRow acc = m_db ? m_db->getAccount(author) : AccountRow(); QString name = authorName(author); @@ -1228,7 +1231,7 @@ void MessageList::onRadioTalk(quint64 groupId, quint64 srcNodeId, bool on) { if (on) { QString name = authorName(srcNodeId); if (name.isEmpty()) name = QStringLiteral("0x%1").arg(srcNodeId, 0, 16); - m_talkingLabel->setText(QString::fromUtf8("\xF0\x9F\x8E\x99 %1 говорит").arg(name)); + m_talkingLabel->setText(QCoreApplication::translate("ChatGui", "🎙 %1 is speaking").arg(name)); m_talkingLabel->show(); } else { m_talkingLabel->hide(); diff --git a/tools/chatgui/src/networksettingspage.cpp b/tools/chatgui/src/networksettingspage.cpp index 1cebb6d1..c903e586 100644 --- a/tools/chatgui/src/networksettingspage.cpp +++ b/tools/chatgui/src/networksettingspage.cpp @@ -1,3 +1,4 @@ +#include // networksettingspage.cpp — Network configuration form (список listen-сокетов) #include "networksettingspage.h" #include "../transport/config_updater.h" @@ -13,7 +14,7 @@ #include #include -static const QString AUTO_IP_LABEL = "Auto (0.0.0.0)"; +static QString autoIpLabel() { return QCoreApplication::translate("ChatGui", "Auto (0.0.0.0)"); } NetworkSettingsPage::NetworkSettingsPage(QWidget* parent) : QWidget(parent) @@ -22,11 +23,11 @@ NetworkSettingsPage::NetworkSettingsPage(QWidget* parent) layout->setSpacing(12); layout->setContentsMargins(0, 0, 0, 0); - auto* title = new QLabel("Listen Sockets", this); + auto* title = new QLabel(QCoreApplication::translate("ChatGui", "Listen Sockets"), this); title->setStyleSheet("font-weight: bold; font-size: 13px;"); layout->addWidget(title); - m_autoSocketsCheck = new QCheckBox("Auto sockets (UDP+TCP from interfaces)", this); + m_autoSocketsCheck = new QCheckBox(QCoreApplication::translate("ChatGui", "Auto sockets (UDP+TCP from interfaces)"), this); layout->addWidget(m_autoSocketsCheck); m_serverList = new QListWidget(this); @@ -34,8 +35,8 @@ NetworkSettingsPage::NetworkSettingsPage(QWidget* parent) layout->addWidget(m_serverList); auto* btnRow = new QHBoxLayout(); - m_addBtn = new QPushButton("Add", this); - m_removeBtn = new QPushButton("Remove", this); + m_addBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Add"), this); + m_removeBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Remove"), this); btnRow->addWidget(m_addBtn); btnRow->addWidget(m_removeBtn); btnRow->addStretch(); @@ -44,41 +45,44 @@ NetworkSettingsPage::NetworkSettingsPage(QWidget* parent) // --- форма правки выбранного сокета --- auto* formLayout = new QGridLayout(); - formLayout->addWidget(new QLabel("Name:", this), 0, 0); + formLayout->addWidget(new QLabel(QCoreApplication::translate("ChatGui", "Name:"), this), 0, 0); m_nameEdit = new QLineEdit(this); m_nameEdit->setMinimumWidth(180); formLayout->addWidget(m_nameEdit, 0, 1); - formLayout->addWidget(new QLabel("IP Address:", this), 1, 0); + formLayout->addWidget(new QLabel(QCoreApplication::translate("ChatGui", "IP Address:"), this), 1, 0); m_ipCombo = new QComboBox(this); m_ipCombo->setEditable(true); m_ipCombo->setMinimumWidth(180); formLayout->addWidget(m_ipCombo, 1, 1); - formLayout->addWidget(new QLabel("Port:", this), 2, 0); + formLayout->addWidget(new QLabel(QCoreApplication::translate("ChatGui", "Port:"), this), 2, 0); m_portSpin = new QSpinBox(this); m_portSpin->setRange(1, 65535); m_portSpin->setValue(1333); formLayout->addWidget(m_portSpin, 2, 1); - formLayout->addWidget(new QLabel("Protocol:", this), 3, 0); + formLayout->addWidget(new QLabel(QCoreApplication::translate("ChatGui", "Protocol:"), this), 3, 0); m_protoCombo = new QComboBox(this); m_protoCombo->addItems({"UDP", "TCP"}); formLayout->addWidget(m_protoCombo, 3, 1); - formLayout->addWidget(new QLabel("Type:", this), 4, 0); + formLayout->addWidget(new QLabel(QCoreApplication::translate("ChatGui", "Type:"), this), 4, 0); m_typeCombo = new QComboBox(this); - m_typeCombo->addItems({"public", "nat", "private", "local"}); + m_typeCombo->addItem(QCoreApplication::translate("ChatGui", "Public"), QStringLiteral("public")); + m_typeCombo->addItem(QStringLiteral("NAT"), QStringLiteral("nat")); + m_typeCombo->addItem(QCoreApplication::translate("ChatGui", "Private"), QStringLiteral("private")); + m_typeCombo->addItem(QCoreApplication::translate("ChatGui", "Local"), QStringLiteral("local")); formLayout->addWidget(m_typeCombo, 4, 1); - formLayout->addWidget(new QLabel("Interface:", this), 5, 0); + formLayout->addWidget(new QLabel(QCoreApplication::translate("ChatGui", "Interface:"), this), 5, 0); m_interfaceCombo = new QComboBox(this); m_interfaceCombo->setMinimumWidth(180); formLayout->addWidget(m_interfaceCombo, 5, 1); layout->addLayout(formLayout); - auto* note = new QLabel("Changes take effect after restart.", this); + auto* note = new QLabel(QCoreApplication::translate("ChatGui", "Changes take effect after restart."), this); note->setStyleSheet("color: gray; font-size: 11px;"); layout->addWidget(note); layout->addStretch(); @@ -111,7 +115,7 @@ static void splitAddr(const QString& addr, QString& ip, int& port) { } void NetworkSettingsPage::populateInterfaces() { - m_interfaceCombo->addItem("Auto (any)"); + m_interfaceCombo->addItem(QCoreApplication::translate("ChatGui", "Auto (any)")); for (const QNetworkInterface& iface : QNetworkInterface::allInterfaces()) { if (iface.flags() & QNetworkInterface::IsLoopBack) continue; if (!(iface.flags() & QNetworkInterface::IsUp)) continue; @@ -120,7 +124,7 @@ void NetworkSettingsPage::populateInterfaces() { } void NetworkSettingsPage::populateAddresses() { - m_ipCombo->addItem(AUTO_IP_LABEL); + m_ipCombo->addItem(autoIpLabel()); for (const QNetworkInterface& iface : QNetworkInterface::allInterfaces()) { if (iface.flags() & QNetworkInterface::IsLoopBack) continue; if (!(iface.flags() & QNetworkInterface::IsUp)) continue; @@ -166,11 +170,11 @@ void NetworkSettingsPage::loadFormFromSelected() { m_nameEdit->setText(s.name); QString ip; int port = 0; splitAddr(s.addr, ip, port); - if (ip == "0.0.0.0") ip = AUTO_IP_LABEL; + if (ip == "0.0.0.0") ip = autoIpLabel(); m_ipCombo->setCurrentText(ip); m_portSpin->setValue(port > 0 ? port : 1333); m_protoCombo->setCurrentIndex(s.transport == "tcp" ? 1 : 0); - int typeIdx = m_typeCombo->findText(s.type); + int typeIdx = m_typeCombo->findData(s.type); m_typeCombo->setCurrentIndex(typeIdx >= 0 ? typeIdx : 0); int ifIdx = m_interfaceCombo->findText(s.netif); m_interfaceCombo->setCurrentIndex(ifIdx >= 0 ? ifIdx : 0); @@ -185,13 +189,13 @@ void NetworkSettingsPage::updateSelectedServerFromForm() { s.name = m_nameEdit->text().trimmed(); QString ip = m_ipCombo->currentText().trimmed(); - if (ip == AUTO_IP_LABEL) ip = "0.0.0.0"; + if (ip == autoIpLabel()) ip = "0.0.0.0"; s.addr = ip + ":" + QString::number(m_portSpin->value()); s.transport = (m_protoCombo->currentIndex() == 1) ? "tcp" : "udp"; - s.type = m_typeCombo->currentText().trimmed(); + s.type = m_typeCombo->currentData().toString(); QString iface = m_interfaceCombo->currentText(); - s.netif = iface.startsWith("Auto") ? QString() : iface; + s.netif = m_interfaceCombo->currentIndex() == 0 ? QString() : iface; } void NetworkSettingsPage::updateSelectedListItemText() { diff --git a/tools/chatgui/src/nodespage.cpp b/tools/chatgui/src/nodespage.cpp index 2780c043..32439e5c 100644 --- a/tools/chatgui/src/nodespage.cpp +++ b/tools/chatgui/src/nodespage.cpp @@ -1,3 +1,4 @@ +#include // nodespage.cpp — Nodes table + dump in settings #include "nodespage.h" #include "../db/db_manager.h" @@ -59,12 +60,15 @@ NodesPage::NodesPage(DbManager* db, QWidget* parent) layout->setSpacing(8); layout->setContentsMargins(0, 0, 0, 0); - auto* title = new QLabel("Known Nodes", this); + auto* title = new QLabel(QCoreApplication::translate("ChatGui", "Known Nodes"), this); title->setStyleSheet("font-weight: bold; font-size: 13px;"); layout->addWidget(title); m_table = new QTableWidget(0, 5, this); - m_table->setHorizontalHeaderLabels({"Node ID", "Name", "x25519", "ed25519", "Addrs"}); + m_table->setHorizontalHeaderLabels({QCoreApplication::translate("ChatGui", + "Node ID"), QCoreApplication::translate("ChatGui", + "Name"), "x25519", "ed25519", QCoreApplication::translate("ChatGui", + "Addrs")}); m_table->horizontalHeader()->setStretchLastSection(true); m_table->setSelectionBehavior(QAbstractItemView::SelectRows); m_table->setSelectionMode(QAbstractItemView::SingleSelection); @@ -78,7 +82,7 @@ NodesPage::NodesPage(DbManager* db, QWidget* parent) " font-weight: bold; font-size: 9px; }"); layout->addWidget(m_table, 2); - auto* dumpLabel = new QLabel("Details", this); + auto* dumpLabel = new QLabel(QCoreApplication::translate("ChatGui", "Details"), this); dumpLabel->setStyleSheet("font-weight: bold; font-size: 12px; margin-top: 8px;"); layout->addWidget(dumpLabel); @@ -88,13 +92,13 @@ NodesPage::NodesPage(DbManager* db, QWidget* parent) m_dumpText->setStyleSheet( "QPlainTextEdit { border: 1px solid palette(mid); border-radius: 4px;" " padding: 8px; background: palette(base); }"); - m_dumpText->setPlaceholderText("Click Refresh to load nodes"); + m_dumpText->setPlaceholderText(QCoreApplication::translate("ChatGui", "Click Refresh to load nodes")); layout->addWidget(m_dumpText, 3); auto* btnLayout = new QHBoxLayout(); btnLayout->addStretch(); - m_refreshBtn = new QPushButton("Refresh", this); - m_refreshBtn->setFixedWidth(90); + m_refreshBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Refresh"), this); + m_refreshBtn->setMinimumWidth(90); m_refreshBtn->setStyleSheet( "QPushButton { border: 1px solid palette(mid); border-radius: 4px;" " padding: 6px 16px; font-size: 13px; background: palette(button); }" @@ -109,7 +113,7 @@ void NodesPage::onRefreshClicked() { refreshNodes(); } void NodesPage::refreshNodes() { if (!m_db || !m_db->m_db) { m_table->setRowCount(0); - m_dumpText->setPlainText(QString("Database not connected (m_db=%1, m_db->m_db=%2)") + m_dumpText->setPlainText(QCoreApplication::translate("ChatGui", "Database not connected (m_db=%1, m_db->m_db=%2)") .arg((quintptr)m_db, 0, 16).arg(m_db ? (quintptr)m_db->m_db : 0, 0, 16)); qWarning("NodesPage::refreshNodes: DB not ready m_db=%p m_db->m_db=%p", (void*)m_db, m_db ? (void*)m_db->m_db : nullptr); return; @@ -167,7 +171,7 @@ void NodesPage::refreshNodes() { "SELECT node_id, name, x25519_pubkey, ed25519_pubkey," " last_seen_at, created_at FROM nodes ORDER BY node_id", -1, &st, nullptr); - if (!st) { m_dumpText->setPlainText("Query error"); return; } + if (!st) { m_dumpText->setPlainText(QCoreApplication::translate("ChatGui", "Query error")); return; } int nodeCount = 0; while (sqlite3_step(st) == SQLITE_ROW) { @@ -181,15 +185,17 @@ void NodesPage::refreshNodes() { qint64 last_seen = sqlite3_column_int64(st, 4); qint64 created = sqlite3_column_int64(st, 5); - dump += QString("Node: %1").arg(hex16(nid)); + dump += QCoreApplication::translate("ChatGui", "Node: %1").arg(hex16(nid)); if (!name.isEmpty()) dump += QString(" (%1)").arg(name); dump += QString(" id=0x%1\n").arg(nid, 16, 16, QChar('0')); dump += QString(" x25519: %1\n").arg(x25.isEmpty() ? "-" : shortenKey(x25)); dump += QString(" ed25519: %1\n").arg(ed.isEmpty() ? "-" : shortenKey(ed)); if (last_seen > 0) - dump += QString(" last_seen: %1\n").arg(QDateTime::fromSecsSinceEpoch(last_seen).toString("yyyy-MM-dd hh:mm:ss")); + dump += QCoreApplication::translate("ChatGui", + " last seen: %1\n").arg(QDateTime::fromSecsSinceEpoch(last_seen).toString("yyyy-MM-dd hh:mm:ss")); if (created > 0) - dump += QString(" created: %1\n").arg(QDateTime::fromSecsSinceEpoch(created).toString("yyyy-MM-dd hh:mm:ss")); + dump += QCoreApplication::translate("ChatGui", + " created: %1\n").arg(QDateTime::fromSecsSinceEpoch(created).toString("yyyy-MM-dd hh:mm:ss")); sqlite3_stmt* na = nullptr; sqlite3_prepare_v2(d, @@ -200,7 +206,7 @@ void NodesPage::refreshNodes() { sqlite3_bind_int64(na, 1, (sqlite3_int64)nid); int ac = 0; while (sqlite3_step(na) == SQLITE_ROW) { - if (ac == 0) dump += " addresses:\n"; + if (ac == 0) dump += QCoreApplication::translate("ChatGui", " addresses:\n"); int family = sqlite3_column_int(na, 0); int proto = sqlite3_column_int(na, 1); const void* addr = sqlite3_column_blob(na, 2); @@ -219,14 +225,14 @@ void NodesPage::refreshNodes() { } ac++; } - if (ac == 0) dump += " addresses: none\n"; + if (ac == 0) dump += QCoreApplication::translate("ChatGui", " addresses: none\n"); sqlite3_finalize(na); } else { - dump += " addresses: none\n"; + dump += QCoreApplication::translate("ChatGui", " addresses: none\n"); } } sqlite3_finalize(st); - if (nodeCount == 0) dump = "No nodes found."; + if (nodeCount == 0) dump = QCoreApplication::translate("ChatGui", "No nodes found."); m_dumpText->setPlainText(dump); } diff --git a/tools/chatgui/src/ptt_hotkey_manager.cpp b/tools/chatgui/src/ptt_hotkey_manager.cpp index e960431c..bd52a81b 100644 --- a/tools/chatgui/src/ptt_hotkey_manager.cpp +++ b/tools/chatgui/src/ptt_hotkey_manager.cpp @@ -1,3 +1,4 @@ +#include #include "ptt_hotkey_manager.h" #include "radio_audio_engine.h" #include "../db/db_manager.h" @@ -196,7 +197,7 @@ QString PttHotkeyManager::keyName(int key) const { if (name) return QString::fromLatin1(name); } #endif - return QString("Key %1").arg(key); + return QCoreApplication::translate("ChatGui", "Key %1").arg(key); } QString PttHotkeyManager::describe(const PttKeyChord& chord) const { @@ -204,7 +205,7 @@ QString PttHotkeyManager::describe(const PttKeyChord& chord) const { std::sort(keys.begin(), keys.end()); QStringList names; for (int key : keys) names << keyName(key); - return names.isEmpty() ? QStringLiteral("Не задано") : names.join(" + "); + return names.isEmpty() ? QCoreApplication::translate("ChatGui", "Not set") : names.join(" + "); } bool PttHotkeyManager::usesPortal() { diff --git a/tools/chatgui/src/ptt_portal.cpp b/tools/chatgui/src/ptt_portal.cpp index 23646ebd..a4369173 100644 --- a/tools/chatgui/src/ptt_portal.cpp +++ b/tools/chatgui/src/ptt_portal.cpp @@ -1,3 +1,4 @@ +#include #include "ptt_portal.h" #include "../../lib/debug_config.h" @@ -50,7 +51,7 @@ void PttPortal::setStatus(const QString& text) { m_status = text; emit statusCha void PttPortal::initialize() { if (busy() || m_state == State::Ready || m_state == State::Unbound) return; m_state = State::Checking; - setStatus("Подключение глобальных хоткеев…"); + setStatus(QCoreApplication::translate("ChatGui", "Connecting global shortcuts…")); auto message = QDBusMessage::createMethodCall(kService, kPath, "org.freedesktop.DBus.Properties", "Get"); message.setArguments({kInterface, "version"}); auto* watcher = new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall(message), this); @@ -108,7 +109,7 @@ void PttPortal::response(uint code, const QVariantMap& results, const QDBusMessa DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "PTT portal: response state=%d code=%u", int(state), code); if (code != 0) { fail(QString("request %1: response=%2%3").arg(int(state)).arg(code).arg(code == 1 ? " (cancelled)" : "")); - if (code == 1) setStatus("Назначение отменено. Нажмите кнопку, чтобы повторить."); + if (code == 1) setStatus(QCoreApplication::translate("ChatGui", "Assignment cancelled. Click the button to try again.")); return; } if (state == State::Creating) { @@ -127,11 +128,11 @@ void PttPortal::response(uint code, const QVariantMap& results, const QDBusMessa m_configureAfterBind = known.size() == 3 && m_configureRequested; m_state = State::Unbound; if (!known.isEmpty() || m_configureRequested) bind(); - else setStatus("Назначьте сочетание в системных настройках."); + else setStatus(QCoreApplication::translate("ChatGui", "Assign a shortcut in system settings.")); } else if (state == State::Binding) { m_state = State::Ready; updateDescriptions(qdbus_cast(results.value("shortcuts"))); - setStatus("Глобальные хоткеи подключены."); + setStatus(QCoreApplication::translate("ChatGui", "Global shortcuts connected.")); DEBUG_INFO(DEBUG_CATEGORY_RADIO, "PTT portal: bindings ready session=%s", qPrintable(m_session)); // Первое BindShortcuts само открывает окно назначения, повторное Configure — отдельный вызов. m_configureRequested = false; @@ -143,9 +144,10 @@ bool PttPortal::isPttId(const QString& id) { return id == "ptt1" || id == "ptt2" void PttPortal::bind() { PttPortalShortcuts shortcuts; - for (int i = 1; i <= 3; i++) shortcuts << PttPortalShortcut{QString("ptt%1").arg(i), {{"description", QString("Рация: передача %1").arg(i)}}}; + for (int i = 1; i <= 3; i++) shortcuts << PttPortalShortcut{QString("ptt%1").arg(i), {{"description", QCoreApplication::translate("ChatGui", + "Radio: transmit %1").arg(i)}}}; m_state = State::Binding; - setStatus("Назначение глобальных хоткеев…"); + setStatus(QCoreApplication::translate("ChatGui", "Assigning global shortcuts…")); request("BindShortcuts", {QVariant::fromValue(QDBusObjectPath(m_session)), QVariant::fromValue(shortcuts), m_parentWindow}, {}); } @@ -230,6 +232,6 @@ void PttPortal::fail(const QString& detail) { m_configureRequested = m_configureAfterBind = false; m_state = State::Failed; m_descriptions = {"", "", ""}; emit descriptionsChanged(); - setStatus("Глобальные хоткеи недоступны. Повторите назначение; подробности в логе radio."); + setStatus(QCoreApplication::translate("ChatGui", "Global shortcuts unavailable. Try assigning again; see the radio log for details.")); } void PttPortal::sessionClosed() { fail("portal session closed"); } diff --git a/tools/chatgui/src/radio_audio_engine.cpp b/tools/chatgui/src/radio_audio_engine.cpp index 89e188ae..3a503c7f 100644 --- a/tools/chatgui/src/radio_audio_engine.cpp +++ b/tools/chatgui/src/radio_audio_engine.cpp @@ -1,3 +1,4 @@ +#include #include "radio_audio_engine.h" #include "sound_manager.h" #include "../transport/audio_diagnostics.h" @@ -82,7 +83,7 @@ void RadioAudioEngine::loadSettings(const QString& configPath) { /* Проверяем значения и атомарно сохраняем INI до отправки команды в ядро. */ bool RadioAudioEngine::applySettings(const QMap& values) { if (!m_settingsReady || m_settingsBusy || !gui_bridge_is_uasync_ready() || !gui_bridge_get_inst()) { - m_settingsError = QStringLiteral("Настройки рации ещё не готовы"); + m_settingsError = QCoreApplication::translate("ChatGui", "Radio settings are not ready yet"); DEBUG_WARN(DEBUG_CATEGORY_RADIO, "RadioAudio: cannot apply settings ready=%d busy=%d", m_settingsReady, m_settingsBusy); emit stateChanged(); return false; @@ -92,7 +93,7 @@ bool RadioAudioEngine::applySettings(const QMap& values) { size_t index = 0; while (index < next.size() && it.key() != QLatin1String(settingKeys[index])) ++index; if (index == next.size() || it.value() < settingMin[index] || it.value() > settingMax[index]) { - m_settingsError = QStringLiteral("Некорректная настройка рации: %1").arg(it.key()); + m_settingsError = QCoreApplication::translate("ChatGui", "Invalid radio setting: %1").arg(it.key()); DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "RadioAudio: invalid setting %s=%d", qPrintable(it.key()), it.value()); emit stateChanged(); return false; @@ -103,7 +104,7 @@ bool RadioAudioEngine::applySettings(const QMap& values) { if (next != m_settings && !m_configPath.isEmpty()) { QFile input(m_configPath); if (!input.open(QIODevice::ReadOnly)) { - m_settingsError = QStringLiteral("Не удалось прочитать конфигурацию: %1").arg(input.errorString()); + m_settingsError = QCoreApplication::translate("ChatGui", "Failed to read configuration: %1").arg(input.errorString()); DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "RadioAudio: read config failed: %s", qPrintable(input.errorString())); emit stateChanged(); return false; @@ -116,7 +117,7 @@ bool RadioAudioEngine::applySettings(const QMap& values) { if (!data.endsWith('\n')) data.append('\n'); QSaveFile output(m_configPath); if (!output.open(QIODevice::WriteOnly) || output.write(data) != data.size() || !output.commit()) { - m_settingsError = QStringLiteral("Не удалось сохранить конфигурацию: %1").arg(output.errorString()); + m_settingsError = QCoreApplication::translate("ChatGui", "Failed to save configuration: %1").arg(output.errorString()); DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "RadioAudio: save config failed: %s", qPrintable(output.errorString())); emit stateChanged(); return false; @@ -131,7 +132,7 @@ void RadioAudioEngine::postSettings(const std::array& values, bool apply auto* inst = gui_bridge_get_inst(); void* memory = inst && gui_bridge_is_uasync_ready() ? u_malloc(sizeof(SettingsRequest)) : nullptr; if (!memory) { - m_settingsError = QStringLiteral("Не удалось отправить настройки рации в ядро"); + m_settingsError = QCoreApplication::translate("ChatGui", "Failed to send radio settings to the core"); DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "RadioAudio: settings request unavailable inst=%d uasync=%d", inst != nullptr, gui_bridge_is_uasync_ready()); emit stateChanged(); @@ -164,7 +165,7 @@ void RadioAudioEngine::postSettings(const std::array& values, bool apply engine->m_settings = snapshot; engine->m_settingsReady = true; engine->m_settingsBusy = false; - if (!success) engine->m_settingsError = QStringLiteral("Ядро отклонило настройки рации"); + if (!success) engine->m_settingsError = QCoreApplication::translate("ChatGui", "The core rejected radio settings"); DEBUG_INFO(DEBUG_CATEGORY_RADIO, "RadioAudio: settings applied=%d vad=%d threshold=%d hangover=%dms TX_AGC=%d " "TX_gain=%ddB VAD_gain=%ddB", success, snapshot[0], snapshot[1], snapshot[2], snapshot[3], snapshot[4], snapshot[5]); if ((changed || apply) && engine->m_active) engine->reconfigureCapture(); diff --git a/tools/chatgui/src/radiopanel.cpp b/tools/chatgui/src/radiopanel.cpp index b149bed6..6f7aab12 100644 --- a/tools/chatgui/src/radiopanel.cpp +++ b/tools/chatgui/src/radiopanel.cpp @@ -1,3 +1,4 @@ +#include #include "radiopanel.h" #include "radio_audio_engine.h" #include "sound_manager.h" @@ -24,8 +25,8 @@ RadioPanel::RadioPanel(QWidget* parent) : QWidget(parent) { row->setSpacing(8); m_input = new QToolButton(this); m_output = new QToolButton(this); - m_input->setText(QStringLiteral("Микрофон")); - m_output->setText(QStringLiteral("Выход звука")); + m_input->setText(QCoreApplication::translate("ChatGui", "Microphone")); + m_output->setText(QCoreApplication::translate("ChatGui", "Audio output")); m_input->setObjectName(QStringLiteral("radioInputButton")); m_output->setObjectName(QStringLiteral("radioOutputButton")); for (auto* button : {m_input, m_output}) { @@ -43,15 +44,15 @@ RadioPanel::RadioPanel(QWidget* parent) : QWidget(parent) { row->addWidget(m_status, 1); m_vad = new QPushButton(QStringLiteral("VAD"), this); m_vad->setObjectName(QStringLiteral("radioVadButton")); - m_vad->setAccessibleName(QStringLiteral("Автоматическая передача по голосу")); + m_vad->setAccessibleName(QCoreApplication::translate("ChatGui", "Automatic voice transmission")); m_vad->setCheckable(true); m_vad->setFixedSize(52, 34); m_vad->setStyleSheet("QPushButton:checked { background: #3390EC; color: white; }"); row->addWidget(m_vad); m_ptt = new QPushButton(QStringLiteral("PTT"), this); m_ptt->setObjectName(QStringLiteral("radioPttButton")); - m_ptt->setAccessibleName(QStringLiteral("Держите и говорите")); - m_ptt->setToolTip(QStringLiteral("Держите кнопку или глобальный хоткей для передачи")); + m_ptt->setAccessibleName(QCoreApplication::translate("ChatGui", "Hold to talk")); + m_ptt->setToolTip(QCoreApplication::translate("ChatGui", "Hold the button or a global shortcut to transmit")); m_ptt->setFixedSize(64, 34); m_ptt->setStyleSheet("QPushButton[transmitting=\"true\"] { background: #27ae60; color: white; }"); row->addWidget(m_ptt); @@ -82,16 +83,16 @@ void RadioPanel::updateState() { m_ptt->update(); } QString state; - if (!engine->settingsError().isEmpty()) state = QStringLiteral("Ошибка настройки"); - else if (engine->settingsBusy()) state = QStringLiteral("Применение настроек…"); - else if (!engine->captureAvailable()) state = QStringLiteral("Микрофон недоступен"); - else if (engine->vadEnabled() && !engine->vadAvailable()) state = QStringLiteral("VAD недоступен"); - else if (transmitting) state = QStringLiteral("Передача"); - else state = engine->vadEnabled() ? QStringLiteral("VAD слушает") : QStringLiteral("Приём"); + if (!engine->settingsError().isEmpty()) state = QCoreApplication::translate("ChatGui", "Settings error"); + else if (engine->settingsBusy()) state = QCoreApplication::translate("ChatGui", "Applying settings…"); + else if (!engine->captureAvailable()) state = QCoreApplication::translate("ChatGui", "Microphone unavailable"); + else if (engine->vadEnabled() && !engine->vadAvailable()) state = QCoreApplication::translate("ChatGui", "VAD unavailable"); + else if (transmitting) state = QCoreApplication::translate("ChatGui", "Transmitting"); + else state = engine->vadEnabled() ? QCoreApplication::translate("ChatGui", "VAD listening") : QCoreApplication::translate("ChatGui", "Receiving"); m_status->setText(state); m_status->setToolTip(engine->settingsError().isEmpty() ? state : engine->settingsError()); - m_vad->setToolTip(engine->vadEnabled() ? QStringLiteral("Выключить автоматическую передачу по голосу") - : QStringLiteral("Включить автоматическую передачу по голосу")); + m_vad->setToolTip(engine->vadEnabled() ? QCoreApplication::translate("ChatGui", "Disable automatic voice transmission") + : QCoreApplication::translate("ChatGui", "Enable automatic voice transmission")); } /* Список перечитывается при открытии меню: подключённые устройства видны без перезапуска. */ @@ -107,7 +108,7 @@ void RadioPanel::populateDevices(QToolButton* button, bool capture) { group->addAction(action); connect(action, &QAction::triggered, this, [this, id, capture]() { selectDevice(id, capture); }); }; - add(QStringLiteral("По умолчанию (авто)"), QString()); + add(QCoreApplication::translate("ChatGui", "Default (auto)"), QString()); auto devices = capture ? SoundManager::enumerateCaptureDevices() : SoundManager::enumeratePlaybackDevices(); for (const auto& device : devices) add(device.name, device.id); } @@ -116,7 +117,7 @@ void RadioPanel::selectDevice(const QString& id, bool capture) { auto* inst = gui_bridge_get_inst(); if (!inst || !gui_bridge_is_uasync_ready()) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "RadioPanel: cannot save device: core unavailable"); - m_status->setText(QStringLiteral("Ядро недоступно")); + m_status->setText(QCoreApplication::translate("ChatGui", "Core unavailable")); return; } const char* key = capture ? "audio_input_id" : "audio_output_id"; @@ -124,14 +125,14 @@ void RadioPanel::selectDevice(const QString& id, bool capture) { auto* request = static_cast(u_calloc(1, sizeof(save_ui_state_arg))); if (!request) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "RadioPanel: device settings allocation failed"); - m_status->setText(QStringLiteral("Не удалось выбрать устройство")); + m_status->setText(QCoreApplication::translate("ChatGui", "Failed to select device")); return; } size_t keyLen = strlen(key), valueLen = value.size(); if (keyLen + valueLen + 2 > sizeof(request->data)) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "RadioPanel: device ID too long bytes=%zu", valueLen); u_free(request); - m_status->setText(QStringLiteral("Не удалось выбрать устройство")); + m_status->setText(QCoreApplication::translate("ChatGui", "Failed to select device")); return; } request->inst = inst; @@ -148,9 +149,9 @@ void RadioPanel::updateDevices() { for (auto* button : {m_input, m_output}) { bool capture = button == m_input; QString id = capture ? manager->captureDevice() : manager->playbackDevice(); - QString name = QStringLiteral("По умолчанию (авто)"); + QString name = QCoreApplication::translate("ChatGui", "Default (auto)"); if (!id.isEmpty()) { - name = QStringLiteral("Выбранное устройство недоступно; используется доступное"); + name = QCoreApplication::translate("ChatGui", "Selected device unavailable; using an available device"); auto devices = capture ? manager->enumerateCaptureDevices() : manager->enumeratePlaybackDevices(); for (const auto& device : devices) if (device.id == id) { name = device.name; break; } } diff --git a/tools/chatgui/src/radiosettingspage.cpp b/tools/chatgui/src/radiosettingspage.cpp index ab78a65b..dc23fe97 100644 --- a/tools/chatgui/src/radiosettingspage.cpp +++ b/tools/chatgui/src/radiosettingspage.cpp @@ -1,3 +1,4 @@ +#include #include "radiosettingspage.h" #include "ptt_hotkey_manager.h" #include "radio_audio_engine.h" @@ -17,7 +18,7 @@ RadioSettingsPage::RadioSettingsPage(DbManager* db, QWidget* parent) : QWidget(parent), m_db(db) { auto* layout = new QVBoxLayout(this); - auto* title = new QLabel("Рация (PTT)", this); + auto* title = new QLabel(QCoreApplication::translate("ChatGui", "Radio (PTT)"), this); title->setStyleSheet("font-weight: bold; font-size: 13px;"); layout->addWidget(title); m_audioSettings = new QWidget(this); @@ -30,16 +31,20 @@ RadioSettingsPage::RadioSettingsPage(DbManager* db, QWidget* parent) : QWidget(p form->addRow(label, spin); return spin; }; - m_threshold = add(QStringLiteral("Порог речи VAD:"), 10, 95, QStringLiteral(" %")); - m_threshold->setToolTip(QStringLiteral("Порог вероятности речи: меньше — чувствительнее. Это не порог громкости.")); - m_hangover = add(QStringLiteral("Тишина до окончания передачи:"), 50, 2000, QStringLiteral(" мс")); - m_compressor = new QCheckBox(QStringLiteral("Компрессор передачи и VAD"), m_audioSettings); + m_threshold = add(QCoreApplication::translate("ChatGui", "VAD speech threshold:"), 10, 95, QStringLiteral(" %")); + m_threshold->setToolTip(QCoreApplication::translate("ChatGui", + "Speech probability threshold: lower is more sensitive. This is not a volume threshold.")); + m_hangover = add(QCoreApplication::translate("ChatGui", + "Silence before ending transmission:"), 50, 2000, QCoreApplication::translate("ChatGui", + " ms")); + m_compressor = new QCheckBox(QCoreApplication::translate("ChatGui", "Transmission and VAD compressor"), m_audioSettings); form->addRow(m_compressor); - m_txGain = add(QStringLiteral("Max gain передачи:"), 1, 60, QStringLiteral(" dB")); - m_vadGain = add(QStringLiteral("Max gain VAD:"), 1, 60, QStringLiteral(" dB")); - auto* explanation = new QLabel(QStringLiteral("VAD включается кнопкой на панели рации. Буфер начала речи — 0,6 с. " - "Компрессор VAD имеет отдельный max gain; остальные параметры общие с компрессором передачи. " - "Скорость нарастания усиления задаётся в разделе Audio."), m_audioSettings); + m_txGain = add(QCoreApplication::translate("ChatGui", "Transmission max gain:"), 1, 60, QCoreApplication::translate("ChatGui", " dB")); + m_vadGain = add(QCoreApplication::translate("ChatGui", "VAD max gain:"), 1, 60, QCoreApplication::translate("ChatGui", " dB")); + auto* explanation = new QLabel(QCoreApplication::translate("ChatGui", + "Enable VAD using the button on the radio panel. Speech prebuffer is 0.6 s. The VAD " + "compressor has a separate maximum gain; other parameters are shared with the transmission " + "compressor. Set the gain rise rate in Audio."), m_audioSettings); explanation->setWordWrap(true); form->addRow(explanation); layout->addWidget(m_audioSettings); @@ -50,12 +55,13 @@ RadioSettingsPage::RadioSettingsPage(DbManager* db, QWidget* parent) : QWidget(p }); loadAudioSettings(); if (PttHotkeyManager::usesPortal()) { - auto* hint = new QLabel("Глобальные хоткеи работают из любых приложений. " - "Назначьте обычную клавишу с Ctrl, Shift, Alt или Win в системном окне. " - "Изменения сохраняются системой сразу. Во время настройки передача с клавиатуры отключена.", this); + auto* hint = new QLabel(QCoreApplication::translate("ChatGui", + "Global shortcuts work from any application. Assign a regular key with Ctrl, Shift, Alt or " + "Win in the system dialog. Changes are saved by the system immediately. Keyboard " + "transmission is disabled during setup."), this); hint->setWordWrap(true); layout->addWidget(hint); - auto* button = new QPushButton("Задать PTT хоткеи…", this); + auto* button = new QPushButton(QCoreApplication::translate("ChatGui", "Set PTT shortcuts…"), this); layout->addWidget(button); m_buttons << button; for (int i = 0; i < 3; i++) { @@ -75,23 +81,27 @@ RadioSettingsPage::RadioSettingsPage(DbManager* db, QWidget* parent) : QWidget(p return; } auto* hint = new QLabel( - "Удерживайте сочетание для передачи из любого приложения — даже когда ChatGUI не в фокусе. " - "Рация канала должна быть включена. Для записи зажмите нужные клавиши вместе и отпустите их. " - "Левые и правые Ctrl, Shift, Alt, Win различаются. Fn доступна, только если клавиатура передаёт её ОС.", this); + QCoreApplication::translate("ChatGui", + "Hold a shortcut to transmit from any application, even when ChatGUI is not focused. " + "Channel radio must be enabled. To record a shortcut, hold the desired keys together and " + "release them. Left and right Ctrl, Shift, Alt and Win are distinct. Fn is available only " + "if the keyboard reports it to the OS."), this); hint->setWordWrap(true); hint->setStyleSheet("color: gray; font-size: 11px;"); layout->addWidget(hint); if (!PttHotkeyManager::isSupported()) { - auto* warn = new QLabel("Глобальные хоткеи доступны на Windows и Linux/X11. На этой платформе запись недоступна.", this); + auto* warn = new QLabel(QCoreApplication::translate("ChatGui", + "Global shortcuts are available on Windows and Linux/X11. Shortcut recording is " + "unavailable on this platform."), this); warn->setWordWrap(true); warn->setStyleSheet("color: #e74c3c; font-size: 12px;"); layout->addWidget(warn); } for (int i = 0; i < 3; i++) { auto* row = new QHBoxLayout(); - auto* button = new QPushButton(QString("Задать PTT хоткей %1").arg(i + 1), this); + auto* button = new QPushButton(QCoreApplication::translate("ChatGui", "Set PTT shortcut %1").arg(i + 1), this); button->setEnabled(PttHotkeyManager::isSupported()); - auto* clear = new QPushButton("Очистить", this); + auto* clear = new QPushButton(QCoreApplication::translate("ChatGui", "Clear"), this); row->addWidget(button); row->addWidget(clear); layout->addLayout(row); auto* label = new QLabel(this); @@ -127,13 +137,13 @@ RadioSettingsPage::~RadioSettingsPage() { void RadioSettingsPage::startCapture(int index) { finishCapture(false); if (!PttHotkeyManager::instance()->beginCapture()) { - m_labels[index]->setText("Не удалось начать запись клавиш. Подробности в логе radio."); + m_labels[index]->setText(QCoreApplication::translate("ChatGui", "Failed to start key recording. See the radio log for details.")); return; } m_captureIndex = index; m_capture.candidate.clear(); - m_buttons[index]->setText("Отменить запись"); - m_labels[index]->setText("Нажмите одну или несколько клавиш вместе…"); + m_buttons[index]->setText(QCoreApplication::translate("ChatGui", "Cancel recording")); + m_labels[index]->setText(QCoreApplication::translate("ChatGui", "Press one or more keys together…")); qApp->installEventFilter(this); } @@ -147,7 +157,7 @@ void RadioSettingsPage::finishCapture(bool save) { m_captureIndex = -1; qApp->removeEventFilter(this); PttHotkeyManager::instance()->endCapture(); - m_buttons[index]->setText(QString("Задать PTT хоткей %1").arg(index + 1)); + m_buttons[index]->setText(QCoreApplication::translate("ChatGui", "Set PTT shortcut %1").arg(index + 1)); updateLabel(index); } @@ -223,7 +233,9 @@ void RadioSettingsPage::updatePortalLabels() { const auto descriptions = manager->portalDescriptions(); for (int i = 0; i < m_labels.size(); i++) { QString text = descriptions.value(i); - m_labels[i]->setText(QString("PTT хоткей %1: %2").arg(i + 1).arg(text.isEmpty() ? "Не задано" : text)); + m_labels[i]->setText(QCoreApplication::translate("ChatGui", + "PTT shortcut %1: %2").arg(i + 1).arg(text.isEmpty() ? QCoreApplication::translate("ChatGui", + "Not set") : text)); } m_portalStatus->setText(manager->portalStatus()); m_buttons[0]->setEnabled(!manager->portalBusy()); diff --git a/tools/chatgui/src/renamedialog.cpp b/tools/chatgui/src/renamedialog.cpp index fd5dff94..e0154b5f 100644 --- a/tools/chatgui/src/renamedialog.cpp +++ b/tools/chatgui/src/renamedialog.cpp @@ -1,3 +1,4 @@ +#include #include "renamedialog.h" #include "../db/db_manager.h" #include @@ -13,7 +14,7 @@ RenameDialog::RenameDialog(DbManager* db, const QString& channelId, , m_channelId(channelId) , m_nodeId(nodeId) { - setWindowTitle("Rename"); + setWindowTitle(QCoreApplication::translate("ChatGui", "Rename")); setMinimumWidth(300); setModal(true); @@ -50,8 +51,8 @@ RenameDialog::RenameDialog(DbManager* db, const QString& channelId, auto* btnLayout = new QHBoxLayout(); btnLayout->addStretch(); - auto* cancelBtn = new QPushButton("Cancel", this); - cancelBtn->setFixedWidth(90); + auto* cancelBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Cancel"), this); + cancelBtn->setMinimumWidth(90); cancelBtn->setStyleSheet( "QPushButton { border: 1px solid palette(mid); border-radius: 4px;" " padding: 6px 16px; font-size: 13px; background: palette(button); }" @@ -59,8 +60,8 @@ RenameDialog::RenameDialog(DbManager* db, const QString& channelId, connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject); btnLayout->addWidget(cancelBtn); - auto* saveBtn = new QPushButton("Save", this); - saveBtn->setFixedWidth(90); + auto* saveBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Save"), this); + saveBtn->setMinimumWidth(90); saveBtn->setDefault(true); saveBtn->setStyleSheet( "QPushButton { border: none; border-radius: 4px;" diff --git a/tools/chatgui/src/settingsdialog.cpp b/tools/chatgui/src/settingsdialog.cpp index 51a3e680..58130d64 100644 --- a/tools/chatgui/src/settingsdialog.cpp +++ b/tools/chatgui/src/settingsdialog.cpp @@ -1,3 +1,4 @@ +#include // settingsdialog.cpp — Settings dialog with category list #include "settingsdialog.h" #include "networksettingspage.h" @@ -14,6 +15,9 @@ #include "../../lib/mem.h" #include "../db/db_manager.h" #include "../transport/utun_node.h" +#include "../transport/node_config.h" +#include "../transport/config_updater.h" +#include "../../lib/debug_config.h" extern "C" { #include "chat/chat_core.h" } @@ -33,7 +37,7 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNod : QDialog(parent) , m_configPath(configPath) { - setWindowTitle("Settings"); + setWindowTitle(QCoreApplication::translate("ChatGui", "Settings")); setMinimumSize(760, 460); setWindowModality(Qt::WindowModal); @@ -49,16 +53,16 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNod " background: palette(window); font-size: 13px; }" "QListWidget::item { padding: 10px 14px; }" "QListWidget::item:selected { background: #3390EC; color: white; }"); - m_categoryList->addItem("Profile"); - m_categoryList->addItem("Storage"); - m_categoryList->addItem("Network"); - m_categoryList->addItem("Connections"); - m_categoryList->addItem("Database"); - m_categoryList->addItem("Status"); - m_categoryList->addItem("Nodes"); - m_categoryList->addItem("Sound"); - m_categoryList->addItem("Audio"); - m_categoryList->addItem("Рация"); + m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Profile")); + m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Storage")); + m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Network")); + m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Connections")); + m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Database")); + m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Status")); + m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Nodes")); + m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Sound")); + m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Audio")); + m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Radio")); mainLayout->addWidget(m_categoryList); // --- Right panel: stacked pages --- @@ -73,35 +77,50 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNod auto* profilePage = new QWidget(m_pages); auto* profLayout = new QFormLayout(profilePage); profLayout->setContentsMargins(0, 12, 0, 0); - auto* nameLabel = new QLabel("Display name:", profilePage); + auto* nameLabel = new QLabel(QCoreApplication::translate("ChatGui", "Display name:"), profilePage); m_nicknameEdit = new QLineEdit(profilePage); - m_nicknameEdit->setPlaceholderText("Your name (shown to others)"); + m_nicknameEdit->setPlaceholderText(QCoreApplication::translate("ChatGui", "Your name (shown to others)")); m_nicknameEdit->setMaxLength(32); m_nicknameEdit->setStyleSheet( "QLineEdit { border: 1px solid palette(mid); border-radius: 4px;" " padding: 6px 10px; font-size: 13px; background: palette(base); }"); profLayout->addRow(nameLabel, m_nicknameEdit); - auto* sortLabel = new QLabel("Channel sorting:", profilePage); + m_languageCombo = new QComboBox(profilePage); + m_languageCombo->setObjectName(QStringLiteral("languageCombo")); + m_languageCombo->addItem(QCoreApplication::translate("ChatGui", "System language"), QStringLiteral("system")); + m_languageCombo->addItem(QStringLiteral("English"), QStringLiteral("en")); + m_languageCombo->addItem(QStringLiteral("Русский"), QStringLiteral("ru")); + NodeConfig languageConfig(m_configPath); + if (!languageConfig.load()) { + DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "GUI language: cannot read preferences from %s", qPrintable(m_configPath)); + } + m_languageCombo->setCurrentIndex(m_languageCombo->findData(languageConfig.language())); + profLayout->addRow(QCoreApplication::translate("ChatGui", "Interface language:"), m_languageCombo); + auto* languageNote = new QLabel(QCoreApplication::translate("ChatGui", "Language changes take effect after restart."), profilePage); + languageNote->setWordWrap(true); + profLayout->addRow(languageNote); + + auto* sortLabel = new QLabel(QCoreApplication::translate("ChatGui", "Channel sorting:"), profilePage); m_sortCombo = new QComboBox(profilePage); - m_sortCombo->addItem("Alphabetical (A-Z)"); - m_sortCombo->addItem("Recent Activity"); + m_sortCombo->addItem(QCoreApplication::translate("ChatGui", "Alphabetical (A-Z)")); + m_sortCombo->addItem(QCoreApplication::translate("ChatGui", "Recent Activity")); m_sortCombo->setStyleSheet( "QComboBox { border: 1px solid palette(mid); border-radius: 4px;" " padding: 6px 10px; font-size: 13px; background: palette(base); }"); profLayout->addRow(sortLabel, m_sortCombo); - auto* joinLabel = new QLabel("Join requests:", profilePage); + auto* joinLabel = new QLabel(QCoreApplication::translate("ChatGui", "Join requests:"), profilePage); m_joinPolicyCombo = new QComboBox(profilePage); - m_joinPolicyCombo->addItem("Auto-join"); - m_joinPolicyCombo->addItem("Ask before joining"); - m_joinPolicyCombo->addItem("Deny all"); + m_joinPolicyCombo->addItem(QCoreApplication::translate("ChatGui", "Auto-join")); + m_joinPolicyCombo->addItem(QCoreApplication::translate("ChatGui", "Ask before joining")); + m_joinPolicyCombo->addItem(QCoreApplication::translate("ChatGui", "Deny all")); m_joinPolicyCombo->setStyleSheet( "QComboBox { border: 1px solid palette(mid); border-radius: 4px;" " padding: 6px 10px; font-size: 13px; background: palette(base); }"); profLayout->addRow(joinLabel, m_joinPolicyCombo); - m_autoConnectCheck = new QCheckBox("Auto-connect to group nodes", profilePage); + m_autoConnectCheck = new QCheckBox(QCoreApplication::translate("ChatGui", "Auto-connect to group nodes"), profilePage); m_autoConnectCheck->setStyleSheet("font-size: 13px;"); profLayout->addRow(m_autoConnectCheck); m_pages->addWidget(profilePage); @@ -120,11 +139,11 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNod auto* connPage = new QWidget(m_pages); auto* connLayout = new QVBoxLayout(connPage); connLayout->setAlignment(Qt::AlignCenter); - auto* hintLabel = new QLabel("Live ETCP connection monitor\nwith per-link BBR statistics", connPage); + auto* hintLabel = new QLabel(QCoreApplication::translate("ChatGui", "Live ETCP connection monitor\nwith per-link BBR statistics"), connPage); hintLabel->setAlignment(Qt::AlignCenter); hintLabel->setStyleSheet("font-size: 12px; color: palette(text); padding: 20px;"); connLayout->addWidget(hintLabel); - auto* openBtn = new QPushButton("Open Connection Monitor", connPage); + auto* openBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Open Connection Monitor"), connPage); openBtn->setStyleSheet("QPushButton { padding: 10px 20px; font-size: 13px; }"); connect(openBtn, &QPushButton::clicked, this, &SettingsDialog::onOpenConnMonitor); connLayout->addWidget(openBtn); @@ -160,8 +179,8 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNod btnLayout->setSpacing(8); btnLayout->addStretch(); - auto* cancelBtn = new QPushButton("Cancel", rightPanel); - cancelBtn->setFixedWidth(90); + auto* cancelBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Cancel"), rightPanel); + cancelBtn->setMinimumWidth(90); cancelBtn->setStyleSheet( "QPushButton { border: 1px solid palette(mid); border-radius: 4px;" " padding: 6px 16px; font-size: 13px; background: palette(button); }" @@ -169,8 +188,8 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNod connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject); btnLayout->addWidget(cancelBtn); - m_saveBtn = new QPushButton("Save", rightPanel); - m_saveBtn->setFixedWidth(90); + m_saveBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Save"), rightPanel); + m_saveBtn->setMinimumWidth(90); m_saveBtn->setDefault(true); m_saveBtn->setStyleSheet( "QPushButton { border: none; border-radius: 4px;" @@ -299,8 +318,10 @@ void SettingsDialog::onSave() { m_audioDevicePage->applyAndSave(); m_storagePage->applyAndSave(); if (!m_radioPage->apply()) { - QMessageBox::warning(this, QStringLiteral("Настройки рации"), QStringLiteral("Не удалось применить настройки рации. " - "Проверьте доступность ядра и файла конфигурации; подробности в логе radio.")); + QMessageBox::warning(this, QCoreApplication::translate("ChatGui", + "Radio settings"), QCoreApplication::translate("ChatGui", + "Failed to apply radio settings. Check that the core and configuration file are available; " + "see the radio log for details.")); return; } @@ -357,6 +378,15 @@ void SettingsDialog::onSave() { postChatSetting(m_configPath, "group_autoconnect", v); } } + const QString language = m_languageCombo->currentData().toString(); + if (!config_update_option(m_configPath, "gui", "language", language)) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "GUI language: failed to save preference=%s path=%s", + qPrintable(language), qPrintable(m_configPath)); + QMessageBox::warning(this, QCoreApplication::translate("ChatGui", "Settings"), + QCoreApplication::translate("ChatGui", "Failed to save the interface language. Check the configuration file.")); + return; + } + DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "GUI language: saved preference=%s; applies after restart", qPrintable(language)); accept(); } diff --git a/tools/chatgui/src/settingsdialog.h b/tools/chatgui/src/settingsdialog.h index 5917fc85..aecdf30d 100644 --- a/tools/chatgui/src/settingsdialog.h +++ b/tools/chatgui/src/settingsdialog.h @@ -43,6 +43,7 @@ private: QPushButton* m_saveBtn; QLineEdit* m_nicknameEdit; QComboBox* m_sortCombo; + QComboBox* m_languageCombo; QComboBox* m_joinPolicyCombo; QCheckBox* m_autoConnectCheck; QString m_oldNickname; diff --git a/tools/chatgui/src/soundsettingspage.cpp b/tools/chatgui/src/soundsettingspage.cpp index c9099c49..c6767f3f 100644 --- a/tools/chatgui/src/soundsettingspage.cpp +++ b/tools/chatgui/src/soundsettingspage.cpp @@ -1,3 +1,4 @@ +#include #include "soundsettingspage.h" #include "sound_manager.h" #include "../db/db_manager.h" @@ -22,12 +23,12 @@ SoundSettingsPage::SoundSettingsPage(DbManager* db, QWidget* parent) layout->setContentsMargins(0, 12, 0, 0); layout->setSpacing(14); - m_enabledCheck = new QCheckBox("Enable sounds", this); + m_enabledCheck = new QCheckBox(QCoreApplication::translate("ChatGui", "Enable sounds"), this); m_enabledCheck->setStyleSheet("font-size: 13px;"); layout->addWidget(m_enabledCheck); auto* volLayout = new QHBoxLayout(); - auto* volLabel = new QLabel("Volume:", this); + auto* volLabel = new QLabel(QCoreApplication::translate("ChatGui", "Volume:"), this); volLabel->setStyleSheet("font-size: 13px;"); volLayout->addWidget(volLabel); @@ -51,25 +52,25 @@ SoundSettingsPage::SoundSettingsPage(DbManager* db, QWidget* parent) sep->setStyleSheet("QFrame { color: palette(mid); }"); layout->addWidget(sep); - auto* header = new QLabel("Events", this); + auto* header = new QLabel(QCoreApplication::translate("ChatGui", "Events"), this); header->setStyleSheet("font-size: 13px; font-weight: bold;"); layout->addWidget(header); - m_events[0] = {"msg_incoming", "Incoming message", nullptr, nullptr, nullptr}; - m_events[1] = {"msg_outgoing", "Outgoing message", nullptr, nullptr, nullptr}; - m_events[2] = {"connect", "Peer connected", nullptr, nullptr, nullptr}; - m_events[3] = {"disconnect", "Peer disconnected", nullptr, nullptr, nullptr}; - m_events[4] = {"call_incoming", "Incoming call", nullptr, nullptr, nullptr}; - m_events[5] = {"call_connecting", "Call: connecting", nullptr, nullptr, nullptr}; - m_events[6] = {"call_ringing", "Call: ringback", nullptr, nullptr, nullptr}; + m_events[0] = {"msg_incoming", QT_TRANSLATE_NOOP("ChatGui", "Incoming message"), nullptr, nullptr, nullptr}; + m_events[1] = {"msg_outgoing", QT_TRANSLATE_NOOP("ChatGui", "Outgoing message"), nullptr, nullptr, nullptr}; + m_events[2] = {"connect", QT_TRANSLATE_NOOP("ChatGui", "Peer connected"), nullptr, nullptr, nullptr}; + m_events[3] = {"disconnect", QT_TRANSLATE_NOOP("ChatGui", "Peer disconnected"), nullptr, nullptr, nullptr}; + m_events[4] = {"call_incoming", QT_TRANSLATE_NOOP("ChatGui", "Incoming call"), nullptr, nullptr, nullptr}; + m_events[5] = {"call_connecting", QT_TRANSLATE_NOOP("ChatGui", "Call: connecting"), nullptr, nullptr, nullptr}; + m_events[6] = {"call_ringing", QT_TRANSLATE_NOOP("ChatGui", "Call: ringback"), nullptr, nullptr, nullptr}; for (int i = 0; i < 7; i++) { auto& e = m_events[i]; auto* row = new QHBoxLayout(); row->setSpacing(8); - auto* lbl = new QLabel(e.label, this); - lbl->setFixedWidth(140); + auto* lbl = new QLabel(QCoreApplication::translate("ChatGui", e.label), this); + lbl->setMinimumWidth(140); lbl->setStyleSheet("font-size: 13px;"); row->addWidget(lbl); diff --git a/tools/chatgui/src/statuspage.cpp b/tools/chatgui/src/statuspage.cpp index ba89b77e..e9bf547e 100644 --- a/tools/chatgui/src/statuspage.cpp +++ b/tools/chatgui/src/statuspage.cpp @@ -1,3 +1,4 @@ +#include // statuspage.cpp — Status page (NTP + connections) in settings #include "statuspage.h" #include "../transport/utun_node.h" @@ -50,7 +51,7 @@ StatusPage::StatusPage(UtunNode* node, QWidget* parent) layout->addWidget(m_clockFrame); /* ── System Status section ── */ - auto* title = new QLabel("System Status", this); + auto* title = new QLabel(QCoreApplication::translate("ChatGui", "System Status"), this); title->setStyleSheet("font-weight: bold; font-size: 13px;"); layout->addWidget(title); @@ -60,13 +61,13 @@ StatusPage::StatusPage(UtunNode* node, QWidget* parent) m_textEdit->setStyleSheet( "QPlainTextEdit { border: 1px solid palette(mid); border-radius: 4px;" " padding: 8px; background: palette(base); }"); - m_textEdit->setPlaceholderText("Click Refresh to load status"); + m_textEdit->setPlaceholderText(QCoreApplication::translate("ChatGui", "Click Refresh to load status")); layout->addWidget(m_textEdit, 1); auto* btnLayout = new QHBoxLayout(); btnLayout->addStretch(); - m_refreshBtn = new QPushButton("Refresh", this); - m_refreshBtn->setFixedWidth(90); + m_refreshBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Refresh"), this); + m_refreshBtn->setMinimumWidth(90); m_refreshBtn->setStyleSheet( "QPushButton { border: 1px solid palette(mid); border-radius: 4px;" " padding: 6px 16px; font-size: 13px; background: palette(button); }" @@ -104,7 +105,7 @@ void StatusPage::updateClock() { if (synced) m_clockLabel->setText(QString("%1 [NTP ✓]").arg(t)); else - m_clockLabel->setText(QString("%1 [LOCAL]").arg(t)); + m_clockLabel->setText(QCoreApplication::translate("ChatGui", "%1 [LOCAL]").arg(t)); int msToNext = 1000 - (corrected.time().msec()); m_clockTimer->start(msToNext); @@ -112,7 +113,7 @@ void StatusPage::updateClock() { void StatusPage::refreshStatus() { updateClock(); - m_textEdit->setPlainText("Loading..."); + m_textEdit->setPlainText(QCoreApplication::translate("ChatGui", "Loading...")); gui_bridge_post_uasync_fn(chat_core_collect_status_trampoline, gui_bridge_get_inst()); } diff --git a/tools/chatgui/src/storagesettingspage.cpp b/tools/chatgui/src/storagesettingspage.cpp index 071011fe..b5af2bd9 100644 --- a/tools/chatgui/src/storagesettingspage.cpp +++ b/tools/chatgui/src/storagesettingspage.cpp @@ -1,3 +1,4 @@ +#include #include "storagesettingspage.h" #include "../db/db_manager.h" #include "../transport/gui_bridge.h" @@ -46,7 +47,7 @@ StorageSettingsPage::StorageSettingsPage(DbManager* db, QWidget* parent) layout->setContentsMargins(0, 12, 0, 0); layout->setSpacing(14); - auto* title = new QLabel("Media Storage", this); + auto* title = new QLabel(QCoreApplication::translate("ChatGui", "Media Storage"), this); title->setStyleSheet("font-weight: bold; font-size: 13px;"); layout->addWidget(title); @@ -54,27 +55,27 @@ StorageSettingsPage::StorageSettingsPage(DbManager* db, QWidget* parent) form->setSpacing(10); form->setContentsMargins(0, 8, 0, 0); - m_autoloadCheck = new QCheckBox("Autoload media", this); + m_autoloadCheck = new QCheckBox(QCoreApplication::translate("ChatGui", "Autoload media"), this); m_autoloadCheck->setStyleSheet("font-size: 13px;"); form->addRow(m_autoloadCheck); m_autoloadMaxSize = new QSpinBox(this); m_autoloadMaxSize->setRange(1, 500); - m_autoloadMaxSize->setSuffix(" MB"); + m_autoloadMaxSize->setSuffix(QCoreApplication::translate("ChatGui", " MB")); m_autoloadMaxSize->setFixedWidth(110); m_autoloadMaxSize->setStyleSheet( "QSpinBox { border: 1px solid palette(mid); border-radius: 4px;" " padding: 5px 8px; font-size: 13px; background: palette(base); }"); - form->addRow("Autoload max size:", m_autoloadMaxSize); + form->addRow(QCoreApplication::translate("ChatGui", "Autoload max size:"), m_autoloadMaxSize); m_maxStorageSize = new QSpinBox(this); m_maxStorageSize->setRange(1, 100); - m_maxStorageSize->setSuffix(" GB"); + m_maxStorageSize->setSuffix(QCoreApplication::translate("ChatGui", " GB")); m_maxStorageSize->setFixedWidth(110); m_maxStorageSize->setStyleSheet( "QSpinBox { border: 1px solid palette(mid); border-radius: 4px;" " padding: 5px 8px; font-size: 13px; background: palette(base); }"); - form->addRow("Max storage size:", m_maxStorageSize); + form->addRow(QCoreApplication::translate("ChatGui", "Max storage size:"), m_maxStorageSize); layout->addLayout(form); diff --git a/tools/chatgui/src/videoplayer_engine.cpp b/tools/chatgui/src/videoplayer_engine.cpp index 3ee68865..89e94382 100644 --- a/tools/chatgui/src/videoplayer_engine.cpp +++ b/tools/chatgui/src/videoplayer_engine.cpp @@ -1,3 +1,4 @@ +#include #include "videoplayer_engine.h" #include "sound_manager.h" #include "../../lib/debug_config.h" @@ -74,10 +75,10 @@ void Worker::run() { }; if (avformat_open_input(&fmt, path.toUtf8().constData(), nullptr, nullptr) != 0) { - worker_fail(this, QString("cannot open: %1").arg(path)); return; + worker_fail(this, QCoreApplication::translate("ChatGui", "cannot open: %1").arg(path)); return; } if (avformat_find_stream_info(fmt, nullptr) < 0) { - worker_fail(this, QString("no stream info: %1").arg(path)); cleanup(); return; + worker_fail(this, QCoreApplication::translate("ChatGui", "no stream info: %1").arg(path)); cleanup(); return; } for (unsigned i = 0; i < fmt->nb_streams; i++) { @@ -85,7 +86,7 @@ void Worker::run() { if (p->codec_type == AVMEDIA_TYPE_VIDEO && vidx < 0) vidx = (int)i; else if (p->codec_type == AVMEDIA_TYPE_AUDIO && aidx < 0) aidx = (int)i; } - if (vidx < 0) { worker_fail(this, QString("no video stream: %1").arg(path)); cleanup(); return; } + if (vidx < 0) { worker_fail(this, QCoreApplication::translate("ChatGui", "no video stream: %1").arg(path)); cleanup(); return; } AVStream* vst = fmt->streams[vidx]; int vw = vst->codecpar->width, vh = vst->codecpar->height; @@ -131,7 +132,7 @@ void Worker::run() { if (aidx >= 0) { aframe = av_frame_alloc(); pkt = av_packet_alloc(); - if (!aframe || !pkt) { worker_fail(this, "alloc failed"); cleanup(); return; } + if (!aframe || !pkt) { worker_fail(this, QCoreApplication::translate("ChatGui", "alloc failed")); cleanup(); return; } while (av_read_frame(fmt, pkt) >= 0) { if (abort->loadRelaxed()) { av_packet_unref(pkt); cleanup(); return; } @@ -175,11 +176,11 @@ void Worker::run() { vdec = avcodec_alloc_context3(vc); if (!vc || !vdec || avcodec_parameters_to_context(vdec, vst->codecpar) < 0 || avcodec_open2(vdec, vc, nullptr) < 0) { - worker_fail(this, "video decoder open failed"); cleanup(); return; + worker_fail(this, QCoreApplication::translate("ChatGui", "video decoder open failed")); cleanup(); return; } frame = av_frame_alloc(); pkt = av_packet_alloc(); - if (!frame || !pkt) { worker_fail(this, "alloc failed"); cleanup(); return; } + if (!frame || !pkt) { worker_fail(this, QCoreApplication::translate("ChatGui", "alloc failed")); cleanup(); return; } /* YUVJ* (deprecated full-range) → обычный формат + явный range, чтобы * не ловить warning swscaler и не искажать цвета */ AVPixelFormat srcFmt = vdec->pix_fmt; @@ -192,7 +193,7 @@ void Worker::run() { } sws = sws_getContext(vw, vh, srcFmt, vw, vh, AV_PIX_FMT_RGB24, SWS_BILINEAR, nullptr, nullptr, nullptr); - if (!sws) { worker_fail(this, "sws init failed"); cleanup(); return; } + if (!sws) { worker_fail(this, QCoreApplication::translate("ChatGui", "sws init failed")); cleanup(); return; } int *invTbl = nullptr, *tbl = nullptr, sr = 0, dr = 0, br = 0, ct = 0, st = 0; if (sws_getColorspaceDetails(sws, &invTbl, &sr, &tbl, &dr, &br, &ct, &st) >= 0) sws_setColorspaceDetails(sws, invTbl, srcRange, tbl, 1 /* RGB full-range */, br, ct, st); @@ -252,7 +253,7 @@ void Worker::run() { void Worker::run() { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "VideoEngine: built without FFmpeg"); - emit error(QStringLiteral("видео недоступно (сборка без FFmpeg)")); + emit error(QCoreApplication::translate("ChatGui", "video unavailable (built without FFmpeg)")); } #endif /* HAVE_FFMPEG */ diff --git a/tools/chatgui/src/videoplayer_window.cpp b/tools/chatgui/src/videoplayer_window.cpp index 40fecf01..25e94085 100644 --- a/tools/chatgui/src/videoplayer_window.cpp +++ b/tools/chatgui/src/videoplayer_window.cpp @@ -1,3 +1,4 @@ +#include #include "videoplayer_window.h" #include "videoplayer_engine.h" #include "../../lib/debug_config.h" @@ -76,7 +77,7 @@ public: VideoPlayerWindow::VideoPlayerWindow(QWidget* parent) : QWidget(parent, Qt::Window | Qt::FramelessWindowHint) { - setWindowTitle(QStringLiteral("Video")); + setWindowTitle(QCoreApplication::translate("ChatGui", "Video")); setFocusPolicy(Qt::StrongFocus); setMouseTracking(true); @@ -99,7 +100,7 @@ VideoPlayerWindow::VideoPlayerWindow(QWidget* parent) "QSlider::handle:horizontal { width: 12px; margin: -4px 0; background: white; border-radius: 6px; }"); m_playBtn = new QPushButton(QStringLiteral("⏸"), m_controls); - m_playBtn->setToolTip(QStringLiteral("Пауза")); + m_playBtn->setToolTip(QCoreApplication::translate("ChatGui", "Pause")); m_playBtn->setFixedWidth(40); m_slider = new QSlider(Qt::Horizontal, m_controls); m_slider->setStyle(new SeekSliderStyle()); @@ -115,7 +116,7 @@ VideoPlayerWindow::VideoPlayerWindow(QWidget* parent) /* крестик закрытия — отдельно, справа вверху */ m_closeBtn = new QPushButton(QStringLiteral("✕"), this); - m_closeBtn->setToolTip(QStringLiteral("Закрыть")); + m_closeBtn->setToolTip(QCoreApplication::translate("ChatGui", "Close")); m_closeBtn->setCursor(Qt::ArrowCursor); m_closeBtn->setStyleSheet( "QPushButton { background: rgba(0,0,0,0.5); color: white; border: none; " @@ -193,10 +194,10 @@ void VideoPlayerWindow::onStateChanged(int state) { VideoPlayerEngine::State s = (VideoPlayerEngine::State)state; if (s == VideoPlayerEngine::Playing) { m_playBtn->setText(QStringLiteral("⏸")); - m_playBtn->setToolTip(QStringLiteral("Пауза")); + m_playBtn->setToolTip(QCoreApplication::translate("ChatGui", "Pause")); } else if (s == VideoPlayerEngine::Paused || s == VideoPlayerEngine::Finished) { m_playBtn->setText(QStringLiteral("▶")); - m_playBtn->setToolTip(QStringLiteral("Играть")); + m_playBtn->setToolTip(QCoreApplication::translate("ChatGui", "Play")); showControls(); } } diff --git a/tools/chatgui/tests/test_gui_language.cpp b/tools/chatgui/tests/test_gui_language.cpp new file mode 100644 index 00000000..c0c64a75 --- /dev/null +++ b/tools/chatgui/tests/test_gui_language.cpp @@ -0,0 +1,153 @@ +#include "../src/gui_language.h" +#include "../src/networksettingspage.h" +#include "../transport/node_config.h" +#include "../transport/config_updater.h" +#include "../../../lib/debug_config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class GuiLanguageTest : public QObject { + Q_OBJECT + +private slots: + void initTestCase() { debug_config_init(); } + + void systemPreference() { + QCOMPARE(resolveGuiLanguage("system", {"ru-RU", "en-US"}), QString("ru")); + QCOMPARE(resolveGuiLanguage("system", {"en-GB", "ru-RU"}), QString("en")); + QCOMPARE(resolveGuiLanguage("system", {"de-DE", "ru_RU"}), QString("ru")); + QCOMPARE(resolveGuiLanguage("system", {"fr-FR"}), QString("en")); + QCOMPARE(resolveGuiLanguage("system", {}), QString("en")); + QCOMPARE(resolveGuiLanguage("ru", {"en-US"}), QString("ru")); + QCOMPARE(resolveGuiLanguage("en", {"ru-RU"}), QString("en")); + } + + void preferencePersistence() { + QTemporaryDir directory; + QVERIFY(directory.isValid()); + const QString path = directory.filePath("vibechat.cfg"); + NodeConfig config(path); + QVERIFY(config.create("Language test", 1333)); + QCOMPARE(config.language(), QString("system")); + for (const QString& language : {QString("ru"), QString("en"), QString("system")}) { + QVERIFY(config_update_option(path, "gui", "language", language)); + QVERIFY(config.load()); + QVERIFY2(!config.hasErrors(), qPrintable(config.errors().join('\n'))); + QCOMPARE(config.language(), language); + QVERIFY(config.save()); + NodeConfig reloaded(path); + QVERIFY(reloaded.load()); + QCOMPARE(reloaded.language(), language); + QVERIFY(reloaded.saveFull()); + QVERIFY(reloaded.load()); + QCOMPARE(reloaded.language(), language); + } + QVERIFY(config_update_option(path, "gui", "language", "invalid")); + QVERIFY(config.load()); + QCOMPARE(config.language(), QString("system")); + QVERIFY(config_update_option(path, "gui", "language", "ru")); + QVERIFY(config.load()); + QFile file(path); + QVERIFY(file.open(QIODevice::WriteOnly | QIODevice::Truncate)); + const QByteArray minimalConfig("[gui]\ndb_path=chat_data\n"); + QCOMPARE(file.write(minimalConfig), qint64(minimalConfig.size())); + file.close(); + QVERIFY(config.load()); + QCOMPARE(config.language(), QString("system")); + } + + void embeddedCatalogs() { + for (const QString& language : {QString("en"), QString("ru")}) { + QTranslator translator; + QVERIFY(translator.load(":/translations/chatgui_" + language + ".qm")); + QVERIFY(qApp->installTranslator(&translator)); + QCOMPARE(QCoreApplication::translate("ChatGui", "Settings"), language == "ru" ? QString("Настройки") : QString("Settings")); + QFile file(QStringLiteral(GUI_TS_DIR) + "/chatgui_" + language + ".ts"); + QVERIFY(file.open(QIODevice::ReadOnly)); + QXmlStreamReader xml(&file); + QString source; + int count = 0; + const QRegularExpression placeholders("%(?:L?[1-9][0-9]?|n)"); + auto parameters = [&](const QString& text) { + QStringList list; + auto matches = placeholders.globalMatch(text); + while (matches.hasNext()) list.append(matches.next().captured()); + list.sort(); + return list; + }; + while (!xml.atEnd()) { + xml.readNext(); + if (!xml.isStartElement()) continue; + if (xml.name() == QLatin1String("source")) source = xml.readElementText(); + if (xml.name() != QLatin1String("translation")) continue; + QVERIFY(!xml.attributes().hasAttribute("type")); + const QString translation = xml.readElementText(); + QVERIFY2(!translation.isEmpty(), qPrintable(source)); + QCOMPARE(parameters(translation), parameters(source)); + QCOMPARE(QCoreApplication::translate("ChatGui", source.toUtf8().constData()), translation); + if (language == "en") QCOMPARE(translation, source); + count++; + } + QVERIFY2(!xml.hasError(), qPrintable(xml.errorString())); + QVERIFY(count > 0); + qApp->removeTranslator(&translator); + } + } + + void standardButtons() { + QTranslator translator; + QVERIFY(translator.load(":/translations/qtbase_ru.qm")); + QVERIFY(qApp->installTranslator(&translator)); + QDialogButtonBox buttons(QDialogButtonBox::Cancel | QDialogButtonBox::Save); + QCOMPARE(buttons.button(QDialogButtonBox::Cancel)->text().remove('&'), QString("Отмена")); + QCOMPARE(buttons.button(QDialogButtonBox::Save)->text().remove('&'), QString("Сохранить")); + qApp->removeTranslator(&translator); + } + + void translatedNetworkValues_data() { + QTest::addColumn("language"); + QTest::newRow("English") << QString("en"); + QTest::newRow("Russian") << QString("ru"); + } + + void translatedNetworkValues() { + QFETCH(QString, language); + QTranslator translator; + QVERIFY(translator.load(":/translations/chatgui_" + language + ".qm")); + QVERIFY(qApp->installTranslator(&translator)); + QTemporaryDir directory; + QVERIFY(directory.isValid()); + const QString path = directory.filePath("vibechat.cfg"); + QVERIFY(config_rewrite_servers(path, {{"test", "0.0.0.0:1333", "tcp", "private", ""}})); + NetworkSettingsPage page; + page.loadFromConfig(path); + QComboBox* type = nullptr; + for (auto* combo : page.findChildren()) { + if (combo->findData(QString("public")) >= 0) type = combo; + } + QVERIFY(type); + QCOMPARE(type->currentData().toString(), QString("private")); + QCOMPARE(type->currentText(), language == "ru" ? QString("Частный") : QString("Private")); + type->setCurrentIndex(type->findData(QString("local"))); + page.saveToConfig(path); + QFile file(path); + QVERIFY(file.open(QIODevice::ReadOnly)); + const QByteArray content = file.readAll(); + QVERIFY(content.contains("type=local\n")); + QVERIFY(content.contains("addr=0.0.0.0:1333\n")); + QVERIFY(content.contains("transport=tcp\n")); + QVERIFY(!content.contains("netif=")); + qApp->removeTranslator(&translator); + } +}; + +QTEST_MAIN(GuiLanguageTest) +#include "test_gui_language.moc" diff --git a/tools/chatgui/tests/test_ptt_portal.cpp b/tools/chatgui/tests/test_ptt_portal.cpp index 6aa3cbff..12cae5d7 100644 --- a/tools/chatgui/tests/test_ptt_portal.cpp +++ b/tools/chatgui/tests/test_ptt_portal.cpp @@ -159,7 +159,7 @@ int main(int argc, char** argv) { QTimer::singleShot(10000, &app, &QCoreApplication::quit); portal.initialize(); app.exec(); - return done && !portal.status().contains("недоступны") ? 0 : 1; + return done && !portal.status().contains("unavailable") ? 0 : 1; } PortalTest test; return QTest::qExec(&test, argc, argv); diff --git a/tools/chatgui/tests/test_radio_panel.cpp b/tools/chatgui/tests/test_radio_panel.cpp index f2ddf6af..6b08e503 100644 --- a/tools/chatgui/tests/test_radio_panel.cpp +++ b/tools/chatgui/tests/test_radio_panel.cpp @@ -144,7 +144,7 @@ int main(int argc, char** argv) { detectorAvailable = false; vad->click(); await([&]() { return !engine->settingsBusy(); }); - REQUIRE(vad->isChecked() && engine->captureAvailable() && !engine->vadAvailable() && status->text() == "VAD недоступен"); + REQUIRE(vad->isChecked() && engine->captureAvailable() && !engine->vadAvailable() && status->text() == "VAD unavailable"); QTest::mousePress(ptt, Qt::LeftButton); await([&]() { return engine->isTransmitting(); }); QTest::mouseRelease(ptt, Qt::LeftButton); diff --git a/tools/chatgui/translations/chatgui_en.ts b/tools/chatgui/translations/chatgui_en.ts new file mode 100644 index 00000000..e56badfb --- /dev/null +++ b/tools/chatgui/translations/chatgui_en.ts @@ -0,0 +1,2058 @@ + + + + + ChatGui + + + addresses: + + addresses: + + + + + + addresses: none + + addresses: none + + + + + created: %1 + + created: %1 + + + + + last seen: %1 + + last seen: %1 + + + + + (auto) + (auto) + + + + + + (default) + (default) + + + + (self) + (self) + + + + +%1 more + +%1 more + + + + GB + GB + + + + MB + MB + + + + + + dB + dB + + + + kbps + kbps + + + + ms + ms + + + + s + s + + + + · Radio enabled + · Radio enabled + + + + ⚠ This is your own link + ⚠ This is your own link + + + + %1 [LOCAL] + %1 [LOCAL] + + + + + + + %1 B + %1 B + + + + + + + %1 KB + %1 KB + + + + + + + %1 MB + %1 MB + + + + %1: FAILED (%2 ms) + %1: FAILED (%2 ms) + + + + %1: OK (%2 ms) + %1: OK (%2 ms) + + + + + %1B + %1B + + + + + %1GB + %1GB + + + + + %1KB + %1KB + + + + + %1MB + %1MB + + + + %1Mbps + %1Mbps + + + + %1d + %1d + + + + %1h + %1h + + + + %1kbps + %1kbps + + + + %1m + %1m + + + + %1s + %1s + + + + (Admin/Super require channel private key) + (Admin/Super require channel private key) + + + + + Accept + Accept + + + + Active ETCP Connections (10 fps) + Active ETCP Connections (10 fps) + + + + Add + Add + + + + Addrs + Addrs + + + + Admin + Admin + + + + Alphabetical (A-Z) + Alphabetical (A-Z) + + + + Animations + Animations + + + + Another invitation is still being created. Try again. + Another invitation is still being created. Try again. + + + + Application minimized to tray. + Application minimized to tray. + + + + Applying settings… + Applying settings… + + + + As a file + As a file + + + + As a video + As a video + + + + Ask before joining + Ask before joining + + + + Assign a shortcut in system settings. + Assign a shortcut in system settings. + + + + Assigning global shortcuts… + Assigning global shortcuts… + + + + Assignment cancelled. Click the button to try again. + Assignment cancelled. Click the button to try again. + + + + Attach + Attach + + + + Attach file + Attach file + + + + Audio + Audio + + + + Audio output + Audio output + + + + Auto (0.0.0.0) + Auto (0.0.0.0) + + + + Auto (any) + Auto (any) + + + + Auto sockets (UDP+TCP from interfaces) + Auto sockets (UDP+TCP from interfaces) + + + + Auto-connect to group nodes + Auto-connect to group nodes + + + + Auto-join + Auto-join + + + + Auto-play voice messages on receive + Auto-play voice messages on receive + + + + Autoload max size: + Autoload max size: + + + + Autoload media + Autoload media + + + + Automatic voice transmission + Automatic voice transmission + + + + + Awaiting delivery + Awaiting delivery + + + + Bitrate: + Bitrate: + + + + Buffer: %1 ms · Reserve: %2 ms +Over 10 s: minimum %3 · maximum %4 ms +Speed: %5× · RTT: %6 ms +Buffer drops: %7 · Underruns: %8 + Buffer: %1 ms · Reserve: %2 ms +Over 10 s: minimum %3 · maximum %4 ms +Speed: %5× · RTT: %6 ms +Buffer drops: %7 · Underruns: %8 + + + + Call + Call + + + + + + Call peer + Call peer + + + + Call: connecting + Call: connecting + + + + Call: maximum jitter reserve + Call: maximum jitter reserve + + + + Call: ringback + Call: ringback + + + + Calling… + Calling… + + + + + + + + + + Cancel + Cancel + + + + Cancel recording + Cancel recording + + + + Capture stereo (playback is always stereo) + Capture stereo (playback is always stereo) + + + + Changes take effect after restart. + Changes take effect after restart. + + + + Channel + Channel + + + + Channel Settings + Channel Settings + + + + + Channel not found. + Channel not found. + + + + Channel owner (has private key) + Channel owner (has private key) + + + + Channel sorting: + Channel sorting: + + + + Channels + Channels + + + + GRP + GRP + + + + Checking... + Checking... + + + + Clear + Clear + + + + Click Refresh to load nodes + Click Refresh to load nodes + + + + Click Refresh to load status + Click Refresh to load status + + + + + Close + Close + + + + + Connect + Connect + + + + Connected! + Connected! + + + + Connecting global shortcuts… + Connecting global shortcuts… + + + + Connecting to peer... + Connecting to peer... + + + + Connecting… + Connecting… + + + + + Connection Monitor + Connection Monitor + + + + Connection failed: %1 + Connection failed: %1 + + + + Connection timed out. + Connection timed out. + + + + Connection: %1 + Connection: %1 + + + + Connections + Connections + + + + Copy + Copy + + + + Core unavailable + Core unavailable + + + + Create + Create + + + + + Create group + Create group + + + + Creating and sending invitation… + Creating and sending invitation… + + + + Ctrl+wheel — zoom + Ctrl+wheel — zoom + + + + Database + Database + + + + Database Maintenance + Database Maintenance + + + + + Database not connected + Database not connected + + + + Database not connected (m_db=%1, m_db->m_db=%2) + Database not connected (m_db=%1, m_db->m_db=%2) + + + + + + Database not open + Database not open + + + + + Decline + Decline + + + + + + + + Default (auto) + Default (auto) + + + + Deleted + Deleted + + + + + Delivered + Delivered + + + + Deny all + Deny all + + + + Details + Details + + + + Direct messages + Direct messages + + + + Disable automatic voice transmission + Disable automatic voice transmission + + + + Display name: + Display name: + + + + + Download + Download + + + + Downloading... + Downloading... + + + + Downloading… + Downloading… + + + + Edit + Edit + + + + Emoji + Emoji + + + + Enable VAD using the button on the radio panel. Speech prebuffer is 0.6 s. The VAD compressor has a separate maximum gain; other parameters are shared with the transmission compressor. Set the gain rise rate in Audio. + Enable VAD using the button on the radio panel. Speech prebuffer is 0.6 s. The VAD compressor has a separate maximum gain; other parameters are shared with the transmission compressor. Set the gain rise rate in Audio. + + + + Enable automatic voice transmission + Enable automatic voice transmission + + + + Enable compressor (leveler/normalizer) + Enable compressor (leveler/normalizer) + + + + Enable echo cancellation (AEC) in calls + Enable echo cancellation (AEC) in calls + + + + Enable sounds + Enable sounds + + + + + End call + End call + + + + Ended + Ended + + + + Enter a name... + Enter a name... + + + + Enter an invite link. + Enter an invite link. + + + + Entry node: + Entry node: + + + + + + + Error + Error + + + + Error: %1 + Error: %1 + + + + Error: this is your own link (you cannot invite yourself) + Error: this is your own link (you cannot invite yourself) + + + + Events + Events + + + + Expected a utun:// link... + Expected a utun:// link... + + + + Failed to apply radio settings. Check that the core and configuration file are available; see the radio log for details. + Failed to apply radio settings. Check that the core and configuration file are available; see the radio log for details. + + + + Failed to build the link (see log) + Failed to build the link (see log) + + + + Failed to create a direct conversation. + Failed to create a direct conversation. + + + + Failed to create an invitation. Check the connection and try again. + Failed to create an invitation. Check the connection and try again. + + + + Failed to identify the node (see log) + Failed to identify the node (see log) + + + + Failed to load photo + Failed to load photo + + + + Failed to open the link. Check that a browser is installed. + Failed to open the link. Check that a browser is installed. + + + + Failed to prepare attachment + Failed to prepare attachment + + + + Failed to read configuration: %1 + Failed to read configuration: %1 + + + + Failed to save configuration: %1 + Failed to save configuration: %1 + + + + Failed to save the interface language. Check the configuration file. + Failed to save the interface language. Check the configuration file. + + + + + Failed to select device + Failed to select device + + + + Failed to send radio settings to the core + Failed to send radio settings to the core + + + + Failed to send the invitation. + Failed to send the invitation. + + + + Failed to start key recording. See the radio log for details. + Failed to start key recording. See the radio log for details. + + + + File: + File: + + + + + Full Check + Full Check + + + + + Generating link... + Generating link... + + + + Gestures + Gestures + + + + Global shortcuts are available on Windows and Linux/X11. Shortcut recording is unavailable on this platform. + Global shortcuts are available on Windows and Linux/X11. Shortcut recording is unavailable on this platform. + + + + Global shortcuts connected. + Global shortcuts connected. + + + + Global shortcuts unavailable. Try assigning again; see the radio log for details. + Global shortcuts unavailable. Try assigning again; see the radio log for details. + + + + Global shortcuts work from any application. Assign a regular key with Ctrl, Shift, Alt or Win in the system dialog. Changes are saved by the system immediately. Keyboard transmission is disabled during setup. + Global shortcuts work from any application. Assign a regular key with Ctrl, Shift, Alt or Win in the system dialog. Changes are saved by the system immediately. Keyboard transmission is disabled during setup. + + + + Group name: + Group name: + + + + Hearts & Symbols + Hearts & Symbols + + + + High (64 kbps, best) + High (64 kbps, best) + + + + Hold a shortcut to transmit from any application, even when ChatGUI is not focused. Channel radio must be enabled. To record a shortcut, hold the desired keys together and release them. Left and right Ctrl, Shift, Alt and Win are distinct. Fn is available only if the keyboard reports it to the OS. + Hold a shortcut to transmit from any application, even when ChatGUI is not focused. Channel radio must be enabled. To record a shortcut, hold the desired keys together and release them. Left and right Ctrl, Shift, Alt and Win are distinct. Fn is available only if the keyboard reports it to the OS. + + + + Hold the button or a global shortcut to transmit + Hold the button or a global shortcut to transmit + + + + Hold to talk + Hold to talk + + + + IP Address: + IP Address: + + + + In call + In call + + + + Incoming call + Incoming call + + + + Incoming call from %1 + Incoming call from %1 + + + + Incoming call… + Incoming call… + + + + Incoming message + Incoming message + + + + Interface language: + Interface language: + + + + Interface: + Interface: + + + + Invalid invite link: + Invalid invite link: + + + + Invalid invite link: pubkey is %1 bytes (need 32) + Invalid invite link: pubkey is %1 bytes (need 32) + + + + Invalid link. + Invalid link. + + + + + + Invalid link: + Invalid link: + + + + Invalid radio setting: %1 + Invalid radio setting: %1 + + + + + + Invitation + Invitation + + + + Invitation creation cancelled. + Invitation creation cancelled. + + + + Invitation sent, waiting for a response... + Invitation sent, waiting for a response... + + + + Invitation to group “%1” + Invitation to group “%1” + + + + + Invite + Invite + + + + Invite link + Invite link + + + + Invite to channel %1 + Invite to channel %1 + + + + + + Invite to group + Invite to group + + + + Join + Join + + + + Join Channel + Join Channel + + + + Join requests: + Join requests: + + + + Key %1 + Key %1 + + + + Known Nodes + Known Nodes + + + + Language changes take effect after restart. + Language changes take effect after restart. + + + + + Link + Link + + + + Link: node 0x%1 key %2, %3 addresses (%4) +Invitation to channel: %5 + Link: node 0x%1 key %2, %3 addresses (%4) +Invitation to channel: %5 + + + + Listen Sockets + Listen Sockets + + + + Live ETCP connection monitor +with per-link BBR statistics + Live ETCP connection monitor +with per-link BBR statistics + + + + Loading... + Loading... + + + + Local + Local + + + + Local nickname... + Local nickname... + + + + Low (16 kbps, fast) + Low (16 kbps, fast) + + + + Max gain: + Max gain: + + + + Max storage size: + Max storage size: + + + + Maximum adaptive jitter reserve for calls. Applies to the next call. + Maximum adaptive jitter reserve for calls. Applies to the next call. + + + + Media Storage + Media Storage + + + + Member Properties + Member Properties + + + + + Members + Members + + + + Members 🔑 + Members 🔑 + + + + Microphone + Microphone + + + + Microphone test + Microphone test + + + + Microphone unavailable + Microphone unavailable + + + + Mode: + Mode: + + + + Music (AUDIO) + Music (AUDIO) + + + + + + Mute + Mute + + + + Name + Name + + + + Name: + Name: + + + + Network + Network + + + + No addresses. + No addresses. + + + + No groups available to invite to. + No groups available to invite to. + + + + No nodes found. + No nodes found. + + + + No peer addresses in the invite link. + No peer addresses in the invite link. + + + + Node ID + Node ID + + + + + + Node is not running. + Node is not running. + + + + + Node not initialized. + Node not initialized. + + + + Node: %1 + Node: %1 + + + + Node: 0x%1 + Node: 0x%1 + + + + Node: 0x%1 %2 + Node: 0x%1 %2 + + + + Nodes + Nodes + + + + Not enough memory. + Not enough memory. + + + + + Not set + Not set + + + + + Offline + Offline + + + + Online + Online + + + + Open Connection Monitor + Open Connection Monitor + + + + + Optimize + Optimize + + + + Outgoing message + Outgoing message + + + + PTT shortcut %1: %2 + PTT shortcut %1: %2 + + + + Parsing link... + Parsing link... + + + + Paste invite link: + Paste invite link: + + + + Paste the invited node's utun:// link: + Paste the invited node's utun:// link: + + + + Paste the node's utun:// link. + Paste the node's utun:// link. + + + + + Pause + Pause + + + + Peer connected + Peer connected + + + + Peer disconnected + Peer disconnected + + + + + Peer unavailable + Peer unavailable + + + + Photo + Photo + + + + Play + Play + + + + Play a notification sound when a message arrives in this channel. + Play a notification sound when a message arrives in this channel. + + + + Play sound on incoming message + Play sound on incoming message + + + + Playback device: + Playback device: + + + + Port: + Port: + + + + Preparing media… + Preparing media… + + + + Press one or more keys together… + Press one or more keys together… + + + + Private + Private + + + + Profile + Profile + + + + Protocol: + Protocol: + + + + Public + Public + + + + Push-To-Talk (PTT) mode + Push-To-Talk (PTT) mode + + + + Query error + Query error + + + + + Quick Check + Quick Check + + + + Quit + Quit + + + + + Radio + Radio + + + + Radio (PTT) + Radio (PTT) + + + + Radio (PTT) — Opus + Radio (PTT) — Opus + + + + Radio settings + Radio settings + + + + Radio settings are not ready yet + Radio settings are not ready yet + + + + Radio: toggle channel listening + Radio: toggle channel listening + + + + Radio: transmit %1 + Radio: transmit %1 + + + + Received + Received + + + + Receiving + Receiving + + + + Recent Activity + Recent Activity + + + + Recipient: %1 + Recipient: %1 + + + + Record voice messages with push-to-talk. (Coming soon) + Record voice messages with push-to-talk. (Coming soon) + + + + Recording device: + Recording device: + + + + + Refresh + Refresh + + + + Relay: 0x%1 + Relay: 0x%1 + + + + Remove + Remove + + + + + Rename + Rename + + + + Rise rate (x/sec): + Rise rate (x/sec): + + + + Run a check to see results... + Run a check to see results... + + + + + + + Save + Save + + + + Save file + Save file + + + + Save to file + Save to file + + + + Scans structure without verifying indexes (fast) + Scans structure without verifying indexes (fast) + + + + Scroll to bottom + Scroll to bottom + + + + Select a connection to view metrics + Select a connection to view metrics + + + + Select a group + Select a group + + + + + Select a member + Select a member + + + + Selected device unavailable; using an available device + Selected device unavailable; using an available device + + + + Send + Send + + + + Send message + Send message + + + + Send the channel private key to "%1"? + +The key is transferred encrypted and signed. The recipient will become a co-owner and will be able to manage members. + Send the channel private key to "%1"? + +The key is transferred encrypted and signed. The recipient will become a co-owner and will be able to manage members. + + + + Send the channel private key to this member (encrypted + signed) + Send the channel private key to this member (encrypted + signed) + + + + Send video + Send video + + + + + Set PTT shortcut %1 + Set PTT shortcut %1 + + + + Set PTT shortcuts… + Set PTT shortcuts… + + + + + + + Settings + Settings + + + + Settings error + Settings error + + + + Settings for channel + Settings for channel + + + + Show + Show + + + + Silence before ending transmission: + Silence before ending transmission: + + + + Size: + Size: + + + + Smileys & People + Smileys & People + + + + Sound + Sound + + + + Speaker test + Speaker test + + + + Speech probability threshold: lower is more sensitive. This is not a volume threshold. + Speech probability threshold: lower is more sensitive. This is not a volume threshold. + + + + Standard (32 kbps) + Standard (32 kbps) + + + + Status + Status + + + + Stop Test + Stop Test + + + + Storage + Storage + + + + Storage (priority attachment keeper) + Storage (priority attachment keeper) + + + + Storage period expired + Storage period expired + + + + Supernode + Supernode + + + + System Status + System Status + + + + System language + System language + + + + + Test Microphone + Test Microphone + + + + Test Speaker (440Hz) + Test Speaker (440Hz) + + + + + + The core is not running. + The core is not running. + + + + The core rejected radio settings + The core rejected radio settings + + + + The file contains video. How would you like to send it? + The file contains video. How would you like to send it? + + + + The invitation expires 10 minutes after creation. +If it has expired, ask the sender to create a new link. + The invitation expires 10 minutes after creation. +If it has expired, ask the sender to create a new link. + + + + The link expires %1 minutes after creation. + The link expires %1 minutes after creation. + + + + The link expires %1 minutes after creation. +If the direct message arrives late, a new invitation will be needed. + The link expires %1 minutes after creation. +If the direct message arrives late, a new invitation will be needed. + + + + The node has joined the channel. + The node has joined the channel. + + + + The public key must be 32 bytes. + The public key must be 32 bytes. + + + + Timed out — the node did not respond. + Timed out — the node did not respond. + + + + + Transfer admin rights + Transfer admin rights + + + + Transmission and VAD compressor + Transmission and VAD compressor + + + + Transmission max gain: + Transmission max gain: + + + + Transmitting + Transmitting + + + + Type: + Type: + + + + + Unknown error. + Unknown error. + + + + Unmute + Unmute + + + + Update query planner statistics for better performance + Update query planner statistics for better performance + + + + VAD listening + VAD listening + + + + VAD max gain: + VAD max gain: + + + + VAD speech threshold: + VAD speech threshold: + + + + VAD unavailable + VAD unavailable + + + + Verified + Verified + + + + Verifies all data structures and indexes (thorough) + Verifies all data structures and indexes (thorough) + + + + Video + Video + + + + Voice (VOIP) + Voice (VOIP) + + + + Voice Compressor + Voice Compressor + + + + Voice codec quality + Voice codec quality + + + + Voice message (hold) + Voice message (hold) + + + + Voice messages will play automatically when received. Multiple messages queue up and play sequentially without interrupting current playback. + Voice messages will play automatically when received. Multiple messages queue up and play sequentially without interrupting current playback. + + + + Volume: + Volume: + + + + You are now a channel co-owner. +Received admin key for channel %1 from 0x%2 + You are now a channel co-owner. +Received admin key for channel %1 from 0x%2 + + + + Your name (shown to others) + Your name (shown to others) + + + + + alloc failed + alloc failed + + + + base64 decode failed + base64 decode failed + + + + blob too short (%1 bytes) + blob too short (%1 bytes) + + + + cannot open: %1 + cannot open: %1 + + + + created %1 + created %1 + + + + direct + direct + + + + edited + edited + + + + indirect + indirect + + + + invalid addr count %1 + invalid addr count %1 + + + + invalid prefix (expected utun://) + invalid prefix (expected utun://) + + + + + n/a + n/a + + + + no stream info: %1 + no stream info: %1 + + + + no video stream: %1 + no video stream: %1 + + + + not connected + not connected + + + + offline + offline + + + + online + online + + + + reverse + reverse + + + + seen %1 + seen %1 + + + + sws init failed + sws init failed + + + + truncated at addr %1 of block + truncated at addr %1 of block + + + + truncated at channel_id + truncated at channel_id + + + + truncated at join_key + truncated at join_key + + + + truncated at pass_len + truncated at pass_len + + + + truncated at password + truncated at password + + + + truncated at pubkey + truncated at pubkey + + + + truncated at reality + truncated at reality + + + + truncated at reality params + truncated at reality params + + + + truncated at reality server_name + truncated at reality server_name + + + + uTun thread is not ready yet. Please wait and try again. + uTun thread is not ready yet. Please wait and try again. + + + + unsupported version 0x%1 + unsupported version 0x%1 + + + + vibechat is already running. + vibechat is already running. + + + + video decoder open failed + video decoder open failed + + + + video unavailable (built without FFmpeg) + video unavailable (built without FFmpeg) + + + + ── Addresses ── + ── Addresses ── + + + + ── Connections ── + ── Connections ── + + + + ── Listen ── + ── Listen ── + + + + 🎙 %1 is speaking + 🎙 %1 is speaking + + + + 🔴 Recording %1:%2 + 🔴 Recording %1:%2 + + + + 🔴 Recording 0:00 + 🔴 Recording 0:00 + + + diff --git a/tools/chatgui/translations/chatgui_ru.ts b/tools/chatgui/translations/chatgui_ru.ts new file mode 100644 index 00000000..633b75ba --- /dev/null +++ b/tools/chatgui/translations/chatgui_ru.ts @@ -0,0 +1,2058 @@ + + + + + ChatGui + + + addresses: + + адреса: + + + + + + addresses: none + + адреса: нет + + + + + created: %1 + + создан: %1 + + + + + last seen: %1 + + был в сети: %1 + + + + + (auto) + (авто) + + + + + + (default) + (по умолчанию) + + + + (self) + (свой) + + + + +%1 more + ещё +%1 + + + + GB + ГБ + + + + MB + МБ + + + + + + dB + дБ + + + + kbps + кбит/с + + + + ms + мс + + + + s + с + + + + · Radio enabled + · Рация включена + + + + ⚠ This is your own link + ⚠ Это ваша собственная ссылка + + + + %1 [LOCAL] + %1 [ЛОКАЛЬНОЕ] + + + + + + + %1 B + %1 Б + + + + + + + %1 KB + %1 КБ + + + + + + + %1 MB + %1 МБ + + + + %1: FAILED (%2 ms) + %1: ОШИБКА (%2 мс) + + + + %1: OK (%2 ms) + %1: OK (%2 мс) + + + + + %1B + %1 Б + + + + + %1GB + %1 ГБ + + + + + %1KB + %1 КБ + + + + + %1MB + %1 МБ + + + + %1Mbps + %1 Мбит/с + + + + %1d + %1 д + + + + %1h + %1 ч + + + + %1kbps + %1 кбит/с + + + + %1m + %1 мин + + + + %1s + %1 с + + + + (Admin/Super require channel private key) + (Администратор/суперузел требуют приватный ключ канала) + + + + + Accept + Принять + + + + Active ETCP Connections (10 fps) + Активные соединения ETCP (10 обновлений/с) + + + + Add + Добавить + + + + Addrs + Адреса + + + + Admin + Администратор + + + + Alphabetical (A-Z) + По алфавиту (А–Я) + + + + Animations + Анимации + + + + Another invitation is still being created. Try again. + Другое приглашение ещё создаётся. Повторите попытку. + + + + Application minimized to tray. + Приложение свёрнуто в трей. + + + + Applying settings… + Применение настроек… + + + + As a file + Как файл + + + + As a video + Как видео + + + + Ask before joining + Спрашивать перед присоединением + + + + Assign a shortcut in system settings. + Назначьте сочетание в системных настройках. + + + + Assigning global shortcuts… + Назначение глобальных хоткеев… + + + + Assignment cancelled. Click the button to try again. + Назначение отменено. Нажмите кнопку, чтобы повторить. + + + + Attach + Прикрепить + + + + Attach file + Прикрепить файл + + + + Audio + Аудио + + + + Audio output + Выход звука + + + + Auto (0.0.0.0) + Авто (0.0.0.0) + + + + Auto (any) + Авто (любой) + + + + Auto sockets (UDP+TCP from interfaces) + Автосокеты (UDP+TCP на интерфейсах) + + + + Auto-connect to group nodes + Автоматически подключаться к узлам группы + + + + Auto-join + Присоединяться автоматически + + + + Auto-play voice messages on receive + Автоматически воспроизводить полученные голосовые сообщения + + + + Autoload max size: + Максимальный размер автозагрузки: + + + + Autoload media + Автоматически загружать медиа + + + + Automatic voice transmission + Автоматическая передача по голосу + + + + + Awaiting delivery + Ожидает доставки + + + + Bitrate: + Битрейт: + + + + Buffer: %1 ms · Reserve: %2 ms +Over 10 s: minimum %3 · maximum %4 ms +Speed: %5× · RTT: %6 ms +Buffer drops: %7 · Underruns: %8 + Буфер: %1 мс · Запас: %2 мс +За 10 с: минимум %3 · максимум %4 мс +Скорость: %5× · RTT: %6 мс +Потери буфера: %7 · Недостачи: %8 + + + + Call + Звонок + + + + + + Call peer + Позвонить + + + + Call: connecting + Звонок: подключение + + + + Call: maximum jitter reserve + Звонок: максимальный запас джиттер-буфера + + + + Call: ringback + Звонок: ожидание ответа + + + + Calling… + Звоним… + + + + + + + + + + Cancel + Отмена + + + + Cancel recording + Отменить запись + + + + Capture stereo (playback is always stereo) + Запись в стерео (воспроизведение всегда в стерео) + + + + Changes take effect after restart. + Изменения вступят в силу после перезапуска. + + + + Channel + Канал + + + + Channel Settings + Настройки канала + + + + + Channel not found. + Канал не найден. + + + + Channel owner (has private key) + Владелец канала (имеет приватный ключ) + + + + Channel sorting: + Сортировка каналов: + + + + Channels + Каналы + + + + GRP + ГРП + + + + Checking... + Проверка... + + + + Clear + Очистить + + + + Click Refresh to load nodes + Нажмите «Обновить», чтобы загрузить узлы + + + + Click Refresh to load status + Нажмите «Обновить», чтобы загрузить состояние + + + + + Close + Закрыть + + + + + Connect + Подключиться + + + + Connected! + Подключено! + + + + Connecting global shortcuts… + Подключение глобальных хоткеев… + + + + Connecting to peer... + Подключение к узлу... + + + + Connecting… + Пытаемся связаться… + + + + + Connection Monitor + Монитор соединений + + + + Connection failed: %1 + Ошибка подключения: %1 + + + + Connection timed out. + Время ожидания подключения истекло. + + + + Connection: %1 + Подключение: %1 + + + + Connections + Соединения + + + + Copy + Копировать + + + + Core unavailable + Ядро недоступно + + + + Create + Создать + + + + + Create group + Создать группу + + + + Creating and sending invitation… + Создание и отправка приглашения… + + + + Ctrl+wheel — zoom + Ctrl+колесо — масштаб + + + + Database + База данных + + + + Database Maintenance + Обслуживание базы данных + + + + + Database not connected + База данных не подключена + + + + Database not connected (m_db=%1, m_db->m_db=%2) + База данных не подключена (m_db=%1, m_db->m_db=%2) + + + + + + Database not open + База данных не открыта + + + + + Decline + Отклонить + + + + + + + + Default (auto) + По умолчанию (авто) + + + + Deleted + Удалён + + + + + Delivered + Доставлено + + + + Deny all + Отклонять все + + + + Details + Подробности + + + + Direct messages + Личные + + + + Disable automatic voice transmission + Выключить автоматическую передачу по голосу + + + + Display name: + Отображаемое имя: + + + + + Download + Скачать + + + + Downloading... + Загрузка... + + + + Downloading… + Загрузка… + + + + Edit + Изменить + + + + Emoji + Смайлики + + + + Enable VAD using the button on the radio panel. Speech prebuffer is 0.6 s. The VAD compressor has a separate maximum gain; other parameters are shared with the transmission compressor. Set the gain rise rate in Audio. + VAD включается кнопкой на панели рации. Буфер начала речи — 0,6 с. Компрессор VAD имеет отдельное максимальное усиление; остальные параметры общие с компрессором передачи. Скорость нарастания усиления задаётся в разделе «Аудио». + + + + Enable automatic voice transmission + Включить автоматическую передачу по голосу + + + + Enable compressor (leveler/normalizer) + Включить компрессор (выравнивание громкости) + + + + Enable echo cancellation (AEC) in calls + Включить подавление эха (AEC) в звонках + + + + Enable sounds + Включить звуки + + + + + End call + Завершить + + + + Ended + Завершён + + + + Enter a name... + Введите название... + + + + Enter an invite link. + Введите пригласительную ссылку. + + + + Entry node: + Узел входа: + + + + + + + Error + Ошибка + + + + Error: %1 + Ошибка: %1 + + + + Error: this is your own link (you cannot invite yourself) + Ошибка: это ваша собственная ссылка (нельзя пригласить себя) + + + + Events + События + + + + Expected a utun:// link... + Ожидается utun:// ссылка... + + + + Failed to apply radio settings. Check that the core and configuration file are available; see the radio log for details. + Не удалось применить настройки рации. Проверьте доступность ядра и файла конфигурации; подробности в логе radio. + + + + Failed to build the link (see log) + Не удалось собрать ссылку (см. лог) + + + + Failed to create a direct conversation. + Не удалось создать личную беседу. + + + + Failed to create an invitation. Check the connection and try again. + Не удалось создать приглашение. Проверьте подключение и повторите попытку. + + + + Failed to identify the node (see log) + Не удалось определить узел (см. лог) + + + + Failed to load photo + Не удалось загрузить фото + + + + Failed to open the link. Check that a browser is installed. + Не удалось открыть ссылку. Проверьте наличие браузера. + + + + Failed to prepare attachment + Не удалось подготовить вложение + + + + Failed to read configuration: %1 + Не удалось прочитать конфигурацию: %1 + + + + Failed to save configuration: %1 + Не удалось сохранить конфигурацию: %1 + + + + Failed to save the interface language. Check the configuration file. + Не удалось сохранить язык интерфейса. Проверьте файл конфигурации. + + + + + Failed to select device + Не удалось выбрать устройство + + + + Failed to send radio settings to the core + Не удалось отправить настройки рации в ядро + + + + Failed to send the invitation. + Не удалось отправить приглашение. + + + + Failed to start key recording. See the radio log for details. + Не удалось начать запись клавиш. Подробности в логе radio. + + + + File: + Файл: + + + + + Full Check + Полная проверка + + + + + Generating link... + Генерация ссылки... + + + + Gestures + Жесты + + + + Global shortcuts are available on Windows and Linux/X11. Shortcut recording is unavailable on this platform. + Глобальные хоткеи доступны на Windows и Linux/X11. На этой платформе запись недоступна. + + + + Global shortcuts connected. + Глобальные хоткеи подключены. + + + + Global shortcuts unavailable. Try assigning again; see the radio log for details. + Глобальные хоткеи недоступны. Повторите назначение; подробности в логе radio. + + + + Global shortcuts work from any application. Assign a regular key with Ctrl, Shift, Alt or Win in the system dialog. Changes are saved by the system immediately. Keyboard transmission is disabled during setup. + Глобальные хоткеи работают из любых приложений. Назначьте обычную клавишу с Ctrl, Shift, Alt или Win в системном окне. Изменения сохраняются системой сразу. Во время настройки передача с клавиатуры отключена. + + + + Group name: + Название группы: + + + + Hearts & Symbols + Сердца и символы + + + + High (64 kbps, best) + Высокое (64 кбит/с, лучшее) + + + + Hold a shortcut to transmit from any application, even when ChatGUI is not focused. Channel radio must be enabled. To record a shortcut, hold the desired keys together and release them. Left and right Ctrl, Shift, Alt and Win are distinct. Fn is available only if the keyboard reports it to the OS. + Удерживайте сочетание для передачи из любого приложения — даже когда ChatGUI не в фокусе. Рация канала должна быть включена. Для записи зажмите нужные клавиши вместе и отпустите их. Левые и правые Ctrl, Shift, Alt, Win различаются. Fn доступна, только если клавиатура передаёт её ОС. + + + + Hold the button or a global shortcut to transmit + Держите кнопку или глобальный хоткей для передачи + + + + Hold to talk + Держите и говорите + + + + IP Address: + IP-адрес: + + + + In call + Разговор + + + + Incoming call + Входящий звонок + + + + Incoming call from %1 + Входящий звонок от %1 + + + + Incoming call… + Входящий звонок… + + + + Incoming message + Входящее сообщение + + + + Interface language: + Язык интерфейса: + + + + Interface: + Интерфейс: + + + + Invalid invite link: + Неверная пригласительная ссылка: + + + + Invalid invite link: pubkey is %1 bytes (need 32) + Неверная пригласительная ссылка: публичный ключ содержит %1 байт (нужно 32) + + + + Invalid link. + Некорректная ссылка. + + + + + + Invalid link: + Некорректная ссылка: + + + + Invalid radio setting: %1 + Некорректная настройка рации: %1 + + + + + + Invitation + Приглашение + + + + Invitation creation cancelled. + Создание приглашения отменено. + + + + Invitation sent, waiting for a response... + Приглашение отправлено, ожидание ответа... + + + + Invitation to group “%1” + Приглашение в группу «%1» + + + + + Invite + Пригласить + + + + Invite link + Пригласительная ссылка + + + + Invite to channel %1 + Пригласить в канал %1 + + + + + + Invite to group + Пригласить в группу + + + + Join + Присоединиться + + + + Join Channel + Присоединиться к каналу + + + + Join requests: + Запросы на присоединение: + + + + Key %1 + Клавиша %1 + + + + Known Nodes + Известные узлы + + + + Language changes take effect after restart. + Смена языка вступит в силу после перезапуска. + + + + + Link + Ссылка + + + + Link: node 0x%1 key %2, %3 addresses (%4) +Invitation to channel: %5 + Ссылка: узел 0x%1 ключ %2, %3 адр. (%4) +Приглашение в канал: %5 + + + + Listen Sockets + Слушающие сокеты + + + + Live ETCP connection monitor +with per-link BBR statistics + Монитор соединений ETCP в реальном времени +со статистикой BBR по каждому линку + + + + Loading... + Загрузка... + + + + Local + Локальный + + + + Local nickname... + Локальное имя... + + + + Low (16 kbps, fast) + Низкое (16 кбит/с, быстро) + + + + Max gain: + Максимальное усиление: + + + + Max storage size: + Максимальный размер хранилища: + + + + Maximum adaptive jitter reserve for calls. Applies to the next call. + Максимальный адаптивный запас джиттер-буфера звонков. Применяется к следующему звонку. + + + + Media Storage + Хранилище медиа + + + + Member Properties + Свойства участника + + + + + Members + Участники + + + + Members 🔑 + Участники 🔑 + + + + Microphone + Микрофон + + + + Microphone test + Проверка микрофона + + + + Microphone unavailable + Микрофон недоступен + + + + Mode: + Режим: + + + + Music (AUDIO) + Музыка (AUDIO) + + + + + + Mute + Выключить микрофон + + + + Name + Имя + + + + Name: + Имя: + + + + Network + Сеть + + + + No addresses. + Нет адресов. + + + + No groups available to invite to. + Нет групп для приглашения. + + + + No nodes found. + Узлы не найдены. + + + + No peer addresses in the invite link. + В пригласительной ссылке нет адресов узла. + + + + Node ID + ID узла + + + + + + Node is not running. + Узел не запущен. + + + + + Node not initialized. + Узел не инициализирован. + + + + Node: %1 + Узел: %1 + + + + Node: 0x%1 + Узел: 0x%1 + + + + Node: 0x%1 %2 + Узел: 0x%1 %2 + + + + Nodes + Узлы + + + + Not enough memory. + Недостаточно памяти. + + + + + Not set + Не задано + + + + + Offline + Не в сети + + + + Online + В сети + + + + Open Connection Monitor + Открыть монитор соединений + + + + + Optimize + Оптимизировать + + + + Outgoing message + Исходящее сообщение + + + + PTT shortcut %1: %2 + PTT хоткей %1: %2 + + + + Parsing link... + Разбор ссылки... + + + + Paste invite link: + Вставьте пригласительную ссылку: + + + + Paste the invited node's utun:// link: + Вставьте utun:// ссылку приглашаемого узла: + + + + Paste the node's utun:// link. + Вставьте utun:// ссылку узла. + + + + + Pause + Пауза + + + + Peer connected + Узел подключён + + + + Peer disconnected + Узел отключён + + + + + Peer unavailable + Собеседник недоступен + + + + Photo + Фото + + + + Play + Воспроизвести + + + + Play a notification sound when a message arrives in this channel. + Воспроизводить звук уведомления при получении сообщения в этом канале. + + + + Play sound on incoming message + Звук при получении сообщения + + + + Playback device: + Устройство воспроизведения: + + + + Port: + Порт: + + + + Preparing media… + Подготовка медиа… + + + + Press one or more keys together… + Нажмите одну или несколько клавиш вместе… + + + + Private + Частный + + + + Profile + Профиль + + + + Protocol: + Протокол: + + + + Public + Публичный + + + + Push-To-Talk (PTT) mode + Режим «нажми и говори» (PTT) + + + + Query error + Ошибка запроса + + + + + Quick Check + Быстрая проверка + + + + Quit + Выход + + + + + Radio + Рация + + + + Radio (PTT) + Рация (PTT) + + + + Radio (PTT) — Opus + Рация (PTT) — Opus + + + + Radio settings + Настройки рации + + + + Radio settings are not ready yet + Настройки рации ещё не готовы + + + + Radio: toggle channel listening + Рация: включить/выключить прослушивание канала + + + + Radio: transmit %1 + Рация: передача %1 + + + + Received + Получено + + + + Receiving + Приём + + + + Recent Activity + По последней активности + + + + Recipient: %1 + Получатель: %1 + + + + Record voice messages with push-to-talk. (Coming soon) + Запись голосовых сообщений в режиме «нажми и говори». (Скоро) + + + + Recording device: + Устройство записи: + + + + + Refresh + Обновить + + + + Relay: 0x%1 + Посредник: 0x%1 + + + + Remove + Удалить + + + + + Rename + Переименовать + + + + Rise rate (x/sec): + Скорость нарастания (×/с): + + + + Run a check to see results... + Запустите проверку, чтобы увидеть результаты... + + + + + + + Save + Сохранить + + + + Save file + Сохранить файл + + + + Save to file + Сохранить в файл + + + + Scans structure without verifying indexes (fast) + Проверка структуры без проверки индексов (быстро) + + + + Scroll to bottom + Прокрутить вниз + + + + Select a connection to view metrics + Выберите соединение для просмотра показателей + + + + Select a group + Выберите группу + + + + + Select a member + Выберите участника + + + + Selected device unavailable; using an available device + Выбранное устройство недоступно; используется доступное + + + + Send + Отправить + + + + Send message + Написать сообщение + + + + Send the channel private key to "%1"? + +The key is transferred encrypted and signed. The recipient will become a co-owner and will be able to manage members. + Отправить приватный ключ канала участнику «%1»? + +Ключ передаётся с шифрованием и подписью. Получатель станет совладельцем и сможет управлять участниками. + + + + Send the channel private key to this member (encrypted + signed) + Отправить участнику приватный ключ канала (с шифрованием и подписью) + + + + Send video + Отправить видео + + + + + Set PTT shortcut %1 + Задать PTT хоткей %1 + + + + Set PTT shortcuts… + Задать PTT хоткеи… + + + + + + + Settings + Настройки + + + + Settings error + Ошибка настройки + + + + Settings for channel + Настройки канала + + + + Show + Показать + + + + Silence before ending transmission: + Тишина до окончания передачи: + + + + Size: + Размер: + + + + Smileys & People + Смайлики и люди + + + + Sound + Звук + + + + Speaker test + Проверка динамиков + + + + Speech probability threshold: lower is more sensitive. This is not a volume threshold. + Порог вероятности речи: меньше — чувствительнее. Это не порог громкости. + + + + Standard (32 kbps) + Стандартное (32 кбит/с) + + + + Status + Состояние + + + + Stop Test + Остановить проверку + + + + Storage + Хранилище + + + + Storage (priority attachment keeper) + Хранилище (приоритетное хранение вложений) + + + + Storage period expired + Срок хранения истёк + + + + Supernode + Суперузел + + + + System Status + Состояние системы + + + + System language + Язык системы + + + + + Test Microphone + Проверить микрофон + + + + Test Speaker (440Hz) + Проверить динамики (440 Гц) + + + + + + The core is not running. + Ядро не запущено. + + + + The core rejected radio settings + Ядро отклонило настройки рации + + + + The file contains video. How would you like to send it? + Файл содержит видео. Как отправить? + + + + The invitation expires 10 minutes after creation. +If it has expired, ask the sender to create a new link. + Приглашение действует 10 минут с момента создания. +Если срок истёк, попросите отправителя создать новую ссылку. + + + + The link expires %1 minutes after creation. + Ссылка действует %1 минут с момента создания. + + + + The link expires %1 minutes after creation. +If the direct message arrives late, a new invitation will be needed. + Ссылка действует %1 минут с момента создания. +При поздней доставке личного сообщения потребуется новое приглашение. + + + + The node has joined the channel. + Узел присоединился к каналу. + + + + The public key must be 32 bytes. + Публичный ключ должен содержать 32 байта. + + + + Timed out — the node did not respond. + Таймаут — узел не ответил. + + + + + Transfer admin rights + Передать права администратора + + + + Transmission and VAD compressor + Компрессор передачи и VAD + + + + Transmission max gain: + Максимальное усиление передачи: + + + + Transmitting + Передача + + + + Type: + Тип: + + + + + Unknown error. + Неизвестная ошибка. + + + + Unmute + Включить микрофон + + + + Update query planner statistics for better performance + Обновить статистику планировщика запросов для повышения производительности + + + + VAD listening + VAD слушает + + + + VAD max gain: + Максимальное усиление VAD: + + + + VAD speech threshold: + Порог речи VAD: + + + + VAD unavailable + VAD недоступен + + + + Verified + Проверен + + + + Verifies all data structures and indexes (thorough) + Проверка всех структур данных и индексов (подробно) + + + + Video + Видео + + + + Voice (VOIP) + Голос (VOIP) + + + + Voice Compressor + Голосовой компрессор + + + + Voice codec quality + Качество голосового кодека + + + + Voice message (hold) + Голосовое сообщение (удерживайте) + + + + Voice messages will play automatically when received. Multiple messages queue up and play sequentially without interrupting current playback. + Голосовые сообщения воспроизводятся автоматически при получении. Несколько сообщений ставятся в очередь и воспроизводятся по порядку, не прерывая текущее. + + + + Volume: + Громкость: + + + + You are now a channel co-owner. +Received admin key for channel %1 from 0x%2 + Теперь вы совладелец канала. +Получен ключ администратора канала %1 от 0x%2 + + + + Your name (shown to others) + Ваше имя (видно другим) + + + + + alloc failed + ошибка выделения памяти + + + + base64 decode failed + ошибка декодирования base64 + + + + blob too short (%1 bytes) + данные слишком короткие (%1 байт) + + + + cannot open: %1 + не удалось открыть: %1 + + + + created %1 + создан %1 + + + + direct + прямое + + + + edited + изменено + + + + indirect + через посредника + + + + invalid addr count %1 + неверное число адресов: %1 + + + + invalid prefix (expected utun://) + неверный префикс (ожидается utun://) + + + + + n/a + нет данных + + + + no stream info: %1 + нет информации о потоках: %1 + + + + no video stream: %1 + нет видеопотока: %1 + + + + not connected + нет подключения + + + + offline + не в сети + + + + online + в сети + + + + reverse + обратное + + + + seen %1 + был в сети %1 + + + + sws init failed + ошибка инициализации преобразования изображения + + + + truncated at addr %1 of block + обрезан адрес %1 в блоке + + + + truncated at channel_id + обрезан идентификатор канала + + + + truncated at join_key + обрезан ключ присоединения + + + + truncated at pass_len + обрезано поле длины пароля + + + + truncated at password + обрезан пароль + + + + truncated at pubkey + обрезан публичный ключ + + + + truncated at reality + обрезано поле REALITY + + + + truncated at reality params + обрезаны параметры REALITY + + + + truncated at reality server_name + обрезано имя сервера REALITY + + + + uTun thread is not ready yet. Please wait and try again. + Поток uTun ещё не готов. Подождите и повторите попытку. + + + + unsupported version 0x%1 + неподдерживаемая версия 0x%1 + + + + vibechat is already running. + vibechat уже запущен. + + + + video decoder open failed + не удалось открыть видеодекодер + + + + video unavailable (built without FFmpeg) + видео недоступно (сборка без FFmpeg) + + + + ── Addresses ── + ── Адреса ── + + + + ── Connections ── + ── Соединения ── + + + + ── Listen ── + ── Прослушивание ── + + + + 🎙 %1 is speaking + 🎙 %1 говорит + + + + 🔴 Recording %1:%2 + 🔴 Запись %1:%2 + + + + 🔴 Recording 0:00 + 🔴 Запись 0:00 + + + diff --git a/tools/chatgui/translations/translations.qrc.in b/tools/chatgui/translations/translations.qrc.in new file mode 100644 index 00000000..ccb3a397 --- /dev/null +++ b/tools/chatgui/translations/translations.qrc.in @@ -0,0 +1,7 @@ + + + @CMAKE_CURRENT_BINARY_DIR@/translations/chatgui_en.qm + @CMAKE_CURRENT_BINARY_DIR@/translations/chatgui_ru.qm + @QT_RUSSIAN_CATALOG@ + + diff --git a/tools/chatgui/transport/node_config.cpp b/tools/chatgui/transport/node_config.cpp index bac5ff5f..5e39b388 100644 --- a/tools/chatgui/transport/node_config.cpp +++ b/tools/chatgui/transport/node_config.cpp @@ -1,6 +1,7 @@ // node_config.cpp — Simplified INI config manager for embedded uTun node #include "node_config.h" #include "config_updater.h" +#include "../../../lib/debug_config.h" #include extern "C" { @@ -39,7 +40,7 @@ static const QSet CONTROL_KEYS = { }; static const QSet GUI_KEYS = { - "db_path", "debug_file", "debug_level", + "db_path", "debug_file", "debug_level", "language", "sound_dir", "sound_msg_incoming", "sound_msg_outgoing", "sound_connect", "sound_disconnect" }; @@ -225,6 +226,7 @@ bool NodeConfig::load() { m_servers.clear(); m_clients.clear(); m_errors.clear(); + m_language = QStringLiteral("system"); QString section, controlIp, controlPort; QTextStream in(&f); @@ -336,6 +338,10 @@ bool NodeConfig::load() { if (key == "db_path") m_dbPath = val; else if (key == "debug_file") m_debugFile = val; else if (key == "debug_level") m_debugLevel = val; + else if (key == "language") { + if (val == "system" || val == "en" || val == "ru") m_language = val; + else DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "GUI language: invalid preference '%s', using system language", qPrintable(val)); + } } else if (debugSection) { if (!m_debugCategories.isEmpty()) m_debugCategories += ','; m_debugCategories += key + '=' + val; @@ -386,6 +392,7 @@ bool NodeConfig::saveFull() { out << "[gui]\n"; out << "db_path=" << (m_dbPath.isEmpty() ? "chat_data" : m_dbPath) << "\n"; out << "debug_file=" << m_debugFile << "\n"; + out << "language=" << m_language << "\n"; out << "debug_level=" << (m_debugLevel.isEmpty() ? "info" : m_debugLevel) << "\n\n"; out << "[debug]\n"; @@ -445,6 +452,7 @@ bool NodeConfig::save() { config_update_in_lines(lines, "gui", "db_path", m_dbPath.isEmpty() ? "chat_data" : m_dbPath, true); config_update_in_lines(lines, "gui", "debug_file", m_debugFile, true); + config_update_in_lines(lines, "gui", "language", m_language, true); config_update_in_lines(lines, "gui", "debug_level", m_debugLevel.isEmpty() ? "info" : m_debugLevel, true); /* [debug] section: each category=level as separate line */ diff --git a/tools/chatgui/transport/node_config.h b/tools/chatgui/transport/node_config.h index 7cef90ee..912ab432 100644 --- a/tools/chatgui/transport/node_config.h +++ b/tools/chatgui/transport/node_config.h @@ -54,6 +54,7 @@ public: void setDebugFile(const QString& p) { m_debugFile = p; } QString debugLevel() const { return m_debugLevel; } QString debugCategories() const { return m_debugCategories; } + QString language() const { return m_language; } /* Config validation errors (collected during load()) */ bool hasErrors() const { return !m_errors.isEmpty(); } @@ -77,6 +78,7 @@ private: QString m_debugFile; QString m_debugLevel; QString m_debugCategories; + QString m_language = QStringLiteral("system"); QStringList m_errors; }; #endif // NODE_CONFIG_H