Fix: Refine locale matching logic to support region-specific tags (#411)
* wgui(i18n): Fix language matching logic * wgui(i18n): Fix locale prefix match
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
use cosmic_text::PlatformFallback;
|
use cosmic_text::PlatformFallback;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
|
||||||
|
use crate::i18n::Locale;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct WguiFontConfig<'a> {
|
pub struct WguiFontConfig<'a> {
|
||||||
pub binaries: Vec<&'a [u8]>,
|
pub binaries: Vec<&'a [u8]>,
|
||||||
@@ -14,7 +16,7 @@ pub struct WguiFontSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl WguiFontSystem {
|
impl WguiFontSystem {
|
||||||
pub fn new(config: &WguiFontConfig, lang: &str) -> Self {
|
pub fn new(config: &WguiFontConfig, locale: &Locale) -> Self {
|
||||||
let mut db = cosmic_text::fontdb::Database::new();
|
let mut db = cosmic_text::fontdb::Database::new();
|
||||||
|
|
||||||
let system = if config.binaries.is_empty() {
|
let system = if config.binaries.is_empty() {
|
||||||
@@ -37,7 +39,11 @@ impl WguiFontSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// we don't require anything special, at least for now
|
// we don't require anything special, at least for now
|
||||||
cosmic_text::FontSystem::new_with_locale_and_db_and_fallback(lang.to_string(), db, PlatformFallback)
|
cosmic_text::FontSystem::new_with_locale_and_db_and_fallback(
|
||||||
|
locale.get_matched().to_owned(),
|
||||||
|
db,
|
||||||
|
PlatformFallback,
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ impl WguiGlobals {
|
|||||||
assets_builtin,
|
assets_builtin,
|
||||||
defaults,
|
defaults,
|
||||||
asset_folder,
|
asset_folder,
|
||||||
font_system: WguiFontSystem::new(font_config, i18n_builtin.get_lang()),
|
font_system: WguiFontSystem::new(font_config, i18n_builtin.get_locale()),
|
||||||
i18n_builtin,
|
i18n_builtin,
|
||||||
custom_glyph_cache: CustomGlyphCache::new(),
|
custom_glyph_cache: CustomGlyphCache::new(),
|
||||||
}))))
|
}))))
|
||||||
|
|||||||
119
wgui/src/i18n.rs
119
wgui/src/i18n.rs
@@ -1,4 +1,4 @@
|
|||||||
use std::rc::Rc;
|
use std::{fmt::Display, rc::Rc, str};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
|
||||||
@@ -57,8 +57,89 @@ impl Translation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct I18n {
|
pub struct Locale {
|
||||||
lang: String,
|
lang: String,
|
||||||
|
region: Option<String>,
|
||||||
|
matched: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Locale {
|
||||||
|
pub fn all_locale() -> &'static [&'static str] {
|
||||||
|
&["de", "en", "es", "ja", "it", "pl", "zh_CN"]
|
||||||
|
}
|
||||||
|
pub fn default_lang() -> &'static str {
|
||||||
|
"en"
|
||||||
|
}
|
||||||
|
fn match_locale<'o>(lang: &str, region: Option<&str>, all_locales: &[&'o str]) -> &'o str {
|
||||||
|
if let Some(region) = region {
|
||||||
|
let locale_str = format!("{lang}_{region}");
|
||||||
|
if let Some(locale) = all_locales.iter().find(|&&l| l == locale_str) {
|
||||||
|
return locale;
|
||||||
|
}
|
||||||
|
log::warn!("Unsupported locale \"{locale_str}\", trying \"{lang}\".");
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(locale) = all_locales.iter().find(|&&l| l == lang) {
|
||||||
|
return locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
let prefix = format!("{lang}_");
|
||||||
|
if let Some(locale) = all_locales.iter().find(|&&l| l.starts_with(&prefix)) {
|
||||||
|
return locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
let locale = Self::default_lang();
|
||||||
|
log::warn!("Unsupported language \"{lang}\", defaulting to \"{locale}\".");
|
||||||
|
locale
|
||||||
|
}
|
||||||
|
pub fn new(lang: String, region: Option<String>) -> Self {
|
||||||
|
let matched = Self::match_locale(&lang, region.as_deref(), Self::all_locale()).to_string();
|
||||||
|
Self { lang, region, matched }
|
||||||
|
}
|
||||||
|
pub fn parse_str(locale: &str) -> Self {
|
||||||
|
let base = locale.split(|c| c == '.' || c == '@').next().unwrap_or(locale);
|
||||||
|
let parts: Vec<&str> = base.split(|c| c == '_' || c == '-').collect();
|
||||||
|
// Ensures the format is lang_REGION
|
||||||
|
match parts.as_slice() {
|
||||||
|
[lang, region, ..] => Self::new(lang.to_lowercase(), Some(region.to_uppercase())),
|
||||||
|
[lang] if !lang.is_empty() => Self::new(lang.to_lowercase(), None),
|
||||||
|
_ => Self::new("en".to_string(), None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn from_env() -> Self {
|
||||||
|
use std::env;
|
||||||
|
let vars = ["LC_ALL", "LC_MESSAGES", "LANG"];
|
||||||
|
let full_locale = vars
|
||||||
|
.iter()
|
||||||
|
.find_map(|&v| env::var(v).ok())
|
||||||
|
.filter(|v| !v.is_empty() && v != "C" && v != "POSIX")
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
log::warn!(
|
||||||
|
"LC_ALL/LC_MESSAGES/LANG is not set, defaulting to \"{}\"",
|
||||||
|
Self::default_lang()
|
||||||
|
);
|
||||||
|
Self::default_lang().to_string()
|
||||||
|
});
|
||||||
|
Self::parse_str(&full_locale)
|
||||||
|
}
|
||||||
|
pub fn get_matched(&self) -> &str {
|
||||||
|
&self.matched
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Locale {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.lang)?;
|
||||||
|
if let Some(ref region) = self.region {
|
||||||
|
write!(f, "_{region}")
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct I18n {
|
||||||
|
locale: Locale,
|
||||||
json_root_translated: serde_json::Value, // any language
|
json_root_translated: serde_json::Value, // any language
|
||||||
json_root_fallback: serde_json::Value, // english
|
json_root_fallback: serde_json::Value, // english
|
||||||
}
|
}
|
||||||
@@ -73,35 +154,13 @@ fn find_translation<'a>(translation: &str, mut val: &'a serde_json::Value) -> Op
|
|||||||
val.as_str()
|
val.as_str()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn guess_lang() -> String {
|
|
||||||
if let Ok(lang) = std::env::var("LANG") {
|
|
||||||
if let Some((first, _)) = lang.split_once('_') {
|
|
||||||
String::from(first)
|
|
||||||
} else {
|
|
||||||
lang
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log::warn!("LANG is not set, defaulting to \"en\".");
|
|
||||||
String::from("en")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl I18n {
|
impl I18n {
|
||||||
pub fn new(provider: &mut Box<dyn AssetProvider>) -> anyhow::Result<Self> {
|
pub fn new(provider: &mut Box<dyn AssetProvider>) -> anyhow::Result<Self> {
|
||||||
let mut lang = guess_lang();
|
let locale = Locale::from_env();
|
||||||
log::info!("Guessed system language: {lang}");
|
log::info!("Guessed system language: {locale}");
|
||||||
|
|
||||||
match lang.as_str() {
|
|
||||||
"en" | "pl" | "it" | "ja" | "es" | "de" | "zh_CN" => {}
|
|
||||||
_ => {
|
|
||||||
log::warn!("Unsupported language \"{}\", defaulting to \"en\".", lang.as_str());
|
|
||||||
|
|
||||||
lang = String::from("en");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let data_english = provider.load_from_path("lang/en.json")?;
|
let data_english = provider.load_from_path("lang/en.json")?;
|
||||||
let path = format!("lang/{lang}.json");
|
let path = format!("lang/{}.json", locale.get_matched());
|
||||||
let data_translated = provider
|
let data_translated = provider
|
||||||
.load_from_path(&path)
|
.load_from_path(&path)
|
||||||
.with_context(|| path.clone())
|
.with_context(|| path.clone())
|
||||||
@@ -124,14 +183,14 @@ impl I18n {
|
|||||||
.context("Translation file not valid JSON")?;
|
.context("Translation file not valid JSON")?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
lang,
|
locale,
|
||||||
json_root_translated,
|
json_root_translated,
|
||||||
json_root_fallback,
|
json_root_fallback,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_lang(&self) -> &str {
|
pub fn get_locale(&self) -> &Locale {
|
||||||
&self.lang
|
&self.locale
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn translate(&mut self, translation_key_full: &str) -> Rc<str> {
|
pub fn translate(&mut self, translation_key_full: &str) -> Rc<str> {
|
||||||
|
|||||||
Reference in New Issue
Block a user