AnyBot/src/i18n.rs

231 lines
8.9 KiB
Rust

// 語言: the menus' language and the language bots answer in (the original: "選擇 Grok Bot 選單
// 和訊息使用的語言", 37 languages + automatic).
//
// The window is written in Traditional Chinese. 简体中文 is converted from it by macOS
// (ICU "Hant-Hans"), so every string is covered. English and 日本語 use the tables below for the
// window's chrome (sidebar, header, composer, side panel, settings); the rest stays in Chinese.
// Other languages show the menus in English and make the bots answer in that language.
use std::collections::HashMap;
use std::sync::{Mutex, OnceLock, RwLock};
/// The original's 37 languages: (tag, endonym, English name).
pub const LANGS: [(&str, &str, &str); 37] = [
("af", "Afrikaans", "Afrikaans"),
("ar", "العربية", "Arabic"),
("bn", "বাংলা", "Bengali"),
("cs", "Čeština", "Czech"),
("da", "Dansk", "Danish"),
("de", "Deutsch", "German"),
("el", "Ελληνικά", "Greek"),
("en", "English", "English"),
("es", "Español", "Spanish"),
("fi", "Suomi", "Finnish"),
("fr", "Français", "French"),
("ha", "Hausa", "Hausa"),
("he", "עברית", "Hebrew"),
("hi", "हिन्दी", "Hindi"),
("hu", "Magyar", "Hungarian"),
("id", "Bahasa Indonesia", "Indonesian"),
("it", "Italiano", "Italian"),
("ja", "日本語", "Japanese"),
("ko", "한국어", "Korean"),
("mr", "मराठी", "Marathi"),
("nb", "Norsk bokmål", "Norwegian Bokmål"),
("nl", "Nederlands", "Dutch"),
("pl", "Polski", "Polish"),
("pt-PT", "Português (Portugal)", "Portuguese (Portugal)"),
("pt", "Português (Brasil)", "Portuguese (Brazil)"),
("ru", "Русский", "Russian"),
("sv", "Svenska", "Swedish"),
("sw", "Kiswahili", "Swahili"),
("ta", "தமிழ்", "Tamil"),
("te", "తెలుగు", "Telugu"),
("th", "ไทย", "Thai"),
("tr", "Türkçe", "Turkish"),
("uk", "Українська", "Ukrainian"),
("ur", "اردو", "Urdu"),
("vi", "Tiếng Việt", "Vietnamese"),
("zh-CN", "简体中文", "Simplified Chinese"),
("zh-TW", "繁體中文", "Traditional Chinese"),
];
fn current() -> &'static RwLock<String> {
static L: OnceLock<RwLock<String>> = OnceLock::new();
L.get_or_init(|| RwLock::new("zh-TW".into()))
}
/// "auto" follows the system language.
pub fn set(tag: &str) {
let t = if tag == "auto" || tag.is_empty() { system() } else { tag.to_string() };
*current().write().unwrap() = t;
}
pub fn lang() -> String {
current().read().unwrap().clone()
}
/// The Mac's first preferred language, as one of LANGS (Traditional Chinese when unknown).
pub fn system() -> String {
if !cfg!(target_os = "macos") {
// Linux: LANGUAGE (a list), then LC_ALL / LC_MESSAGES / LANG ("zh_TW.UTF-8").
let first = ["LANGUAGE", "LC_ALL", "LC_MESSAGES", "LANG"]
.iter()
.filter_map(|k| std::env::var(k).ok())
.map(|v| v.split(':').next().unwrap_or("").split('.').next().unwrap_or("").to_string())
.find(|v| !v.is_empty() && v != "C" && v != "POSIX")
.unwrap_or_else(|| "zh-Hant-TW".into());
return match_tag(&first);
}
let out = std::process::Command::new("defaults").args(["read", "-g", "AppleLanguages"]).output().map(|o| String::from_utf8_lossy(&o.stdout).to_string()).unwrap_or_default();
let first = out.split('"').nth(1).unwrap_or("zh-Hant-TW").to_string();
match_tag(&first)
}
pub fn match_tag(code: &str) -> String {
let c = code.replace('_', "-");
if c.starts_with("zh-Hans") || c == "zh-CN" || c == "zh-SG" {
return "zh-CN".into();
}
if c.starts_with("zh") {
return "zh-TW".into();
}
if c.starts_with("pt-PT") {
return "pt-PT".into();
}
let base = c.split('-').next().unwrap_or("");
LANGS.iter().find(|l| l.0 == base).map(|l| l.0.to_string()).unwrap_or_else(|| "zh-TW".into())
}
pub fn english_name(tag: &str) -> &'static str {
LANGS.iter().find(|l| l.0 == tag).map(|l| l.2).unwrap_or("Traditional Chinese")
}
pub fn endonym(tag: &str) -> &'static str {
LANGS.iter().find(|l| l.0 == tag).map(|l| l.1).unwrap_or("繁體中文")
}
/// Added to every bot's instructions: answer in the chosen language ("auto" = the person's).
pub fn reply_instruction(setting: &str) -> String {
if setting == "auto" || setting.is_empty() {
return "回答時使用使用者所用的語言。".into();
}
match setting {
"zh-TW" => "一律用繁體中文回答(台灣用語)。".into(),
"zh-CN" => "一律用简体中文回答。".into(),
t => format!("Always answer in {} ({}), whatever language the instructions are written in.", english_name(t), endonym(t)),
}
}
fn cache() -> &'static Mutex<HashMap<String, String>> {
static C: OnceLock<Mutex<HashMap<String, String>>> = OnceLock::new();
C.get_or_init(|| Mutex::new(HashMap::new()))
}
/// A window string in the chosen language.
pub fn t(s: &str) -> String {
crate::platform::localize(&t_raw(s))
}
fn t_raw(s: &str) -> String {
let l = lang();
if l == "zh-TW" || s.is_empty() {
return s.to_string();
}
let key = format!("{l}\u{1}{s}");
if let Some(v) = cache().lock().unwrap().get(&key) {
return v.clone();
}
let v = match l.as_str() {
"zh-CN" => to_simplified(s),
"ja" => table(s, 2).or_else(|| table(s, 1)).unwrap_or_else(|| s.to_string()),
_ => table(s, 1).unwrap_or_else(|| s.to_string()),
};
cache().lock().unwrap().insert(key, v.clone());
v
}
fn table(s: &str, col: usize) -> Option<String> {
static M: OnceLock<HashMap<&'static str, (&'static str, &'static str)>> = OnceLock::new();
let m = M.get_or_init(|| crate::i18n_table::ROWS.iter().map(|r| (r.0, (r.1, r.2))).collect());
m.get(s).map(|(en, ja)| if col == 2 { ja.to_string() } else { en.to_string() }).filter(|v| !v.is_empty())
}
/// Traditional → Simplified with macOS's own converter (ICU transform "Hant-Hans").
#[cfg(target_os = "macos")]
pub fn to_simplified(s: &str) -> String {
use std::ffi::{c_char, c_void, CString};
type CFTypeRef = *const c_void;
#[repr(C)]
struct CFRange {
location: isize,
length: isize,
}
#[link(name = "CoreFoundation", kind = "framework")]
extern "C" {
fn CFStringCreateWithCString(alloc: CFTypeRef, s: *const c_char, enc: u32) -> CFTypeRef;
fn CFStringCreateMutableCopy(alloc: CFTypeRef, max: isize, s: CFTypeRef) -> CFTypeRef;
fn CFStringTransform(s: CFTypeRef, range: *mut CFRange, transform: CFTypeRef, reverse: u8) -> u8;
fn CFStringGetLength(s: CFTypeRef) -> isize;
fn CFStringGetMaximumSizeForEncoding(len: isize, enc: u32) -> isize;
fn CFStringGetCString(s: CFTypeRef, buf: *mut c_char, size: isize, enc: u32) -> bool;
fn CFRelease(o: CFTypeRef);
}
const UTF8: u32 = 0x0800_0100;
let (Ok(c), Ok(tr)) = (CString::new(s), CString::new("Hant-Hans")) else { return s.to_string() };
unsafe {
let src = CFStringCreateWithCString(std::ptr::null(), c.as_ptr(), UTF8);
let rule = CFStringCreateWithCString(std::ptr::null(), tr.as_ptr(), UTF8);
if src.is_null() || rule.is_null() {
return s.to_string();
}
let m = CFStringCreateMutableCopy(std::ptr::null(), 0, src);
let ok = CFStringTransform(m, std::ptr::null_mut(), rule, 0) != 0;
let size = CFStringGetMaximumSizeForEncoding(CFStringGetLength(m), UTF8) + 1;
let mut buf = vec![0 as c_char; size as usize];
let got = CFStringGetCString(m, buf.as_mut_ptr(), size, UTF8);
CFRelease(m);
CFRelease(src);
CFRelease(rule);
if !ok || !got {
return s.to_string();
}
std::ffi::CStr::from_ptr(buf.as_ptr()).to_string_lossy().to_string()
}
}
/// Traditional → Simplified elsewhere: zhconv (OpenCC and MediaWiki tables, pure Rust).
#[cfg(not(target_os = "macos"))]
pub fn to_simplified(s: &str) -> String {
zhconv::zhconv(s, zhconv::Variant::ZhHans)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn languages_and_tables() {
assert_eq!(LANGS.len(), 37);
assert_eq!(match_tag("zh-Hant-TW"), "zh-TW");
assert_eq!(match_tag("zh-Hans-CN"), "zh-CN");
assert_eq!(match_tag("ja-JP"), "ja");
assert_eq!(match_tag("pt-PT"), "pt-PT");
assert_eq!(to_simplified("設定電腦與權限"), "设定电脑与权限");
set("en");
assert_eq!(t("一般"), "General");
set("ja");
assert_eq!(t("一般"), "一般");
assert_eq!(t("權限"), "権限");
set("de");
assert_eq!(t("權限"), "Permissions");
set("zh-TW");
assert_eq!(t("權限"), "權限");
assert!(reply_instruction("ko").contains("Korean"));
// Every row has an English translation.
for r in crate::i18n_table::ROWS {
assert!(!r.1.is_empty() && !r.2.is_empty(), "{}", r.0);
}
}
}