diff --git a/CMakeLists.txt b/CMakeLists.txt index af1dc49..b566038 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,7 +10,7 @@ set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC ON) # Qt6 -find_package(Qt6 REQUIRED COMPONENTS Core DBus Quick QuickControls2 Widgets) +find_package(Qt6 REQUIRED COMPONENTS Core DBus Quick QuickControls2 Widgets LinguistTools) # KDE Frameworks find_package(KF6Kirigami REQUIRED) diff --git a/README.md b/README.md index 2168b4a..022404d 100644 --- a/README.md +++ b/README.md @@ -121,13 +121,14 @@ If support for other distributions becomes reliable, they should be added here e ### Language Support -The current application UI is primarily in German. +BearWave currently supports English and German. -- UI labels, dialogs, and user-facing controls are currently mostly German -- README, repository metadata, and development-facing material are in English -- full localization support is not implemented yet +- the application uses the system locale to select its UI language +- English is the base UI language +- German is provided as a bundled translation +- README, repository metadata, and development-facing material remain in English -If you expect a fully translated multi-language UI, that is not the current state yet. +If the system language is German, BearWave appears in German. Otherwise it falls back to English. ## Installation Status @@ -284,7 +285,6 @@ If desktop caches are stale, a logout/login cycle may still be required. ## Current Limitations -- UI language is currently primarily German - packaging and install guidance are strongest on Arch Linux - broader distro support is not yet validated to the same standard - there are no official binary releases for non-technical end users yet diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b6777d0..04dc549 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -18,6 +18,22 @@ add_executable(bearwave ${SOURCES}) # Qt Resources target_sources(bearwave PRIVATE qml.qrc) +qt_add_lrelease( + TS_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/../translations/bearwave_de.ts + QM_FILES_OUTPUT_VARIABLE bearwave_qm_files +) + +foreach(qm_file IN LISTS bearwave_qm_files) + get_filename_component(qm_name "${qm_file}" NAME) + set_source_files_properties("${qm_file}" PROPERTIES QT_RESOURCE_ALIAS "${qm_name}") +endforeach() + +qt_add_resources(bearwave bearwave_translations + PREFIX /i18n + FILES ${bearwave_qm_files} +) + # Link libraries target_link_libraries(bearwave PRIVATE Qt6::Core diff --git a/src/main.cpp b/src/main.cpp index 9f63661..89cd597 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,8 +1,10 @@ #include #include #include +#include #include #include +#include #include "radiobackend.h" #include "mprisadaptor.h" @@ -20,6 +22,11 @@ int main(int argc, char *argv[]) app.setWindowIcon(QIcon::fromTheme(QStringLiteral("org.kde.bearwave"))); app.setQuitOnLastWindowClosed(false); + QTranslator appTranslator; + if (appTranslator.load(QLocale::system(), QStringLiteral("bearwave"), QStringLiteral("_"), QStringLiteral(":/i18n"))) { + app.installTranslator(&appTranslator); + } + QQmlApplicationEngine engine; RadioBackend backend; diff --git a/src/qml/Main.qml b/src/qml/Main.qml index f281c65..ba29ba8 100644 --- a/src/qml/Main.qml +++ b/src/qml/Main.qml @@ -5,7 +5,7 @@ import Qt.labs.platform 1.1 as Platform ApplicationWindow { id: root - title: "BearWave" + title: qsTr("BearWave") width: 980 height: 700 minimumWidth: 520 @@ -21,11 +21,11 @@ ApplicationWindow { id: systray visible: true icon.name: "multimedia-player" - tooltip: "BearWave" + tooltip: qsTr("BearWave") menu: Platform.Menu { Platform.MenuItem { - text: backend && backend.player && backend.player.playing ? "Pause" : "Play" + text: backend && backend.player && backend.player.playing ? qsTr("Pause") : qsTr("Play") onTriggered: { if (backend && backend.player) { backend.player.togglePlayPause() @@ -33,7 +33,7 @@ ApplicationWindow { } } Platform.MenuItem { - text: root.visible ? "Verstecken" : "Zeigen" + text: root.visible ? qsTr("Hide") : qsTr("Show") onTriggered: { if (root.visible) { root.hide() @@ -46,7 +46,7 @@ ApplicationWindow { } Platform.MenuSeparator {} Platform.MenuItem { - text: "Beenden" + text: qsTr("Quit") onTriggered: Qt.quit() } } @@ -142,7 +142,7 @@ ApplicationWindow { spacing: 8 Label { - text: "BearWave" + text: qsTr("BearWave") color: textMain font.pixelSize: compactMode ? 18 : 22 font.bold: true @@ -156,7 +156,7 @@ ApplicationWindow { } Button { - text: "Top" + text: qsTr("Top") highlighted: currentPage === "top" onClicked: { if (!backend) return @@ -167,7 +167,7 @@ ApplicationWindow { } Button { - text: "DE" + text: qsTr("DE") highlighted: currentPage === "german" onClicked: { if (!backend) return @@ -178,7 +178,7 @@ ApplicationWindow { } Button { - text: "NL" + text: qsTr("NL") highlighted: currentPage === "dutch" onClicked: { if (!backend) return @@ -189,7 +189,7 @@ ApplicationWindow { } Button { - text: "Favoriten" + text: qsTr("Favorites") highlighted: currentPage === "favorites" onClicked: { currentPage = "favorites" @@ -200,18 +200,18 @@ ApplicationWindow { Item { Layout.fillWidth: true } Button { - text: compactMode ? "+" : "Manuell +" + text: compactMode ? "+" : qsTr("Manual +") onClicked: addDialog.open() } Button { - text: "About" + text: qsTr("About") onClicked: aboutDialog.open() } Button { visible: backend && backend.canResumeLastStation - text: compactMode ? "↺" : "Resume" + text: compactMode ? "↺" : qsTr("Resume") onClicked: { if (backend) { backend.resumeLastStation() @@ -227,7 +227,7 @@ ApplicationWindow { TextField { id: searchField Layout.fillWidth: true - placeholderText: "Sender suchen (Name, Genre, Land)" + placeholderText: qsTr("Search stations (name, genre, country)") onTextChanged: { if (backend) { backend.filterQuery = text @@ -241,7 +241,7 @@ ApplicationWindow { } Button { - text: "Suche" + text: qsTr("Search") highlighted: true onClicked: { if (searchField.text.length < 2 || !backend) return @@ -251,7 +251,7 @@ ApplicationWindow { } Button { - text: compactMode ? "A-Z" : "Sort A-Z" + text: compactMode ? "A-Z" : qsTr("Sort A-Z") onClicked: { if (backend && currentPage !== "favorites") { backend.sortStations("name") @@ -260,7 +260,7 @@ ApplicationWindow { } Button { - text: compactMode ? "kb" : "Sort Bitrate" + text: compactMode ? "kb" : qsTr("Sort Bitrate") onClicked: { if (backend && currentPage !== "favorites") { backend.sortStations("bitrate") @@ -269,7 +269,7 @@ ApplicationWindow { } Button { - text: compactMode ? "❤" : "Sort Votes" + text: compactMode ? "❤" : qsTr("Sort Votes") onClicked: { if (backend && currentPage !== "favorites") { backend.sortStations("votes") @@ -280,7 +280,7 @@ ApplicationWindow { Label { Layout.fillWidth: true - text: "Tipp: Du bist nicht auf DE/NL begrenzt - suche nach Ländern, Genres oder Sendernamen weltweit." + text: qsTr("Tip: you are not limited to DE/NL. Search worldwide by country, genre, or station name.") color: textMuted font.pixelSize: 11 elide: Text.ElideRight @@ -291,13 +291,13 @@ ApplicationWindow { spacing: 6 Label { - text: "Genre:" + text: qsTr("Genre:") color: textMuted font.pixelSize: 11 } Button { - text: "Rock" + text: qsTr("Rock") highlighted: activeQuickFilter === "tag:rock" onClicked: { if (!backend) return @@ -308,7 +308,7 @@ ApplicationWindow { } Button { - text: "News" + text: qsTr("News") highlighted: activeQuickFilter === "tag:news" onClicked: { if (!backend) return @@ -319,7 +319,7 @@ ApplicationWindow { } Button { - text: "Jazz" + text: qsTr("Jazz") highlighted: activeQuickFilter === "tag:jazz" onClicked: { if (!backend) return @@ -332,13 +332,13 @@ ApplicationWindow { Item { Layout.preferredWidth: 10 } Label { - text: "Land:" + text: qsTr("Country:") color: textMuted font.pixelSize: 11 } Button { - text: "US" + text: qsTr("US") highlighted: activeQuickFilter === "cc:US" onClicked: { if (!backend) return @@ -349,7 +349,7 @@ ApplicationWindow { } Button { - text: "UK" + text: qsTr("UK") highlighted: activeQuickFilter === "cc:GB" onClicked: { if (!backend) return @@ -360,7 +360,7 @@ ApplicationWindow { } Button { - text: "FR" + text: qsTr("FR") highlighted: activeQuickFilter === "cc:FR" onClicked: { if (!backend) return @@ -371,7 +371,7 @@ ApplicationWindow { } Button { - text: "WORLD" + text: qsTr("WORLD") highlighted: activeQuickFilter === "world" onClicked: { if (!backend) return @@ -387,7 +387,7 @@ ApplicationWindow { Label { Layout.fillWidth: true visible: backend && backend.lastError.length > 0 - text: backend ? ("Fehler: " + backend.lastError) : "" + text: backend ? (qsTr("Error: ") + backend.lastError) : "" color: warn elide: Text.ElideRight font.pixelSize: 12 @@ -473,7 +473,7 @@ ApplicationWindow { Label { anchors.centerIn: parent - text: "FM" + text: qsTr("FM") color: "#d8ecff" font.bold: true visible: !parent.children[0].visible @@ -496,7 +496,7 @@ ApplicationWindow { Label { Layout.fillWidth: true text: stationCard.modelData.country + " • " - + (stationCard.modelData.bitrate > 0 ? stationCard.modelData.bitrate + " kbps" : "Stream") + + (stationCard.modelData.bitrate > 0 ? stationCard.modelData.bitrate + " kbps" : qsTr("Stream")) color: textMuted font.pixelSize: 11 elide: Text.ElideRight @@ -510,7 +510,7 @@ ApplicationWindow { onClicked: { if (!backend) return backend.toggleFavoriteById(stationCard.modelData.uuid, stationCard.modelData.urlResolved) - toast(stationCard.modelData.isFavorite ? "Aus Favoriten entfernt" : "Zu Favoriten hinzugefügt") + toast(stationCard.modelData.isFavorite ? qsTr("Removed from favorites") : qsTr("Added to favorites")) } } @@ -538,13 +538,13 @@ ApplicationWindow { visible: stationList.count === 0 Label { - text: "Noch keine Sender geladen" + text: qsTr("No stations loaded yet") color: textMain font.bold: true } Label { - text: "Lade DE/NL oder nutze die Suche" + text: qsTr("Load DE/NL stations or use search") color: textMuted } } @@ -588,7 +588,7 @@ ApplicationWindow { Label { Layout.fillWidth: true - text: backend && backend.player ? (backend.player.currentStationName || "Kein Sender ausgewählt") : "Kein Sender ausgewählt" + text: backend && backend.player ? (backend.player.currentStationName || qsTr("No station selected")) : qsTr("No station selected") color: textMain font.pixelSize: 16 font.bold: true @@ -598,8 +598,8 @@ ApplicationWindow { Label { Layout.fillWidth: true text: backend && backend.player && backend.player.currentNowPlaying.length > 0 - ? ("Jetzt läuft: " + backend.player.currentNowPlaying) - : "Jetzt läuft: Keine Titelinfo" + ? (qsTr("Now playing: ") + backend.player.currentNowPlaying) + : qsTr("Now playing: No track info") color: textMuted font.pixelSize: 12 elide: Text.ElideRight @@ -652,7 +652,7 @@ ApplicationWindow { } Label { - text: "Lautstärke" + text: qsTr("Volume") color: textMuted } @@ -699,7 +699,7 @@ ApplicationWindow { } Label { - text: "Lade Sender..." + text: qsTr("Loading stations...") color: textMain anchors.horizontalCenter: parent.horizontalCenter } @@ -715,7 +715,7 @@ ApplicationWindow { Dialog { id: addDialog - title: "Sender manuell hinzufügen" + title: qsTr("Add station manually") modal: true anchors.centerIn: parent width: compactMode ? 320 : 420 @@ -723,15 +723,15 @@ ApplicationWindow { contentItem: ColumnLayout { spacing: 8 - TextField { id: manualName; Layout.fillWidth: true; placeholderText: "Name" } - TextField { id: manualUrl; Layout.fillWidth: true; placeholderText: "Stream URL (http/https)" } - TextField { id: manualCountry; Layout.fillWidth: true; placeholderText: "Land (optional)" } + TextField { id: manualName; Layout.fillWidth: true; placeholderText: qsTr("Name") } + TextField { id: manualUrl; Layout.fillWidth: true; placeholderText: qsTr("Stream URL (http/https)") } + TextField { id: manualCountry; Layout.fillWidth: true; placeholderText: qsTr("Country (optional)") } } onAccepted: { if (backend) { backend.addManualStation(manualName.text, manualUrl.text, manualCountry.text) - toast("Sender hinzugefügt") + toast(qsTr("Station added")) } manualName.text = "" manualUrl.text = "" @@ -768,7 +768,7 @@ ApplicationWindow { Label { Layout.alignment: Qt.AlignHCenter - text: "BearWave" + text: qsTr("BearWave") color: textMain font.bold: true font.pixelSize: 26 @@ -776,7 +776,7 @@ ApplicationWindow { Label { Layout.alignment: Qt.AlignHCenter - text: "Internet Radio Player for KDE" + text: qsTr("Internet Radio Player for KDE") color: textMuted font.pixelSize: 14 } @@ -791,7 +791,7 @@ ApplicationWindow { Label { Layout.alignment: Qt.AlignHCenter - text: "Autor: Sebastian Palencsár" + text: qsTr("Author: Sebastian Palencsár") color: textMain font.bold: true font.pixelSize: 14 @@ -809,7 +809,7 @@ ApplicationWindow { icon.height: 19 onClicked: Qt.openUrlExternally("https://palencsar.pro") ToolTip.visible: hovered - ToolTip.text: "Website öffnen" + ToolTip.text: qsTr("Open website") background: Rectangle { radius: 20 color: parent.hovered ? "#274261" : "#1f3147" @@ -826,7 +826,7 @@ ApplicationWindow { icon.height: 19 onClicked: Qt.openUrlExternally("https://github.com/spalencsar") ToolTip.visible: hovered - ToolTip.text: "GitHub öffnen" + ToolTip.text: qsTr("Open GitHub") background: Rectangle { radius: 20 color: parent.hovered ? "#274261" : "#1f3147" @@ -843,7 +843,7 @@ ApplicationWindow { icon.height: 19 onClicked: Qt.openUrlExternally("https://www.linkedin.com/in/spalencsar/") ToolTip.visible: hovered - ToolTip.text: "LinkedIn öffnen" + ToolTip.text: qsTr("Open LinkedIn") background: Rectangle { radius: 20 color: parent.hovered ? "#274261" : "#1f3147" @@ -864,12 +864,12 @@ ApplicationWindow { Layout.fillWidth: true Label { Layout.fillWidth: true - text: "MIT License" + text: qsTr("MIT License") color: textMain font.bold: true } Label { - text: "Copyright (c) 2026" + text: qsTr("Copyright (c) 2026") color: textMuted font.pixelSize: 11 } diff --git a/src/radiobackend.cpp b/src/radiobackend.cpp index 256750a..5feccc6 100644 --- a/src/radiobackend.cpp +++ b/src/radiobackend.cpp @@ -311,7 +311,7 @@ void RadioBackend::addManualStation(const QString &name, const QString &url, con station->setName(name.trimmed()); station->setUrl(url.trimmed()); station->setUrlResolved(url.trimmed()); - station->setCountry(country.trimmed().isEmpty() ? QStringLiteral("Manual") : country.trimmed()); + station->setCountry(country.trimmed().isEmpty() ? tr("Manual") : country.trimmed()); station->setCodec(QStringLiteral("unknown")); station->setBitrate(0); station->setVotes(0); @@ -406,11 +406,11 @@ void RadioBackend::resumeLastStation() return; } QVariantMap station; - station.insert(QStringLiteral("name"), m_lastStationName.isEmpty() ? QStringLiteral("Zuletzt gehoert") : m_lastStationName); + station.insert(QStringLiteral("name"), m_lastStationName.isEmpty() ? tr("Last played") : m_lastStationName); station.insert(QStringLiteral("urlResolved"), m_lastStationUrl); recordRecentStation(station); saveState(); - m_player->playUrl(m_lastStationUrl, m_lastStationName.isEmpty() ? QStringLiteral("Zuletzt gehört") : m_lastStationName); + m_player->playUrl(m_lastStationUrl, m_lastStationName.isEmpty() ? tr("Last played") : m_lastStationName); } void RadioBackend::loadFavorites() diff --git a/src/radiobrowser.cpp b/src/radiobrowser.cpp index fe2896a..cc9c1d7 100644 --- a/src/radiobrowser.cpp +++ b/src/radiobrowser.cpp @@ -98,7 +98,7 @@ void RadioBrowser::onReplyFinished() QNetworkReply *reply = qobject_cast(sender()); if (!reply) { - emit error("Network error"); + emit error(tr("Network error")); return; } diff --git a/translations/bearwave_de.ts b/translations/bearwave_de.ts new file mode 100644 index 0000000..fdbfef7 --- /dev/null +++ b/translations/bearwave_de.ts @@ -0,0 +1,233 @@ + + + + + Main + + BearWave + BearWave + + + Pause + Pause + + + Play + Wiedergabe + + + Hide + Verstecken + + + Show + Zeigen + + + Quit + Beenden + + + Top + Top + + + DE + DE + + + NL + NL + + + Favorites + Favoriten + + + Manual + + Manuell + + + + About + About + + + Resume + Fortsetzen + + + Search stations (name, genre, country) + Sender suchen (Name, Genre, Land) + + + Search + Suche + + + Sort A-Z + Sort A-Z + + + Sort Bitrate + Sort Bitrate + + + Sort Votes + Sort Votes + + + Tip: you are not limited to DE/NL. Search worldwide by country, genre, or station name. + Tipp: Du bist nicht auf DE/NL begrenzt. Suche weltweit nach Ländern, Genres oder Sendernamen. + + + Genre: + Genre: + + + Rock + Rock + + + News + News + + + Jazz + Jazz + + + Country: + Land: + + + US + US + + + UK + UK + + + FR + FR + + + WORLD + WELT + + + Error: + Fehler: + + + FM + FM + + + Stream + Stream + + + Removed from favorites + Aus Favoriten entfernt + + + Added to favorites + Zu Favoriten hinzugefügt + + + No stations loaded yet + Noch keine Sender geladen + + + Load DE/NL stations or use search + Lade DE/NL oder nutze die Suche + + + No station selected + Kein Sender ausgewählt + + + Now playing: + Jetzt läuft: + + + Now playing: No track info + Jetzt läuft: Keine Titelinfo + + + Volume + Lautstärke + + + Loading stations... + Lade Sender... + + + Add station manually + Sender manuell hinzufügen + + + Name + Name + + + Stream URL (http/https) + Stream URL (http/https) + + + Country (optional) + Land (optional) + + + Station added + Sender hinzugefügt + + + Internet Radio Player for KDE + Internet Radio Player für KDE + + + Author: Sebastian Palencsár + Autor: Sebastian Palencsár + + + Open website + Website öffnen + + + Open GitHub + GitHub öffnen + + + Open LinkedIn + LinkedIn öffnen + + + MIT License + MIT License + + + Copyright (c) 2026 + Copyright (c) 2026 + + + + RadioBackend + + Manual + Manuell + + + Last played + Zuletzt gehört + + + + RadioBrowser + + Network error + Netzwerkfehler + + +