From b4563194341fd02717d413b02c293247f5712903 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Thu, 3 Apr 2025 14:25:45 +0200 Subject: [PATCH] Read the user's preferred language from the OS (#382) --- .../Layout/MainLayout.razor.cs | 6 ++ app/MindWork AI Studio/Pages/About.razor | 1 + .../plugin.lua | 4 ++ .../plugin.lua | 4 ++ .../Tools/PluginSystem/PluginLanguage.cs | 64 ++++++++++++++++++- .../Tools/Services/RustService.OS.cs | 16 +++++ runtime/Cargo.lock | 10 +++ runtime/Cargo.toml | 1 + runtime/src/environment.rs | 9 +++ runtime/src/runtime_api.rs | 1 + 10 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 app/MindWork AI Studio/Tools/Services/RustService.OS.cs diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs index 4c45a3b9..04abf62c 100644 --- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs +++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs @@ -79,6 +79,12 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis SettingsManager.DataDirectory = dataDir; Directory.CreateDirectory(SettingsManager.DataDirectory); + // + // Read the user language from Rust: + // + var userLanguage = await this.RustService.ReadUserLanguage(); + this.Logger.LogInformation($"The user language is: '{userLanguage}'"); + // Ensure that all settings are loaded: await this.SettingsManager.LoadSettings(); diff --git a/app/MindWork AI Studio/Pages/About.razor b/app/MindWork AI Studio/Pages/About.razor index cfc2e559..32c8b5df 100644 --- a/app/MindWork AI Studio/Pages/About.razor +++ b/app/MindWork AI Studio/Pages/About.razor @@ -113,6 +113,7 @@ + diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index ab05f0bc..634c8e74 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -40,6 +40,10 @@ IS_MAINTAINED = true -- When the plugin is deprecated, this message will be shown to users: DEPRECATION_MESSAGE = "" +-- The IETF BCP 47 tag for the language. It's the ISO 639 language +-- code followed by the ISO 3166-1 country code: +IETF_TAG = "de-DE" + UI_TEXT_CONTENT = { HOME = CONTENT_HOME, } diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index d6c98d49..78f66474 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -40,6 +40,10 @@ IS_MAINTAINED = true -- When the plugin is deprecated, this message will be shown to users: DEPRECATION_MESSAGE = "" +-- The IETF BCP 47 tag for the language. It's the ISO 639 language +-- code followed by the ISO 3166-1 country code: +IETF_TAG = "en-US" + UI_TEXT_CONTENT = { HOME = CONTENT_HOME, } diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginLanguage.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginLanguage.cs index e39a4813..384d81eb 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginLanguage.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginLanguage.cs @@ -6,12 +6,16 @@ public sealed class PluginLanguage : PluginBase, ILanguagePlugin { private readonly Dictionary content = []; private readonly List otherLanguagePlugins = []; + private readonly string langCultureTag; private ILanguagePlugin? baseLanguage; public PluginLanguage(bool isInternal, LuaState state, PluginType type) : base(isInternal, state, type) { - if (this.TryInitUITextContent(out var issue, out var readContent)) + if(!this.TryInitIETFTag(out var issue, out this.langCultureTag)) + this.pluginIssues.Add(issue); + + if (this.TryInitUITextContent(out issue, out var readContent)) this.content = readContent; else this.pluginIssues.Add(issue); @@ -65,4 +69,62 @@ public sealed class PluginLanguage : PluginBase, ILanguagePlugin value = string.Empty; return false; } + + /// + /// Tries to initialize the IETF tag. + /// + /// The error message, when the IETF tag could not be read. + /// The read IETF tag. + /// True, when the IETF tag could be read, false otherwise. + private bool TryInitIETFTag(out string message, out string readLangCultureTag) + { + if (!this.state.Environment["IETF_TAG"].TryRead(out readLangCultureTag)) + { + message = "The field IETF_TAG does not exist or is not a valid string."; + readLangCultureTag = string.Empty; + return false; + } + + if (string.IsNullOrWhiteSpace(readLangCultureTag)) + { + message = "The field IETF_TAG is empty. Use a valid IETF tag like 'en-US'. The first part is the language, the second part is the country code."; + readLangCultureTag = string.Empty; + return false; + } + + if (readLangCultureTag.Length != 5) + { + message = "The field IETF_TAG is not a valid IETF tag. Use a valid IETF tag like 'en-US'. The first part is the language, the second part is the country code."; + readLangCultureTag = string.Empty; + return false; + } + + if (readLangCultureTag[2] != '-') + { + message = "The field IETF_TAG is not a valid IETF tag. Use a valid IETF tag like 'en-US'. The first part is the language, the second part is the country code."; + readLangCultureTag = string.Empty; + return false; + } + + // Check the first part consists of only lower case letters: + for (var i = 0; i < 2; i++) + if (!char.IsLower(readLangCultureTag[i])) + { + message = "The field IETF_TAG is not a valid IETF tag. Use a valid IETF tag like 'en-US'. The first part is the language, the second part is the country code."; + readLangCultureTag = string.Empty; + return false; + } + + // Check the second part consists of only upper case letters: + for (var i = 3; i < 5; i++) + if (!char.IsUpper(readLangCultureTag[i])) + { + message = "The field IETF_TAG is not a valid IETF tag. Use a valid IETF tag like 'en-US'. The first part is the language, the second part is the country code."; + readLangCultureTag = string.Empty; + return false; + } + + message = string.Empty; + return true; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Services/RustService.OS.cs b/app/MindWork AI Studio/Tools/Services/RustService.OS.cs new file mode 100644 index 00000000..215b3a02 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Services/RustService.OS.cs @@ -0,0 +1,16 @@ +namespace AIStudio.Tools.Services; + +public sealed partial class RustService +{ + public async Task ReadUserLanguage() + { + var response = await this.http.GetAsync("/system/language"); + if (!response.IsSuccessStatusCode) + { + this.logger!.LogError($"Failed to read the user language from Rust: '{response.StatusCode}'"); + return string.Empty; + } + + return await response.Content.ReadAsStringAsync(); + } +} \ No newline at end of file diff --git a/runtime/Cargo.lock b/runtime/Cargo.lock index 2ff68b78..38144bea 100644 --- a/runtime/Cargo.lock +++ b/runtime/Cargo.lock @@ -2684,6 +2684,7 @@ dependencies = [ "serde", "serde_json", "sha2", + "sys-locale", "tauri", "tauri-build", "tauri-plugin-window-state", @@ -4759,6 +4760,15 @@ dependencies = [ "syn 2.0.93", ] +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + [[package]] name = "system-configuration" version = "0.5.1" diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 61e62fbb..5f075adf 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -36,6 +36,7 @@ rcgen = { version = "0.13.2", features = ["pem"] } file-format = "0.26.0" calamine = "0.26.1" pdfium-render = "0.8.29" +sys-locale = "0.3.2" # Fixes security vulnerability downstream, where the upstream is not fixed yet: url = "2.5" diff --git a/runtime/src/environment.rs b/runtime/src/environment.rs index 53e8f8fe..af3435b1 100644 --- a/runtime/src/environment.rs +++ b/runtime/src/environment.rs @@ -1,5 +1,6 @@ use std::sync::OnceLock; use rocket::get; +use sys_locale::get_locale; use crate::api_token::APIToken; /// The data directory where the application stores its data. @@ -34,4 +35,12 @@ pub fn is_dev() -> bool { /// Returns true if the application is running in production mode. pub fn is_prod() -> bool { !is_dev() +} + +#[get("/system/language")] +pub fn read_user_language(_token: APIToken) -> String { + get_locale().unwrap_or_else(|| { + log::warn!("Could not determine the system language. Use default 'en-US'."); + String::from("en-US") + }) } \ No newline at end of file diff --git a/runtime/src/runtime_api.rs b/runtime/src/runtime_api.rs index 0f383cc1..bf3fa249 100644 --- a/runtime/src/runtime_api.rs +++ b/runtime/src/runtime_api.rs @@ -77,6 +77,7 @@ pub fn start_runtime_api() { crate::secret::delete_secret, crate::environment::get_data_directory, crate::environment::get_config_directory, + crate::environment::read_user_language, crate::file_data::extract_data, crate::log::get_log_paths, ])