forked from Shinonome/dots-hyprland
translations: make language change happen live
This commit is contained in:
@@ -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.")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user