Read the user's preferred language from the OS (#382)
Some checks are pending
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Build app (linux-arm64) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions

This commit is contained in:
Thorsten Sommer 2025-04-03 14:25:45 +02:00 committed by GitHub
parent ceefc0114b
commit b456319434
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 115 additions and 1 deletions

View File

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

View File

@ -113,6 +113,7 @@
<ThirdPartyComponent Name="file-format" Developer="Mickaël Malécot & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/mmalecot/file-format/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/mmalecot/file-format" UseCase="This library is used to determine the file type of a file. This is necessary, e.g., when we want to stream a file."/>
<ThirdPartyComponent Name="calamine" Developer="Johann Tuffe, Joel Natividad, Eric Jolibois, Dmitriy & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/tafia/calamine/blob/master/LICENSE-MIT.md" RepositoryUrl="https://github.com/tafia/calamine" UseCase="This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat."/>
<ThirdPartyComponent Name="pdfium-render" Developer="Alastair Carey, Dorian Rudolph & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/ajrcarey/pdfium-render/blob/master/LICENSE.md" RepositoryUrl="https://github.com/ajrcarey/pdfium-render" UseCase="This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat."/>
<ThirdPartyComponent Name="sys-locale" Developer="1Password Team, ComplexSpaces & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/1Password/sys-locale/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/1Password/sys-locale" UseCase="This library is used to determine the language of the operating system. This is necessary to set the language of the user interface."/>
<ThirdPartyComponent Name="Lua-CSharp" Developer="Yusuke Nakada & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/nuskey8/Lua-CSharp/blob/main/LICENSE" RepositoryUrl="https://github.com/nuskey8/Lua-CSharp" UseCase="We use Lua as the language for plugins. Lua-CSharp lets Lua scripts communicate with AI Studio and vice versa. Thank you, Yusuke Nakada, for this great library." />
<ThirdPartyComponent Name="HtmlAgilityPack" Developer="ZZZ Projects & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/zzzprojects/html-agility-pack/blob/master/LICENSE" RepositoryUrl="https://github.com/zzzprojects/html-agility-pack" UseCase="We use the HtmlAgilityPack to extract content from the web. This is necessary, e.g., when you provide a URL as input for an assistant."/>
<ThirdPartyComponent Name="ReverseMarkdown" Developer="Babu Annamalai & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/mysticmind/reversemarkdown-net/blob/master/LICENSE" RepositoryUrl="https://github.com/mysticmind/reversemarkdown-net" UseCase="This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant."/>

View File

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

View File

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

View File

@ -6,12 +6,16 @@ public sealed class PluginLanguage : PluginBase, ILanguagePlugin
{
private readonly Dictionary<string, string> content = [];
private readonly List<ILanguagePlugin> 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;
}
/// <summary>
/// Tries to initialize the IETF tag.
/// </summary>
/// <param name="message">The error message, when the IETF tag could not be read.</param>
/// <param name="readLangCultureTag">The read IETF tag.</param>
/// <returns>True, when the IETF tag could be read, false otherwise.</returns>
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;
}
}

View File

@ -0,0 +1,16 @@
namespace AIStudio.Tools.Services;
public sealed partial class RustService
{
public async Task<string> 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();
}
}

10
runtime/Cargo.lock generated
View File

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

View File

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

View File

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

View File

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