2026-11-09 08:37:33 +00:00
|
|
|
// 語言: the menus' language and the language bots answer in (「選擇選單和訊息使用的語言」,
|
|
|
|
|
// 37 languages + automatic).
|
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
|
|
|
//
|
|
|
|
|
// 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()))
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-05 06:46:42 +00:00
|
|
|
/// Replies are wanted in Traditional Chinese (the language setting says so, not "auto").
|
|
|
|
|
static REPLIES_HANT: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
|
|
|
|
|
|
|
|
|
/// What the bot wrote, in Traditional Chinese with Taiwan wording when that is the setting.
|
|
|
|
|
/// Models (Qwen especially) slip into Simplified characters despite the instruction.
|
|
|
|
|
pub fn reply_text(s: &str) -> String {
|
|
|
|
|
if REPLIES_HANT.load(std::sync::atomic::Ordering::Relaxed) && !s.is_empty() {
|
|
|
|
|
zhconv::zhconv(s, zhconv::Variant::ZhTW)
|
|
|
|
|
} else {
|
|
|
|
|
s.to_string()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
|
|
|
/// "auto" follows the system language.
|
|
|
|
|
pub fn set(tag: &str) {
|
2026-11-05 06:46:42 +00:00
|
|
|
REPLIES_HANT.store(tag == "zh-TW", std::sync::atomic::Ordering::Relaxed);
|
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
|
|
|
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");
|
2026-11-05 06:46:42 +00:00
|
|
|
set("zh-TW");
|
|
|
|
|
assert_eq!(reply_text("那边的软件"), "那邊的軟體");
|
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|