translations: make language change happen live

This commit is contained in:
end-4
2025-09-05 23:31:08 +02:00
parent d7382db669
commit b0acc5a68e
4 changed files with 145 additions and 212 deletions
+52 -124
View File
@@ -4,172 +4,100 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.modules.common import qs.modules.common
import qs.modules.common.functions
Singleton { Singleton {
id: root id: root
property var translations: ({}) property var translations: ({})
property string currentLanguage: "en_US"
property var availableLanguages: ["en_US"] property var availableLanguages: ["en_US"]
property bool isScanning: false property bool isScanning: scanLanguagesProcess.running
property bool isLoading: false property bool isLoading: false
property string translationKeepSuffix: "/*keep*/"
property string languageCode: {
var configLang = Config?.options.language.ui ?? "auto";
if (configLang !== "auto")
return configLang;
return Qt.locale().name;
}
Process { Process {
id: scanLanguagesProcess id: scanLanguagesProcess
command: ["find", Qt.resolvedUrl(Directories.config + "/quickshell/translations/").toString().replace("file://", ""), "-name", "*.json", "-exec", "basename", "{}", ".json", ";"] command: ["find", FileUtils.trimFileProtocol(Qt.resolvedUrl(Directories.config + "/quickshell/translations/").toString()), "-name", "*.json", "-exec", "basename", "{}", ".json", ";"]
running: false running: true
stdout: SplitParser { stdout: SplitParser {
onRead: data => { onRead: data => {
if (data.trim().length === 0) return if (data.trim().length === 0)
return;
var files = data.trim().split('\n') var files = data.trim().split('\n');
for (var i = 0; i < files.length; i++) { for (var i = 0; i < files.length; i++) {
var lang = files[i].trim() var lang = files[i].trim();
if (lang.length > 0 && root.availableLanguages.indexOf(lang) === -1) { if (lang.length > 0 && root.availableLanguages.indexOf(lang) === -1) {
root.availableLanguages.push(lang) root.availableLanguages.push(lang);
} }
} }
} }
} }
onExited: (exitCode, exitStatus) => { onExited: (exitCode, exitStatus) => {
root.isScanning = false root.availableLanguages = [...root.availableLanguages] // Forcibly emit change
if (exitCode !== 0) { if (exitCode !== 0) {
root.availableLanguages = ["en_US"] root.availableLanguages = ["en_US"];
} }
root.loadTranslations() // TODO: notify and offer to translate when translation not available
} }
} }
onLanguageCodeChanged: {
translationFileView.reload();
}
FileView { FileView {
id: translationFileView id: translationFileView
path: root.languageCode?.length > 0 ? Qt.resolvedUrl(Directories.config + "/quickshell/translations/" + root.languageCode + ".json") : ""
onLoaded: { onLoaded: {
var textContent = "" var textContent = "";
try { try {
textContent = text() textContent = text();
var jsonData = JSON.parse(textContent);
root.translations = jsonData;
} catch (e) { } catch (e) {
root.translations = {} console.log("[Translation] Failed to load translations:", e);
root.isLoading = false root.translations = {};
return
}
if (textContent.length === 0) {
root.translations = {}
root.isLoading = false
return
}
try {
var jsonData = JSON.parse(textContent)
root.translations = jsonData
root.isLoading = false
} catch (e) {
root.translations = {}
root.isLoading = false
} }
root.isLoading = false;
} }
onLoadFailed: (error) => { onLoadFailed: error => {
root.translations = {} root.translations = {};
root.isLoading = false root.isLoading = false;
} }
} }
function detectSystemLanguage() {
var locale = Qt.locale().name
return locale
}
function getLanguageCode() {
var configLang = "auto"
try {
configLang = Config.options.language.ui
} catch (e) {
configLang = "auto"
}
if (configLang === "auto") {
return detectSystemLanguage()
} else {
if (root.availableLanguages.indexOf(configLang) !== -1) {
return configLang
} else {
return detectSystemLanguage()
}
}
}
function loadTranslations() {
if (root.isScanning) {
return
}
var targetLang = getLanguageCode()
root.currentLanguage = targetLang
// Use empty translations for English (default language)
if (targetLang === "en_US" || targetLang === "en") {
root.translations = {}
return
}
// Check if target language is available
if (root.availableLanguages.indexOf(targetLang) === -1) {
root.currentLanguage = "en_US"
root.translations = {}
return
}
// Load translation file
root.isLoading = true
var translationsPath = Qt.resolvedUrl(Directories.config + "/quickshell/translations/" + targetLang + ".json")
translationFileView.path = translationsPath
}
function tr(text) { function tr(text) {
if (!text) { if (!text)
return "" return "";
} var key = text.toString();
if (root.isLoading)
var key = text.toString() return key;
if (root.isLoading) {
return key
}
if (root.currentLanguage === "en_US" || root.currentLanguage === "en" || !root.translations) {
return key
}
if (root.translations.hasOwnProperty(key)) { if (root.translations.hasOwnProperty(key)) {
var translation = root.translations[key] var translation = root.translations[key].toString().trim();
if (translation && translation.toString().trim().length > 0) { if (translation.length === 0)
var str = translation.toString().trim() return key;
if (str.endsWith("/*keep*/")) {
return str.substring(0, str.length - 8).trim() if (translation.endsWith(root.translationKeepSuffix)) {
} else { translation = translation.substring(0, translation.length - root.translationKeepSuffix.length).trim();
return str
}
} else {
return translation.toString()
} }
return translation;
} }
return key // Fallback to key name return key; // Fallback to key name
}
function reloadTranslations() {
root.scanLanguages()
}
function scanLanguages() {
var translationsDir = Qt.resolvedUrl(Directories.config + "/quickshell/translations/").toString().replace("file://", "")
root.isScanning = true
scanLanguagesProcess.running = true
}
Component.onCompleted: {
root.scanLanguages()
} }
} }
@@ -115,7 +115,6 @@ ContentPage {
to: 150 to: 150
stepSize: 1 stepSize: 1
onValueChanged: { onValueChanged: {
console.log(value/100)
Config.options.background.parallax.workspaceZoom = value / 100; Config.options.background.parallax.workspaceZoom = value / 100;
} }
} }
@@ -403,38 +402,19 @@ ContentPage {
currentValue: Config.options.language.ui currentValue: Config.options.language.ui
onSelected: newValue => { onSelected: newValue => {
Config.options.language.ui = newValue; Config.options.language.ui = newValue;
reloadNotice.visible = true;
} }
options: { options: [
var baseOptions = [ {
{ displayName: Translation.tr("Auto (System)"),
displayName: Translation.tr("Auto (System)"), value: "auto"
value: "auto" },
} ...Translation.availableLanguages.map(lang => {
]; return {
displayName: lang.replace('_', '-'),
// Generate language options from available languages
// Intl.DisplayNames is not used. Show the language code with underscore replaced by hyphen.
for (var i = 0; i < Translation.availableLanguages.length; i++) {
var lang = Translation.availableLanguages[i];
var displayName = lang.replace('_', '-');
baseOptions.push({
displayName: displayName,
value: lang value: lang
}); };
} })
]
return baseOptions;
}
}
NoticeBox {
id: reloadNotice
visible: false
Layout.topMargin: 8
Layout.fillWidth: true
text: Translation.tr("Language setting saved. Please restart Quickshell (Ctrl+Super+R) to apply the new language.")
} }
} }
} }
+3 -1
View File
@@ -211,7 +211,9 @@ ApplicationWindow {
opacity: 1.0 opacity: 1.0
active: Config.ready active: Config.ready
source: root.pages[0].component Component.onCompleted: {
source = root.pages[0].component
}
Connections { Connections {
target: root target: root
+71 -48
View File
@@ -12,7 +12,6 @@ import QtQuick.Layouts
import QtQuick.Window import QtQuick.Window
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Hyprland
import qs import qs
import qs.services import qs.services
import qs.modules.common import qs.modules.common
@@ -27,13 +26,8 @@ ApplicationWindow {
property bool showNextTime: false property bool showNextTime: false
visible: true visible: true
onClosing: { onClosing: {
Quickshell.execDetached([ Quickshell.execDetached(["notify-send", Translation.tr("Welcome app"), Translation.tr("Enjoy! You can reopen the welcome app any time with <tt>Super+Shift+Alt+/</tt>. To open the settings app, hit <tt>Super+I</tt>"), "-a", "Shell"]);
"notify-send", Qt.quit();
Translation.tr("Welcome app"),
Translation.tr("Enjoy! You can reopen the welcome app any time with <tt>Super+Shift+Alt+/</tt>. To open the settings app, hit <tt>Super+I</tt>"),
"-a", "Shell"
])
Qt.quit()
} }
title: Translation.tr("illogical-impulse Welcome") title: Translation.tr("illogical-impulse Welcome")
@@ -118,6 +112,7 @@ ApplicationWindow {
} }
} }
} }
Rectangle { Rectangle {
// Content container // Content container
color: Appearance.m3colors.m3surfaceContainerLow color: Appearance.m3colors.m3surfaceContainerLow
@@ -132,11 +127,69 @@ ApplicationWindow {
anchors.fill: parent anchors.fill: parent
ContentSection { ContentSection {
Layout.fillWidth: true
icon: "language"
title: Translation.tr("Language")
ConfigSelectionArray {
id: languageSelector
currentValue: Config.options.language.ui
onSelected: newValue => {
Config.options.language.ui = newValue;
}
options: [
{
displayName: Translation.tr("Auto (System)"),
value: "auto"
},
...Translation.availableLanguages.map(lang => {
return {
displayName: lang.replace('_', '-'),
value: lang
};
})]
}
}
ContentSection {
icon: "screenshot_monitor"
title: Translation.tr("Bar") title: Translation.tr("Bar")
ConfigRow { ConfigRow {
ContentSubsection { ContentSubsection {
title: "Corner style" title: Translation.tr("Bar position")
ConfigSelectionArray {
currentValue: (Config.options.bar.bottom ? 1 : 0) | (Config.options.bar.vertical ? 2 : 0)
onSelected: newValue => {
Config.options.bar.bottom = (newValue & 1) !== 0;
Config.options.bar.vertical = (newValue & 2) !== 0;
}
options: [
{
displayName: Translation.tr("Top"),
icon: "arrow_upward",
value: 0 // bottom: false, vertical: false
},
{
displayName: Translation.tr("Left"),
icon: "arrow_back",
value: 2 // bottom: false, vertical: true
},
{
displayName: Translation.tr("Bottom"),
icon: "arrow_downward",
value: 1 // bottom: true, vertical: false
},
{
displayName: Translation.tr("Right"),
icon: "arrow_forward",
value: 3 // bottom: true, vertical: true
}
]
}
}
ContentSubsection {
title: Translation.tr("Bar style")
ConfigSelectionArray { ConfigSelectionArray {
currentValue: Config.options.bar.cornerStyle currentValue: Config.options.bar.cornerStyle
@@ -146,64 +199,31 @@ ApplicationWindow {
options: [ options: [
{ {
displayName: Translation.tr("Hug"), displayName: Translation.tr("Hug"),
icon: "line_curve",
value: 0 value: 0
}, },
{ {
displayName: Translation.tr("Float"), displayName: Translation.tr("Float"),
icon: "page_header",
value: 1 value: 1
}, },
{ {
displayName: Translation.tr("Plain rectangle"), displayName: Translation.tr("Rect"),
icon: "toolbar",
value: 2 value: 2
} }
] ]
} }
} }
ContentSubsection {
title: "Bar layout"
ConfigSelectionArray {
currentValue: Config.options.bar.vertical
onSelected: newValue => {
Config.options.bar.vertical = newValue;
}
options: [
{
displayName: Translation.tr("Horizontal"),
value: false
},
{
displayName: Translation.tr("Vertical"),
value: true
},
]
}
}
}
ConfigRow {
ConfigSwitch {
text: Translation.tr("Automatically hide")
checked: Config.options.bar.autoHide.enable
onCheckedChanged: {
Config.options.bar.autoHide.enable = checked;
}
}
ConfigSwitch {
text: Translation.tr("Place at the bottom/right")
checked: Config.options.bar.bottom
onCheckedChanged: {
Config.options.bar.bottom = checked;
}
}
} }
} }
ContentSection { ContentSection {
icon: "format_paint"
title: Translation.tr("Style & wallpaper") title: Translation.tr("Style & wallpaper")
ButtonGroup { ButtonGroup {
Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter
LightDarkPreferenceButton { LightDarkPreferenceButton {
dark: false dark: false
} }
@@ -272,6 +292,7 @@ ApplicationWindow {
} }
ContentSection { ContentSection {
icon: "rule"
title: Translation.tr("Policies") title: Translation.tr("Policies")
ConfigRow { ConfigRow {
@@ -330,6 +351,7 @@ ApplicationWindow {
} }
ContentSection { ContentSection {
icon: "info"
title: Translation.tr("Info") title: Translation.tr("Info")
Flow { Flow {
@@ -384,6 +406,7 @@ ApplicationWindow {
} }
ContentSection { ContentSection {
icon: "monitoring"
title: Translation.tr("Useless buttons") title: Translation.tr("Useless buttons")
Flow { Flow {