feat(i18n): replace ad-hoc loader with rust-i18n backend bundles
This commit is contained in:
@@ -1,43 +1,46 @@
|
||||
use crate::{config::Config, utils::dirs};
|
||||
use once_cell::sync::Lazy;
|
||||
use smartstring::alias::String;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs,
|
||||
path::PathBuf,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
use crate::config::Config;
|
||||
use sys_locale;
|
||||
|
||||
const DEFAULT_LANGUAGE: &str = "zh";
|
||||
|
||||
type TranslationMap = (String, HashMap<String, Arc<str>>);
|
||||
fn supported_languages_internal() -> Vec<&'static str> {
|
||||
rust_i18n::available_locales!()
|
||||
}
|
||||
|
||||
fn get_locales_dir() -> Option<PathBuf> {
|
||||
dirs::app_resources_dir()
|
||||
.map(|resource_path| resource_path.join("locales"))
|
||||
.ok()
|
||||
fn is_supported(language: &str) -> bool {
|
||||
let normalized = language.to_lowercase();
|
||||
supported_languages_internal()
|
||||
.iter()
|
||||
.any(|&lang| lang.eq_ignore_ascii_case(&normalized))
|
||||
}
|
||||
|
||||
const fn fallback_language() -> &'static str {
|
||||
DEFAULT_LANGUAGE
|
||||
}
|
||||
|
||||
fn system_language() -> String {
|
||||
sys_locale::get_locale()
|
||||
.map(|locale| locale.to_lowercase())
|
||||
.and_then(|locale| locale.split(['_', '-']).next().map(str::to_string))
|
||||
.filter(|lang| is_supported(lang))
|
||||
.unwrap_or_else(|| fallback_language().to_string())
|
||||
}
|
||||
|
||||
pub fn get_supported_languages() -> Vec<String> {
|
||||
let mut languages = Vec::new();
|
||||
supported_languages_internal()
|
||||
.into_iter()
|
||||
.map(|lang| lang.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
if let Some(locales_dir) = get_locales_dir()
|
||||
&& let Ok(entries) = fs::read_dir(locales_dir)
|
||||
{
|
||||
for entry in entries.flatten() {
|
||||
if let Some(file_name) = entry.file_name().to_str()
|
||||
&& let Some(lang) = file_name.strip_suffix(".json")
|
||||
{
|
||||
languages.push(lang.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if languages.is_empty() {
|
||||
languages.push(DEFAULT_LANGUAGE.into());
|
||||
}
|
||||
languages
|
||||
pub fn set_locale(language: &str) {
|
||||
let normalized = language.to_lowercase();
|
||||
let lang = if is_supported(&normalized) {
|
||||
normalized
|
||||
} else {
|
||||
fallback_language().to_string()
|
||||
};
|
||||
rust_i18n::set_locale(&lang);
|
||||
}
|
||||
|
||||
pub async fn current_language() -> String {
|
||||
@@ -45,70 +48,19 @@ pub async fn current_language() -> String {
|
||||
.await
|
||||
.latest_arc()
|
||||
.language
|
||||
.as_deref()
|
||||
.map(String::from)
|
||||
.unwrap_or_else(get_system_language)
|
||||
.clone()
|
||||
.filter(|lang| !lang.is_empty())
|
||||
.map(|lang| lang.to_lowercase())
|
||||
.filter(|lang| is_supported(lang))
|
||||
.unwrap_or_else(system_language)
|
||||
}
|
||||
|
||||
static TRANSLATIONS: Lazy<RwLock<TranslationMap>> = Lazy::new(|| {
|
||||
let lang = get_system_language();
|
||||
let map = load_lang_file(&lang).unwrap_or_default();
|
||||
RwLock::new((lang, map))
|
||||
});
|
||||
|
||||
fn load_lang_file(lang: &str) -> Option<HashMap<String, Arc<str>>> {
|
||||
let locales_dir = get_locales_dir()?;
|
||||
let file_path = locales_dir.join(format!("{lang}.json"));
|
||||
fs::read_to_string(file_path)
|
||||
.ok()
|
||||
.and_then(|content| serde_json::from_str::<HashMap<String, String>>(&content).ok())
|
||||
.map(|map| {
|
||||
map.into_iter()
|
||||
.map(|(k, v)| (k, Arc::from(v.as_str())))
|
||||
.collect()
|
||||
})
|
||||
pub async fn sync_locale() -> String {
|
||||
let language = current_language().await;
|
||||
set_locale(&language);
|
||||
language
|
||||
}
|
||||
|
||||
fn get_system_language() -> String {
|
||||
sys_locale::get_locale()
|
||||
.map(|locale| locale.to_lowercase())
|
||||
.and_then(|locale| locale.split(['_', '-']).next().map(String::from))
|
||||
.filter(|lang| get_supported_languages().contains(lang))
|
||||
.unwrap_or_else(|| DEFAULT_LANGUAGE.into())
|
||||
}
|
||||
|
||||
pub async fn t(key: &str) -> Arc<str> {
|
||||
let current_lang = current_language().await;
|
||||
|
||||
{
|
||||
if let Ok(cache) = TRANSLATIONS.read()
|
||||
&& cache.0 == current_lang
|
||||
&& let Some(text) = cache.1.get(key)
|
||||
{
|
||||
return Arc::clone(text);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(new_map) = load_lang_file(¤t_lang)
|
||||
&& let Ok(mut cache) = TRANSLATIONS.write()
|
||||
{
|
||||
*cache = (current_lang.clone(), new_map);
|
||||
|
||||
if let Some(text) = cache.1.get(key) {
|
||||
return Arc::clone(text);
|
||||
}
|
||||
}
|
||||
|
||||
if current_lang != DEFAULT_LANGUAGE
|
||||
&& let Some(default_map) = load_lang_file(DEFAULT_LANGUAGE)
|
||||
&& let Ok(mut cache) = TRANSLATIONS.write()
|
||||
{
|
||||
*cache = (DEFAULT_LANGUAGE.into(), default_map);
|
||||
|
||||
if let Some(text) = cache.1.get(key) {
|
||||
return Arc::clone(text);
|
||||
}
|
||||
}
|
||||
|
||||
Arc::from(key)
|
||||
pub const fn default_language() -> &'static str {
|
||||
fallback_language()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::{core::handle, utils::i18n::t};
|
||||
|
||||
use crate::{core::handle, utils::i18n};
|
||||
use tauri_plugin_notification::NotificationExt;
|
||||
|
||||
pub enum NotificationEvent<'a> {
|
||||
@@ -27,48 +26,44 @@ fn notify(title: &str, body: &str) {
|
||||
}
|
||||
|
||||
pub async fn notify_event<'a>(event: NotificationEvent<'a>) {
|
||||
i18n::sync_locale().await;
|
||||
|
||||
match event {
|
||||
NotificationEvent::DashboardToggled => {
|
||||
notify(
|
||||
&t("DashboardToggledTitle").await,
|
||||
&t("DashboardToggledBody").await,
|
||||
);
|
||||
let title = rust_i18n::t!("notifications.dashboardToggled.title").to_string();
|
||||
let body = rust_i18n::t!("notifications.dashboardToggled.body").to_string();
|
||||
notify(&title, &body);
|
||||
}
|
||||
NotificationEvent::ClashModeChanged { mode } => {
|
||||
notify(
|
||||
&t("ClashModeChangedTitle").await,
|
||||
&t_with_args("ClashModeChangedBody", mode).await,
|
||||
);
|
||||
let title = rust_i18n::t!("notifications.clashModeChanged.title").to_string();
|
||||
let body = rust_i18n::t!("notifications.clashModeChanged.body").replace("{mode}", mode);
|
||||
notify(&title, &body);
|
||||
}
|
||||
NotificationEvent::SystemProxyToggled => {
|
||||
notify(
|
||||
&t("SystemProxyToggledTitle").await,
|
||||
&t("SystemProxyToggledBody").await,
|
||||
);
|
||||
let title = rust_i18n::t!("notifications.systemProxyToggled.title").to_string();
|
||||
let body = rust_i18n::t!("notifications.systemProxyToggled.body").to_string();
|
||||
notify(&title, &body);
|
||||
}
|
||||
NotificationEvent::TunModeToggled => {
|
||||
notify(
|
||||
&t("TunModeToggledTitle").await,
|
||||
&t("TunModeToggledBody").await,
|
||||
);
|
||||
let title = rust_i18n::t!("notifications.tunModeToggled.title").to_string();
|
||||
let body = rust_i18n::t!("notifications.tunModeToggled.body").to_string();
|
||||
notify(&title, &body);
|
||||
}
|
||||
NotificationEvent::LightweightModeEntered => {
|
||||
notify(
|
||||
&t("LightweightModeEnteredTitle").await,
|
||||
&t("LightweightModeEnteredBody").await,
|
||||
);
|
||||
let title = rust_i18n::t!("notifications.lightweightModeEntered.title").to_string();
|
||||
let body = rust_i18n::t!("notifications.lightweightModeEntered.body").to_string();
|
||||
notify(&title, &body);
|
||||
}
|
||||
NotificationEvent::AppQuit => {
|
||||
notify(&t("AppQuitTitle").await, &t("AppQuitBody").await);
|
||||
let title = rust_i18n::t!("notifications.appQuit.title").to_string();
|
||||
let body = rust_i18n::t!("notifications.appQuit.body").to_string();
|
||||
notify(&title, &body);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
NotificationEvent::AppHidden => {
|
||||
notify(&t("AppHiddenTitle").await, &t("AppHiddenBody").await);
|
||||
let title = rust_i18n::t!("notifications.appHidden.title").to_string();
|
||||
let body = rust_i18n::t!("notifications.appHidden.body").to_string();
|
||||
notify(&title, &body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助函数,带参数的i18n
|
||||
async fn t_with_args(key: &str, mode: &str) -> String {
|
||||
t(key).await.replace("{mode}", mode)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user