Add English and German UI localization

This commit is contained in:
Sebastian Palencsar
2026-05-20 18:33:53 +02:00
parent 8548fc5ac6
commit 1038f041c7
8 changed files with 319 additions and 63 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -1,8 +1,10 @@
#include <QApplication>
#include <QDBusConnection>
#include <QIcon>
#include <QLocale>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QTranslator>
#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;

View File

@@ -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
}

View File

@@ -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()

View File

@@ -98,7 +98,7 @@ void RadioBrowser::onReplyFinished()
QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
if (!reply) {
emit error("Network error");
emit error(tr("Network error"));
return;
}

233
translations/bearwave_de.ts Normal file
View File

@@ -0,0 +1,233 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="de_DE">
<context>
<name>Main</name>
<message>
<source>BearWave</source>
<translation>BearWave</translation>
</message>
<message>
<source>Pause</source>
<translation>Pause</translation>
</message>
<message>
<source>Play</source>
<translation>Wiedergabe</translation>
</message>
<message>
<source>Hide</source>
<translation>Verstecken</translation>
</message>
<message>
<source>Show</source>
<translation>Zeigen</translation>
</message>
<message>
<source>Quit</source>
<translation>Beenden</translation>
</message>
<message>
<source>Top</source>
<translation>Top</translation>
</message>
<message>
<source>DE</source>
<translation>DE</translation>
</message>
<message>
<source>NL</source>
<translation>NL</translation>
</message>
<message>
<source>Favorites</source>
<translation>Favoriten</translation>
</message>
<message>
<source>Manual +</source>
<translation>Manuell +</translation>
</message>
<message>
<source>About</source>
<translation>About</translation>
</message>
<message>
<source>Resume</source>
<translation>Fortsetzen</translation>
</message>
<message>
<source>Search stations (name, genre, country)</source>
<translation>Sender suchen (Name, Genre, Land)</translation>
</message>
<message>
<source>Search</source>
<translation>Suche</translation>
</message>
<message>
<source>Sort A-Z</source>
<translation>Sort A-Z</translation>
</message>
<message>
<source>Sort Bitrate</source>
<translation>Sort Bitrate</translation>
</message>
<message>
<source>Sort Votes</source>
<translation>Sort Votes</translation>
</message>
<message>
<source>Tip: you are not limited to DE/NL. Search worldwide by country, genre, or station name.</source>
<translation>Tipp: Du bist nicht auf DE/NL begrenzt. Suche weltweit nach Ländern, Genres oder Sendernamen.</translation>
</message>
<message>
<source>Genre:</source>
<translation>Genre:</translation>
</message>
<message>
<source>Rock</source>
<translation>Rock</translation>
</message>
<message>
<source>News</source>
<translation>News</translation>
</message>
<message>
<source>Jazz</source>
<translation>Jazz</translation>
</message>
<message>
<source>Country:</source>
<translation>Land:</translation>
</message>
<message>
<source>US</source>
<translation>US</translation>
</message>
<message>
<source>UK</source>
<translation>UK</translation>
</message>
<message>
<source>FR</source>
<translation>FR</translation>
</message>
<message>
<source>WORLD</source>
<translation>WELT</translation>
</message>
<message>
<source>Error: </source>
<translation>Fehler: </translation>
</message>
<message>
<source>FM</source>
<translation>FM</translation>
</message>
<message>
<source>Stream</source>
<translation>Stream</translation>
</message>
<message>
<source>Removed from favorites</source>
<translation>Aus Favoriten entfernt</translation>
</message>
<message>
<source>Added to favorites</source>
<translation>Zu Favoriten hinzugefügt</translation>
</message>
<message>
<source>No stations loaded yet</source>
<translation>Noch keine Sender geladen</translation>
</message>
<message>
<source>Load DE/NL stations or use search</source>
<translation>Lade DE/NL oder nutze die Suche</translation>
</message>
<message>
<source>No station selected</source>
<translation>Kein Sender ausgewählt</translation>
</message>
<message>
<source>Now playing: </source>
<translation>Jetzt läuft: </translation>
</message>
<message>
<source>Now playing: No track info</source>
<translation>Jetzt läuft: Keine Titelinfo</translation>
</message>
<message>
<source>Volume</source>
<translation>Lautstärke</translation>
</message>
<message>
<source>Loading stations...</source>
<translation>Lade Sender...</translation>
</message>
<message>
<source>Add station manually</source>
<translation>Sender manuell hinzufügen</translation>
</message>
<message>
<source>Name</source>
<translation>Name</translation>
</message>
<message>
<source>Stream URL (http/https)</source>
<translation>Stream URL (http/https)</translation>
</message>
<message>
<source>Country (optional)</source>
<translation>Land (optional)</translation>
</message>
<message>
<source>Station added</source>
<translation>Sender hinzugefügt</translation>
</message>
<message>
<source>Internet Radio Player for KDE</source>
<translation>Internet Radio Player für KDE</translation>
</message>
<message>
<source>Author: Sebastian Palencsár</source>
<translation>Autor: Sebastian Palencsár</translation>
</message>
<message>
<source>Open website</source>
<translation>Website öffnen</translation>
</message>
<message>
<source>Open GitHub</source>
<translation>GitHub öffnen</translation>
</message>
<message>
<source>Open LinkedIn</source>
<translation>LinkedIn öffnen</translation>
</message>
<message>
<source>MIT License</source>
<translation>MIT License</translation>
</message>
<message>
<source>Copyright (c) 2026</source>
<translation>Copyright (c) 2026</translation>
</message>
</context>
<context>
<name>RadioBackend</name>
<message>
<source>Manual</source>
<translation>Manuell</translation>
</message>
<message>
<source>Last played</source>
<translation>Zuletzt gehört</translation>
</message>
</context>
<context>
<name>RadioBrowser</name>
<message>
<source>Network error</source>
<translation>Netzwerkfehler</translation>
</message>
</context>
</TS>