diff --git a/Commons/I18n.qml b/Commons/I18n.qml index f05ccb59b..fd8d9095f 100644 --- a/Commons/I18n.qml +++ b/Commons/I18n.qml @@ -39,7 +39,9 @@ Singleton { Logger.e("I18n", `Failed to scan translation directory`); // Fallback to default languages availableLanguages = ["en"]; - detectLanguage(); + if (!root.isLoaded) { + detectLanguage(); + } } } } @@ -64,8 +66,19 @@ Singleton { } } onLoadFailed: function (error) { - setLanguage("en"); - Logger.e("I18n", `Failed to load translation file: ${error}`); + if (root.langCode !== "en") { + // Fast-path language file not found, fall back to English directly + Logger.w("I18n", `Translation file for "${root.langCode}" not found, falling back to English`); + root.langCode = "en"; + root.fullLocaleCode = "en"; + root.locale = Qt.locale("en"); + loadTranslations(); + } else { + Logger.e("I18n", `Failed to load English translation file: ${error}`); + // English also failed - still emit signal to unblock startup + root.isLoaded = true; + root.translationsLoaded(); + } } } @@ -90,9 +103,47 @@ Singleton { Component.onCompleted: { Logger.i("I18n", "Service started"); + + // Fast path: immediately determine language and start loading translations + // without waiting for the directory scan + var lang = determineFastLanguage(); + langCode = lang.code; + fullLocaleCode = lang.fullLocale; + locale = Qt.locale(lang.fullLocale); + systemDetectedLangCode = lang.code; + Logger.i("I18n", `Fast path: loading "${lang.code}" (locale: "${lang.fullLocale}")`); + loadTranslations(); + + // Scan available languages in background (needed for settings UI language picker) scanAvailableLanguages(); } + // Determine the most likely language without waiting for directory scan + function determineFastLanguage() { + // Try user preference from Settings (defaults to "" if not yet loaded from disk) + var userLang = Settings.data.general.language; + if (userLang !== "") { + return { + code: userLang, + fullLocale: userLang + }; + } + + // Fall back to system locale + for (var i = 0; i < Qt.locale().uiLanguages.length; i++) { + var fullLang = Qt.locale().uiLanguages[i]; + var shortLang = fullLang.substring(0, 2); + return { + code: shortLang, + fullLocale: fullLang + }; + } + return { + code: "en", + fullLocale: "en" + }; + } + // ------------------------------------------- function scanAvailableLanguages() { Logger.d("I18n", "Scanning for available translation files..."); @@ -107,7 +158,9 @@ Singleton { if (!output || output.trim() === "") { Logger.w("I18n", "Empty directory listing output"); availableLanguages = ["en"]; - detectLanguage(); + if (!root.isLoaded) { + detectLanguage(); + } return; } @@ -141,13 +194,25 @@ Singleton { availableLanguages = languages; Logger.d("I18n", `Found ${languages.length} available languages: ${languages.join(', ')}`); - // Detect language after scanning + // If translations already loaded via fast path, only correct if user preference differs + if (root.isLoaded) { + var userLang = Settings.data.general.language; + if (userLang !== "" && userLang !== root.langCode && availableLanguages.includes(userLang)) { + Logger.i("I18n", `Correcting fast-path: switching to user preference "${userLang}"`); + setLanguage(userLang); + } + return; + } + + // Detect language after scanning (fallback if fast path hasn't completed yet) detectLanguage(); } catch (e) { Logger.e("I18n", `Failed to parse directory listing: ${e}`); // Fallback to default languages availableLanguages = ["en"]; - detectLanguage(); + if (!root.isLoaded) { + detectLanguage(); + } } } @@ -228,8 +293,8 @@ Singleton { isLoaded = false; Logger.d("I18n", `Loading translations: ${langCode}`); - // Only load fallback translations if we are not using english and english is available - if (langCode !== "en" && availableLanguages.includes("en")) { + // Load English fallback for non-English languages (English is always bundled) + if (langCode !== "en") { fallbackFileView.path = `file://${Quickshell.shellDir}/Assets/Translations/en.json`; } } diff --git a/Services/Noctalia/PluginRegistry.qml b/Services/Noctalia/PluginRegistry.qml index 36b25ebee..81a19478e 100644 --- a/Services/Noctalia/PluginRegistry.qml +++ b/Services/Noctalia/PluginRegistry.qml @@ -249,45 +249,62 @@ Singleton { checkProcess.running = true; } - // Scan plugin folder to discover installed plugins + // Scan plugin folder to discover installed plugins (single process reads all manifests) function scanPluginFolder() { Logger.i("PluginRegistry", "Scanning plugin folder:", root.pluginsDir); - var lsProcess = Qt.createQmlObject(` + var scanProcess = Qt.createQmlObject(` import QtQuick import Quickshell.Io Process { - command: ["sh", "-c", "ls -1 '${root.pluginsDir}' 2>/dev/null || true"] + command: ["sh", "-c", "for d in '${root.pluginsDir}'/*/; do [ -d \\"$d\\" ] || continue; [ -f \\"$d/manifest.json\\" ] || continue; echo \\"@@PLUGIN@@$(basename \\"$d\\")\\" ; cat \\"$d/manifest.json\\" ; done"] stdout: StdioCollector {} running: true } - `, root, "ScanPlugins"); + `, root, "ScanAllPlugins"); - lsProcess.exited.connect(function (exitCode) { - var output = String(lsProcess.stdout.text || ""); - var pluginDirs = output.trim().split('\n').filter(function (dir) { - return dir.length > 0; - }); + scanProcess.exited.connect(function (exitCode) { + var output = String(scanProcess.stdout.text || ""); + var sections = output.split("@@PLUGIN@@"); + var loadedCount = 0; - Logger.i("PluginRegistry", "Found", pluginDirs.length, "potential plugin directories"); + for (var i = 1; i < sections.length; i++) { + var section = sections[i]; + var newlineIdx = section.indexOf('\n'); + if (newlineIdx === -1) + continue; - if (pluginDirs.length === 0) { - // No plugins to load, emit signal immediately - root.pluginsChanged(); - lsProcess.destroy(); - return; + var pluginId = section.substring(0, newlineIdx).trim(); + var manifestJson = section.substring(newlineIdx + 1).trim(); + + if (!pluginId || !manifestJson) + continue; + + try { + var manifest = JSON.parse(manifestJson); + var validation = validateManifest(manifest); + + if (validation.valid) { + root.installedPlugins[pluginId] = manifest; + Logger.i("PluginRegistry", "Loaded plugin:", pluginId, "-", manifest.name); + + if (!root.pluginStates[pluginId]) { + root.pluginStates[pluginId] = { + enabled: false + }; + } + loadedCount++; + } else { + Logger.e("PluginRegistry", "Invalid manifest for", pluginId + ":", validation.error); + } + } catch (e) { + Logger.e("PluginRegistry", "Failed to parse manifest for", pluginId + ":", e.toString()); + } } - // Track how many manifests we're loading - root.pendingManifests = pluginDirs.length; - Logger.i("PluginRegistry", "Starting to load", root.pendingManifests, "manifests"); - - // Load each manifest - for (var i = 0; i < pluginDirs.length; i++) { - loadPluginManifest(pluginDirs[i]); - } - - lsProcess.destroy(); + Logger.i("PluginRegistry", "All plugin manifests loaded. Total plugins:", loadedCount); + root.pluginsChanged(); + scanProcess.destroy(); }); } diff --git a/shell.qml b/shell.qml index 0a8441216..d09bed161 100644 --- a/shell.qml +++ b/shell.qml @@ -89,20 +89,26 @@ ShellRoot { sourceComponent: Item { Component.onCompleted: { Logger.i("Shell", "---------------------------"); + + // Critical services needed for initial UI rendering WallpaperService.init(); ImageCacheService.init(); AppThemeService.init(); ColorSchemeService.init(); - LocationService.init(); - NightLightService.apply(); DarkModeService.init(); - HooksService.init(); - BluetoothService.init(); - IdleInhibitorService.init(); - PowerProfileService.init(); - HostService.init(); - GitHubService.init(); - SupporterService.init(); + + // Defer non-critical services to unblock first frame + Qt.callLater(function () { + LocationService.init(); + NightLightService.apply(); + HooksService.init(); + BluetoothService.init(); + IdleInhibitorService.init(); + PowerProfileService.init(); + HostService.init(); + GitHubService.init(); + SupporterService.init(); + }); delayedInitTimer.running = true; }