mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-02-16 18:01:38 +00:00
Improved the user language detection, especially for Linux (#663)
Some checks are pending
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-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage deb 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 / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (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 / Read metadata (push) Waiting to run
Some checks are pending
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-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage deb 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 / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (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 / Read metadata (push) Waiting to run
This commit is contained in:
parent
3671444d28
commit
53f657da71
@ -172,17 +172,31 @@ public sealed class SettingsManager
|
|||||||
{
|
{
|
||||||
case LangBehavior.AUTO:
|
case LangBehavior.AUTO:
|
||||||
var languageCode = await this.rustService.ReadUserLanguage();
|
var languageCode = await this.rustService.ReadUserLanguage();
|
||||||
var languagePlugin = PluginFactory.RunningPlugins.FirstOrDefault(x => x is ILanguagePlugin langPlug && langPlug.IETFTag == languageCode);
|
var languagePlugins = PluginFactory.RunningPlugins.OfType<ILanguagePlugin>().ToList();
|
||||||
if (languagePlugin is null)
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(languageCode))
|
||||||
{
|
{
|
||||||
this.logger.LogWarning($"The language plugin for the language '{languageCode}' is not available.");
|
var exactMatch = languagePlugins.FirstOrDefault(x => string.Equals(x.IETFTag, languageCode, StringComparison.OrdinalIgnoreCase));
|
||||||
return PluginFactory.BaseLanguage;
|
if (exactMatch is not null)
|
||||||
|
return exactMatch;
|
||||||
|
|
||||||
|
var primaryLanguage = GetPrimaryLanguage(languageCode);
|
||||||
|
if (!string.IsNullOrWhiteSpace(primaryLanguage))
|
||||||
|
{
|
||||||
|
var primaryLanguageMatch = languagePlugins
|
||||||
|
.Where(x => string.Equals(GetPrimaryLanguage(x.IETFTag), primaryLanguage, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.OrderBy(x => x.IETFTag, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
if (primaryLanguageMatch is not null)
|
||||||
|
{
|
||||||
|
this.logger.LogWarning($"No exact language plugin found for '{languageCode}'. Use language fallback '{primaryLanguageMatch.IETFTag}'.");
|
||||||
|
return primaryLanguageMatch;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (languagePlugin is ILanguagePlugin langPlugin)
|
this.logger.LogWarning($"The language plugin for the language '{languageCode}' (normalized='{languageCode}') is not available.");
|
||||||
return langPlugin;
|
|
||||||
|
|
||||||
this.logger.LogError("The language plugin is not a language plugin.");
|
|
||||||
return PluginFactory.BaseLanguage;
|
return PluginFactory.BaseLanguage;
|
||||||
|
|
||||||
case LangBehavior.MANUAL:
|
case LangBehavior.MANUAL:
|
||||||
@ -204,6 +218,18 @@ public sealed class SettingsManager
|
|||||||
this.logger.LogError("The language behavior is unknown.");
|
this.logger.LogError("The language behavior is unknown.");
|
||||||
return PluginFactory.BaseLanguage;
|
return PluginFactory.BaseLanguage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string GetPrimaryLanguage(string localeTag)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(localeTag))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var separatorIndex = localeTag.IndexOf('-');
|
||||||
|
if (separatorIndex < 0)
|
||||||
|
return localeTag;
|
||||||
|
|
||||||
|
return localeTag[..separatorIndex];
|
||||||
|
}
|
||||||
|
|
||||||
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
|
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
|
||||||
public Provider GetPreselectedProvider(Tools.Components component, string? currentProviderId = null, bool usePreselectionBeforeCurrentProvider = false)
|
public Provider GetPreselectedProvider(Tools.Components component, string? currentProviderId = null, bool usePreselectionBeforeCurrentProvider = false)
|
||||||
@ -365,4 +391,4 @@ public sealed class SettingsManager
|
|||||||
// Return the full name of the property, including the class name:
|
// Return the full name of the property, including the class name:
|
||||||
return $"{typeof(TIn).Name}.{memberExpr.Member.Name}";
|
return $"{typeof(TIn).Name}.{memberExpr.Member.Name}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
- Improved the document analysis assistant (in beta) by hiding the export functionality by default. Enable the administration options in the app settings to show and use the export functionality. This streamlines the usage for regular users.
|
- Improved the document analysis assistant (in beta) by hiding the export functionality by default. Enable the administration options in the app settings to show and use the export functionality. This streamlines the usage for regular users.
|
||||||
- Improved the workspaces experience by using a different color for the delete button to avoid confusion.
|
- Improved the workspaces experience by using a different color for the delete button to avoid confusion.
|
||||||
- Improved the plugins page by adding an action to open the plugin source link. The action opens website URLs in an external browser, supports `mailto:` links for direct email composition.
|
- Improved the plugins page by adding an action to open the plugin source link. The action opens website URLs in an external browser, supports `mailto:` links for direct email composition.
|
||||||
|
- Improved the system language detection for locale values such as `C` and variants like `de_DE.UTF-8`, enabling AI Studio to apply the matching UI language more reliably.
|
||||||
- Fixed an issue where manually saving chats in workspace manual-storage mode could appear unreliable during response streaming. The save button is now disabled while streaming to prevent partial saves.
|
- Fixed an issue where manually saving chats in workspace manual-storage mode could appear unreliable during response streaming. The save button is now disabled while streaming to prevent partial saves.
|
||||||
- Fixed a bug in the Responses API of our OpenAI provider implementation where streamed whitespace chunks were discarded. We thank Oliver Kunc `OliverKunc` for his first contribution in resolving this issue. We appreciate your help, Oliver.
|
- Fixed a bug in the Responses API of our OpenAI provider implementation where streamed whitespace chunks were discarded. We thank Oliver Kunc `OliverKunc` for his first contribution in resolving this issue. We appreciate your help, Oliver.
|
||||||
- Upgraded dependencies.
|
- Upgraded dependencies.
|
||||||
@ -7,6 +7,8 @@ use serde::Serialize;
|
|||||||
use sys_locale::get_locale;
|
use sys_locale::get_locale;
|
||||||
use crate::api_token::APIToken;
|
use crate::api_token::APIToken;
|
||||||
|
|
||||||
|
const DEFAULT_LANGUAGE: &str = "en-US";
|
||||||
|
|
||||||
/// The data directory where the application stores its data.
|
/// The data directory where the application stores its data.
|
||||||
pub static DATA_DIRECTORY: OnceLock<String> = OnceLock::new();
|
pub static DATA_DIRECTORY: OnceLock<String> = OnceLock::new();
|
||||||
|
|
||||||
@ -41,12 +43,115 @@ pub fn is_prod() -> bool {
|
|||||||
!is_dev()
|
!is_dev()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn normalize_locale_tag(locale: &str) -> Option<String> {
|
||||||
|
let trimmed = locale.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let without_encoding = trimmed
|
||||||
|
.split('.')
|
||||||
|
.next()
|
||||||
|
.unwrap_or(trimmed)
|
||||||
|
.split('@')
|
||||||
|
.next()
|
||||||
|
.unwrap_or(trimmed)
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
if without_encoding.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let normalized_delimiters = without_encoding.replace('_', "-");
|
||||||
|
let mut segments = normalized_delimiters
|
||||||
|
.split('-')
|
||||||
|
.filter(|segment| !segment.is_empty());
|
||||||
|
|
||||||
|
let language = segments.next()?;
|
||||||
|
if language.eq_ignore_ascii_case("c") || language.eq_ignore_ascii_case("posix") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let language = language.to_ascii_lowercase();
|
||||||
|
if language.len() < 2 || !language.chars().all(|c| c.is_ascii_alphabetic()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(region) = segments.next() {
|
||||||
|
if region.len() == 2 && region.chars().all(|c| c.is_ascii_alphabetic()) {
|
||||||
|
return Some(format!("{}-{}", language, region.to_ascii_uppercase()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(language)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn read_locale_from_environment() -> Option<String> {
|
||||||
|
if let Ok(language) = env::var("LANGUAGE") {
|
||||||
|
for candidate in language.split(':') {
|
||||||
|
if let Some(locale) = normalize_locale_tag(candidate) {
|
||||||
|
info!("Detected user language from Linux environment variable 'LANGUAGE': '{}'.", locale);
|
||||||
|
return Some(locale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for key in ["LC_ALL", "LC_MESSAGES", "LANG"] {
|
||||||
|
if let Ok(value) = env::var(key) {
|
||||||
|
if let Some(locale) = normalize_locale_tag(&value) {
|
||||||
|
info!("Detected user language from Linux environment variable '{}': '{}'.", key, locale);
|
||||||
|
return Some(locale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
fn read_locale_from_environment() -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::normalize_locale_tag;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalize_locale_tag_supports_common_linux_formats() {
|
||||||
|
assert_eq!(normalize_locale_tag("de_DE.UTF-8"), Some(String::from("de-DE")));
|
||||||
|
assert_eq!(normalize_locale_tag("de_DE@euro"), Some(String::from("de-DE")));
|
||||||
|
assert_eq!(normalize_locale_tag("de"), Some(String::from("de")));
|
||||||
|
assert_eq!(normalize_locale_tag("en-US"), Some(String::from("en-US")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalize_locale_tag_rejects_non_language_locales() {
|
||||||
|
assert_eq!(normalize_locale_tag("C"), None);
|
||||||
|
assert_eq!(normalize_locale_tag("C.UTF-8"), None);
|
||||||
|
assert_eq!(normalize_locale_tag("POSIX"), None);
|
||||||
|
assert_eq!(normalize_locale_tag(""), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/system/language")]
|
#[get("/system/language")]
|
||||||
pub fn read_user_language(_token: APIToken) -> String {
|
pub fn read_user_language(_token: APIToken) -> String {
|
||||||
get_locale().unwrap_or_else(|| {
|
if let Some(locale) = get_locale() {
|
||||||
warn!("Could not determine the system language. Use default 'en-US'.");
|
if let Some(normalized_locale) = normalize_locale_tag(&locale) {
|
||||||
String::from("en-US")
|
info!("Detected user language from sys-locale: '{}'.", normalized_locale);
|
||||||
})
|
return normalized_locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
warn!("sys-locale returned an unusable locale value: '{}'.", locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(locale) = read_locale_from_environment() {
|
||||||
|
return locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
warn!("Could not determine the system language. Use default '{}'.", DEFAULT_LANGUAGE);
|
||||||
|
String::from(DEFAULT_LANGUAGE)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/system/enterprise/config/id")]
|
#[get("/system/enterprise/config/id")]
|
||||||
@ -300,4 +405,4 @@ fn get_enterprise_configuration(_reg_value: &str, env_name: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user