diff --git a/README.md b/README.md index 68fedad..3edce81 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ Auto-detects your system language. Supported: - 🇻🇳 Vietnamese - 🇪🇸 Spanish - 🇺🇦 Ukrainian +- 🇨🇳 Simplified Chinese *(Falls back to English if system language is not supported)* diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 77b59f8..991d743 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -17,17 +17,72 @@ pub fn greet(name: &str) -> String { #[tauri::command] pub fn get_system_language() -> String { let locale = sys_locale::get_locale().unwrap_or_else(|| "en".to_string()); - let lang = locale.split('-').next().unwrap_or("en").to_lowercase(); - match lang.as_str() { - "pl" => "pl".to_string(), - "it" => "it".to_string(), - "de" => "de".to_string(), - "fr" => "fr".to_string(), - "ru" => "ru".to_string(), - "ja" => "ja".to_string(), - "es" => "es".to_string(), - "uk" => "uk".to_string(), - _ => "en".to_string(), + map_system_locale(&locale).to_string() +} + +fn map_system_locale(locale: &str) -> &'static str { + let normalized = locale.replace('_', "-").to_lowercase(); + let mut parts = normalized.split('-'); + let language = parts.next().unwrap_or("en"); + + if language == "zh" { + let variants: Vec<&str> = parts.collect(); + + if variants.contains(&"hant") { + return "en"; + } + if variants.contains(&"hans") { + return "zh-CN"; + } + if variants + .iter() + .any(|part| matches!(*part, "tw" | "hk" | "mo")) + { + return "en"; + } + if variants.iter().any(|part| matches!(*part, "cn" | "sg")) { + return "zh-CN"; + } + + return "en"; + } + + match language { + "pl" => "pl", + "it" => "it", + "de" => "de", + "fr" => "fr", + "ru" => "ru", + "ja" => "ja", + "es" => "es", + "uk" => "uk", + _ => "en", + } +} + +#[cfg(test)] +mod locale_tests { + use super::map_system_locale; + + #[test] + fn maps_simplified_chinese_variants_to_zh_cn() { + for locale in ["zh-CN", "zh-SG", "zh-Hans", "zh-Hans-CN", "zh_CN"] { + assert_eq!(map_system_locale(locale), "zh-CN", "locale: {locale}"); + } + } + + #[test] + fn falls_back_for_traditional_and_ambiguous_chinese_locales() { + for locale in ["zh-TW", "zh-HK", "zh-MO", "zh-Hant", "zh-Hant-TW", "zh"] { + assert_eq!(map_system_locale(locale), "en", "locale: {locale}"); + } + } + + #[test] + fn preserves_existing_supported_language_mapping() { + assert_eq!(map_system_locale("pl-PL"), "pl"); + assert_eq!(map_system_locale("ja_JP"), "ja"); + assert_eq!(map_system_locale("en-US"), "en"); } } diff --git a/src-tauri/src/i18n.rs b/src-tauri/src/i18n.rs index 0753b13..107a327 100644 --- a/src-tauri/src/i18n.rs +++ b/src-tauri/src/i18n.rs @@ -1,128 +1,142 @@ -use std::collections::HashMap; - -pub struct TrayI18n { - strings: HashMap<&'static str, &'static str>, -} - -impl TrayI18n { - pub fn new(lang: &str) -> Self { - let mut strings = HashMap::new(); - match lang { - "pl" => { - strings.insert("quit", "Zamknij"); - strings.insert("settings", "Ustawienia"); - strings.insert("clean_now", "Uporządkuj teraz"); - strings.insert("tooltip", "Mouzi"); - strings.insert("tooltip_one_pending", "Mouzi – {} plik czeka"); - strings.insert("tooltip_many_pending", "Mouzi – {} pliki czekają"); - strings.insert("popup_title", "Mouzi"); - strings.insert("settings_title", "Ustawienia Mouzi"); - strings.insert("organized", "Uporządkowano {} plik(i)"); - } - "it" => { - strings.insert("quit", "Esci"); - strings.insert("settings", "Impostazioni"); - strings.insert("clean_now", "Organizza ora"); - strings.insert("tooltip", "Mouzi"); - strings.insert("tooltip_one_pending", "Mouzi – {} file in attesa"); - strings.insert("tooltip_many_pending", "Mouzi – {} file in attesa"); - strings.insert("popup_title", "Mouzi"); - strings.insert("settings_title", "Impostazioni Mouzi"); - strings.insert("organized", "Organizzati {} file"); - } - "de" => { - strings.insert("quit", "Beenden"); - strings.insert("settings", "Einstellungen"); - strings.insert("clean_now", "Jetzt organisieren"); - strings.insert("tooltip", "Mouzi"); - strings.insert("tooltip_one_pending", "Mouzi – {} Datei wartend"); - strings.insert("tooltip_many_pending", "Mouzi – {} Dateien wartend"); - strings.insert("popup_title", "Mouzi"); - strings.insert("settings_title", "Mouzi Einstellungen"); - strings.insert("organized", "{} Datei(en) organisiert"); - } - "fr" => { - strings.insert("quit", "Quitter"); - strings.insert("settings", "Paramètres"); - strings.insert("clean_now", "Organiser maintenant"); - strings.insert("tooltip", "Mouzi"); - strings.insert("tooltip_one_pending", "Mouzi – {} fichier en attente"); - strings.insert("tooltip_many_pending", "Mouzi – {} fichiers en attente"); - strings.insert("popup_title", "Mouzi"); - strings.insert("settings_title", "Paramètres Mouzi"); - strings.insert("organized", "{} fichier(s) organisé(s)"); - } - "ru" => { - strings.insert("quit", "Выход"); - strings.insert("settings", "Настройки"); - strings.insert("clean_now", "Организовать сейчас"); - strings.insert("tooltip", "Mouzi"); - strings.insert("tooltip_one_pending", "Mouzi – {} файл ожидает"); - strings.insert("tooltip_many_pending", "Mouzi – {} файла ожидают"); - strings.insert("popup_title", "Mouzi"); - strings.insert("settings_title", "Настройки Mouzi"); - strings.insert("organized", "Организовано {} файл(ов)"); - } - "ja" => { - strings.insert("quit", "終了"); - strings.insert("settings", "設定"); - strings.insert("clean_now", "今すぐ整理"); - strings.insert("tooltip", "Mouzi"); - strings.insert("tooltip_one_pending", "Mouzi – {} 個のファイルが待機中"); - strings.insert("tooltip_many_pending", "Mouzi – {} 個のファイルが待機中"); - strings.insert("popup_title", "Mouzi"); - strings.insert("settings_title", "Mouziの設定"); - strings.insert("organized", "{}個のファイルを整理しました"); - } - "vi" => { - strings.insert("quit", "Thoát"); - strings.insert("settings", "Cài đặt"); - strings.insert("clean_now", "Sắp xếp ngay"); - strings.insert("tooltip", "Mouzi"); - strings.insert("tooltip_one_pending", "Mouzi – {} tệp đang chờ"); - strings.insert("tooltip_many_pending", "Mouzi – {} tệp đang chờ"); - strings.insert("popup_title", "Mouzi"); - strings.insert("settings_title", "Cài đặt Mouzi"); - strings.insert("organized", "Đã sắp xếp {} tệp"); - } - "es" => { - strings.insert("quit", "Salir"); - strings.insert("settings", "Configuración"); - strings.insert("clean_now", "Organizar ahora"); - strings.insert("tooltip", "Mouzi"); - strings.insert("tooltip_one_pending", "Mouzi – {} archivo esperando"); - strings.insert("tooltip_many_pending", "Mouzi – {} archivos esperando"); - strings.insert("popup_title", "Mouzi"); - strings.insert("settings_title", "Configuración de Mouzi"); - strings.insert("organized", "{} archivo(s) organizado(s)"); - } - "uk" => { - strings.insert("quit", "Вийти"); - strings.insert("settings", "Налаштування"); - strings.insert("clean_now", "Упорядкувати зараз"); - strings.insert("tooltip", "Mouzi"); - strings.insert("tooltip_one_pending", "Mouzi — очікує {} файл"); - strings.insert("tooltip_many_pending", "Mouzi — очікує файлів: {}"); - strings.insert("popup_title", "Mouzi"); - strings.insert("settings_title", "Налаштування Mouzi"); - strings.insert("organized", "Впорядковано файлів: {}"); - } - _ => { - strings.insert("quit", "Quit"); - strings.insert("settings", "Settings"); - strings.insert("clean_now", "Organize Now"); - strings.insert("tooltip", "Mouzi"); - strings.insert("tooltip_one_pending", "Mouzi – {} file waiting"); - strings.insert("tooltip_many_pending", "Mouzi – {} files waiting"); - strings.insert("popup_title", "Mouzi"); - strings.insert("settings_title", "Mouzi Settings"); - strings.insert("organized", "Organized {} file(s)"); - } - } - Self { strings } - } - - pub fn get<'a>(&self, key: &'a str) -> &'a str { - self.strings.get(key).copied().unwrap_or(key) - } -} +use std::collections::HashMap; + +pub struct TrayI18n { + strings: HashMap<&'static str, &'static str>, +} + +impl TrayI18n { + pub fn new(lang: &str) -> Self { + let mut strings = HashMap::new(); + match lang { + "pl" => { + strings.insert("quit", "Zamknij"); + strings.insert("settings", "Ustawienia"); + strings.insert("clean_now", "Uporządkuj teraz"); + strings.insert("tooltip", "Mouzi"); + strings.insert("tooltip_one_pending", "Mouzi – {} plik czeka"); + strings.insert("tooltip_many_pending", "Mouzi – {} pliki czekają"); + strings.insert("popup_title", "Mouzi"); + strings.insert("settings_title", "Ustawienia Mouzi"); + strings.insert("organized", "Uporządkowano {} plik(i)"); + } + "it" => { + strings.insert("quit", "Esci"); + strings.insert("settings", "Impostazioni"); + strings.insert("clean_now", "Organizza ora"); + strings.insert("tooltip", "Mouzi"); + strings.insert("tooltip_one_pending", "Mouzi – {} file in attesa"); + strings.insert("tooltip_many_pending", "Mouzi – {} file in attesa"); + strings.insert("popup_title", "Mouzi"); + strings.insert("settings_title", "Impostazioni Mouzi"); + strings.insert("organized", "Organizzati {} file"); + } + "de" => { + strings.insert("quit", "Beenden"); + strings.insert("settings", "Einstellungen"); + strings.insert("clean_now", "Jetzt organisieren"); + strings.insert("tooltip", "Mouzi"); + strings.insert("tooltip_one_pending", "Mouzi – {} Datei wartend"); + strings.insert("tooltip_many_pending", "Mouzi – {} Dateien wartend"); + strings.insert("popup_title", "Mouzi"); + strings.insert("settings_title", "Mouzi Einstellungen"); + strings.insert("organized", "{} Datei(en) organisiert"); + } + "fr" => { + strings.insert("quit", "Quitter"); + strings.insert("settings", "Paramètres"); + strings.insert("clean_now", "Organiser maintenant"); + strings.insert("tooltip", "Mouzi"); + strings.insert("tooltip_one_pending", "Mouzi – {} fichier en attente"); + strings.insert("tooltip_many_pending", "Mouzi – {} fichiers en attente"); + strings.insert("popup_title", "Mouzi"); + strings.insert("settings_title", "Paramètres Mouzi"); + strings.insert("organized", "{} fichier(s) organisé(s)"); + } + "ru" => { + strings.insert("quit", "Выход"); + strings.insert("settings", "Настройки"); + strings.insert("clean_now", "Организовать сейчас"); + strings.insert("tooltip", "Mouzi"); + strings.insert("tooltip_one_pending", "Mouzi – {} файл ожидает"); + strings.insert("tooltip_many_pending", "Mouzi – {} файла ожидают"); + strings.insert("popup_title", "Mouzi"); + strings.insert("settings_title", "Настройки Mouzi"); + strings.insert("organized", "Организовано {} файл(ов)"); + } + "ja" => { + strings.insert("quit", "終了"); + strings.insert("settings", "設定"); + strings.insert("clean_now", "今すぐ整理"); + strings.insert("tooltip", "Mouzi"); + strings.insert("tooltip_one_pending", "Mouzi – {} 個のファイルが待機中"); + strings.insert("tooltip_many_pending", "Mouzi – {} 個のファイルが待機中"); + strings.insert("popup_title", "Mouzi"); + strings.insert("settings_title", "Mouziの設定"); + strings.insert("organized", "{}個のファイルを整理しました"); + } + "vi" => { + strings.insert("quit", "Thoát"); + strings.insert("settings", "Cài đặt"); + strings.insert("clean_now", "Sắp xếp ngay"); + strings.insert("tooltip", "Mouzi"); + strings.insert("tooltip_one_pending", "Mouzi – {} tệp đang chờ"); + strings.insert("tooltip_many_pending", "Mouzi – {} tệp đang chờ"); + strings.insert("popup_title", "Mouzi"); + strings.insert("settings_title", "Cài đặt Mouzi"); + strings.insert("organized", "Đã sắp xếp {} tệp"); + } + "es" => { + strings.insert("quit", "Salir"); + strings.insert("settings", "Configuración"); + strings.insert("clean_now", "Organizar ahora"); + strings.insert("tooltip", "Mouzi"); + strings.insert("tooltip_one_pending", "Mouzi – {} archivo esperando"); + strings.insert("tooltip_many_pending", "Mouzi – {} archivos esperando"); + strings.insert("popup_title", "Mouzi"); + strings.insert("settings_title", "Configuración de Mouzi"); + strings.insert("organized", "{} archivo(s) organizado(s)"); + } + "zh-CN" => { + strings.insert("quit", "退出"); + strings.insert("settings", "设置"); + strings.insert("clean_now", "立即整理"); + strings.insert("tooltip", "Mouzi"); + strings.insert("tooltip_one_pending", "Mouzi – {} 个文件等待整理"); + strings.insert("tooltip_many_pending", "Mouzi – {} 个文件等待整理"); + strings.insert("popup_title", "Mouzi"); + strings.insert("settings_title", "Mouzi 设置"); + strings.insert("organized", "已整理 {} 个文件"); + strings.insert("notification_title", "Mouzi – 点击打开文件夹"); + } + "uk" => { + strings.insert("quit", "Вийти"); + strings.insert("settings", "Налаштування"); + strings.insert("clean_now", "Упорядкувати зараз"); + strings.insert("tooltip", "Mouzi"); + strings.insert("tooltip_one_pending", "Mouzi — очікує {} файл"); + strings.insert("tooltip_many_pending", "Mouzi — очікує файлів: {}"); + strings.insert("popup_title", "Mouzi"); + strings.insert("settings_title", "Налаштування Mouzi"); + strings.insert("organized", "Впорядковано файлів: {}"); + } + _ => { + strings.insert("quit", "Quit"); + strings.insert("settings", "Settings"); + strings.insert("clean_now", "Organize Now"); + strings.insert("tooltip", "Mouzi"); + strings.insert("tooltip_one_pending", "Mouzi – {} file waiting"); + strings.insert("tooltip_many_pending", "Mouzi – {} files waiting"); + strings.insert("popup_title", "Mouzi"); + strings.insert("settings_title", "Mouzi Settings"); + strings.insert("organized", "Organized {} file(s)"); + strings.insert("notification_title", "Mouzi – click to open folder"); + } + } + Self { strings } + } + + pub fn get<'a>(&self, key: &'a str) -> &'a str { + self.strings.get(key).copied().unwrap_or(key) + } +} + diff --git a/src-tauri/src/watcher.rs b/src-tauri/src/watcher.rs index 88f869a..12f2b54 100644 --- a/src-tauri/src/watcher.rs +++ b/src-tauri/src/watcher.rs @@ -1,4 +1,5 @@ use crate::db::{get_watched_folders, is_folder_manual_mode, is_folder_paused_mode}; +use crate::i18n::TrayI18n; use crate::rules::{is_file_ignored_by_mouziignore, process_file, should_ignore_file}; use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher}; use std::collections::{HashMap, HashSet}; @@ -184,21 +185,27 @@ impl FolderWatcher { // when the user clicks the notification (Windows activates the app) *pending_open_folder.lock().unwrap() = Some(last_dest_folder.clone()); + let lang = crate::db::get_settings() + .map(|settings| settings.language) + .unwrap_or_else(|_| "en".to_string()); + let i18n = TrayI18n::new(&lang); + let notification_title = i18n.get("notification_title").to_string(); let body = if organized_count == 1 { last_notification_message .clone() .unwrap_or_else(|| format!("{} → {}", last_file_name, last_rule_name)) } else { - format!("Organized {} files", organized_count) + i18n.get("organized").replace("{}", &organized_count.to_string()) }; #[cfg(target_os = "windows")] { let dest_folder_clone = last_dest_folder.clone(); let body_clone = body.clone(); + let title_clone = notification_title.clone(); let _ = std::thread::spawn(move || { let _ = tauri_winrt_notification::Toast::new("cc.mouzi.app") - .title("Mouzi – click to open folder") + .title(&title_clone) .text1(&body_clone) .on_activated(move |_action| { // Open the folder in Explorer robustly @@ -216,7 +223,7 @@ impl FolderWatcher { let _ = handle .notification() .builder() - .title("Mouzi – click to open folder") + .title(notification_title) .body(body) .extra("destFolder", last_dest_folder) .show(); diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index ff90d39..4969cde 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -912,6 +912,7 @@ export default function Settings() { +