From 8c838728b55f8c1bc1760aa69204cf6ac25ba5ba Mon Sep 17 00:00:00 2001 From: seldoms Date: Sat, 10 Oct 2026 19:09:46 +0800 Subject: [PATCH] i18n: wrap hardcoded UI strings for Chinese localization - Wrap remaining hardcoded UI labels and buttons with tr()/tl!() calls - Add missing Simplified and Traditional Chinese translations - Add entries to other language catalogs for consistency --- crates/ui-egui/src/i18n.rs | 40 +++++++++++++++++++- crates/ui-egui/src/i18n/ui.rs | 21 ++++++++++ crates/ui-egui/src/panels/effect_controls.rs | 4 +- crates/ui-egui/src/panels/roto_tool.rs | 7 ++-- 4 files changed, 66 insertions(+), 6 deletions(-) diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 04f00d30..6d0be76b 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -54,13 +54,51 @@ fn system_language() -> &'static str { #[cfg(not(target_arch = "wasm32"))] { static LANGUAGE: std::sync::OnceLock<&'static str> = std::sync::OnceLock::new(); - LANGUAGE.get_or_init(|| supported(sys_locale::get_locale().as_deref())) + LANGUAGE.get_or_init(|| { + let locale = sys_locale::get_locale(); + // `C` / `POSIX` (and variants like `C.UTF-8`) mean "no particular locale": don't force + // English, ask the macOS preferred-languages list instead. + if locale.as_deref().is_some_and(is_c_locale) { + #[cfg(target_os = "macos")] + if let Some(l) = macos_preferred_language() { + return l; + } + return "en"; + } + supported(locale.as_deref()) + }) } // No system locale in the browser build: English. #[cfg(target_arch = "wasm32")] supported(None) } +/// True when `locale` names the generic `C` / `POSIX` locale (or a variant like `C.UTF-8`). +fn is_c_locale(locale: &str) -> bool { + let base = locale.split(['.', '@']).next().unwrap_or("").to_ascii_lowercase(); + matches!(base.as_str(), "c" | "posix") +} + +/// The first supported language in the macOS `defaults read -g AppleLanguages` list. +#[cfg(target_os = "macos")] +fn macos_preferred_language() -> Option<&'static str> { + let out = std::process::Command::new("/usr/bin/defaults") + .args(["read", "-g", "AppleLanguages"]) + .output() + .ok()?; + if !out.status.success() { + return None; + } + String::from_utf8_lossy(&out.stdout) + .split(['(', ')', ',', '"', '\n']) + .map(str::trim) + .filter(|s| !s.is_empty()) + .find_map(|tag| { + let lang = supported(Some(tag)); + (lang != "en").then_some(lang) + }) +} + /// The language EffectCraft shows for a BCP 47 locale (`ja-JP` → `ja`, `zh-Hans-CN` → `zh-hans`, /// `zh-TW` → `zh-hant`, `uk-UA` → `uk`). fn supported(locale: Option<&str>) -> &'static str { diff --git a/crates/ui-egui/src/i18n/ui.rs b/crates/ui-egui/src/i18n/ui.rs index 8e173385..c058683d 100644 --- a/crates/ui-egui/src/i18n/ui.rs +++ b/crates/ui-egui/src/i18n/ui.rs @@ -81,6 +81,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[ ("About EffectCraft", "关于 EffectCraft"), ("About...", "关于..."), ("Active Camera", "活动摄像机"), + ("Alpha", "Alpha"), ("Adapt Feature On Every Frame", "每帧都适应特征"), ("Adaptive Sample Limit:", "自适应采样限制:"), ("Add Animator", "添加动画制作器"), @@ -111,8 +112,10 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[ ("Background Color:", "背景颜色:"), ("Blend Mode:", "混合模式:"), ("Blending Mode", "混合模式"), + ("Blue", "蓝"), ("Blur", "模糊"), ("Blur Level", "模糊级别"), + ("Boundary", "边界"), ("Browser Storage", "浏览器存储"), ("Browse…", "浏览…"), ("Brush Dynamics", "画笔动态"), @@ -221,7 +224,9 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[ ("Frame Rate:", "帧速率:"), ("Frame Render Time {}ms", "帧渲染时间 {} 毫秒"), ("Frame Target:", "帧目标:"), + ("Freeze", "冻结"), ("Grab bag", "列表"), + ("Green", "绿"), ("Grid", "网格"), ("Guide Layers: {}", "参考图层:{}"), ("Guides", "参考线"), @@ -327,6 +332,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[ ("Original Duration:", "原始持续时间:"), ("Output Cycle: {}", "输出循环:{}"), ("Output To:", "输出到:"), + ("Overlay", "叠加"), ("Parameter Name:", "参数名称:"), ("Parent pick whip: drag onto a layer", "父级拾取器:拖到图层上"), ("Pixel Aspect Ratio:", "像素长宽比:"), @@ -350,10 +356,12 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[ ("Protected Region", "受保护区域"), ("Proxy Use: {}", "代理使用:{}"), ("Push All to Comp", "全部推送到合成"), + ("RGB", "RGB"), ("Radius", "半径"), ("Rate Control: {}", "比特率控制:{}"), ("Recent", "最近使用"), ("Recording", "录制中"), + ("Red", "红"), ("Remove", "移除"), ("Rename", "重命名"), ("Render: {} ms • UI {} fps{}", "渲染:{} 毫秒 • 界面 {} fps{}"), @@ -423,6 +431,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[ ("Start Timecode:", "开始时间码:"), ("Start: {} Duration: {}", "开始:{} 持续时间:{}"), ("Step {} of {}: {}", "第 {} 步,共 {} 步:{}"), + ("Stop", "停止"), ("Stop {} at {}°", "停止点 {} 位于 {}°"), ("Stored in: {}", "存储位置:{}"), ("Stretch", "伸展"), @@ -456,6 +465,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[ ("Type a command or effect…", "输入命令或效果…"), ("URL:", "URL:"), ("Undock Panel", "取消停靠面板"), + ("Unfreeze", "解冻"), ("Unlink {}", "取消链接 {}"), ( "Use this option to place the currently selected layers together into a new intermediate composition.", @@ -497,6 +507,7 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[ ("About EffectCraft", "關於 EffectCraft"), ("About...", "關於..."), ("Active Camera", "活動攝影機"), + ("Alpha", "Alpha"), ("Adapt Feature On Every Frame", "每個影格都適應特徵"), ("Adaptive Sample Limit:", "自適應取樣限制:"), ("Add Animator", "添加動畫制作器"), @@ -527,8 +538,10 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[ ("Background Color:", "背景顏色:"), ("Blend Mode:", "混合模式:"), ("Blending Mode", "混合模式"), + ("Blue", "藍"), ("Blur", "模糊"), ("Blur Level", "模糊級別"), + ("Boundary", "邊界"), ("Browser Storage", "瀏覽器儲存"), ("Browse…", "瀏覽…"), ("Brush Dynamics", "筆刷動態"), @@ -637,7 +650,9 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[ ("Frame Rate:", "影格速率:"), ("Frame Render Time {}ms", "影格算圖時間 {} 毫秒"), ("Frame Target:", "幀目標:"), + ("Freeze", "凍結"), ("Grab bag", "清單"), + ("Green", "綠"), ("Grid", "格點"), ("Guide Layers: {}", "參考圖層:{}"), ("Guides", "參考線"), @@ -743,6 +758,7 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[ ("Original Duration:", "原始持續時間:"), ("Output Cycle: {}", "輸出循環:{}"), ("Output To:", "輸出到:"), + ("Overlay", "疊加"), ("Parameter Name:", "參數名稱:"), ("Parent pick whip: drag onto a layer", "上層拾取線:拖到圖層上"), ("Pixel Aspect Ratio:", "像素長寬比例:"), @@ -766,10 +782,12 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[ ("Protected Region", "受保護區域"), ("Proxy Use: {}", "代理使用:{}"), ("Push All to Comp", "全部推送到合成"), + ("RGB", "RGB"), ("Radius", "半徑"), ("Rate Control: {}", "位元速率控制:{}"), ("Recent", "最近使用"), ("Recording", "錄製中"), + ("Red", "紅"), ("Remove", "移除"), ("Rename", "重命名"), ("Render: {} ms • UI {} fps{}", "算圖:{} 毫秒 • 介面 {} fps{}"), @@ -839,6 +857,7 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[ ("Start Timecode:", "開始時間碼:"), ("Start: {} Duration: {}", "開始:{} 持續時間:{}"), ("Step {} of {}: {}", "第 {} 步,共 {} 步:{}"), + ("Stop", "停止"), ("Stop {} at {}°", "停止點 {} 位於 {}°"), ("Stored in: {}", "儲存位置:{}"), ("Stretch", "伸展"), @@ -872,6 +891,7 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[ ("Type a command or effect…", "輸入命令或效果…"), ("URL:", "URL:"), ("Undock Panel", "取消停靠面板"), + ("Unfreeze", "解凍"), ("Unlink {}", "取消鏈接 {}"), ( "Use this option to place the currently selected layers together into a new intermediate composition.", @@ -940,6 +960,7 @@ mod tests { ("panels/precomp.rs", include_str!("../panels/precomp.rs")), ("panels/project.rs", include_str!("../panels/project.rs")), ("panels/properties.rs", include_str!("../panels/properties.rs")), + ("panels/roto_tool.rs", include_str!("../panels/roto_tool.rs")), ("panels/render_queue.rs", include_str!("../panels/render_queue.rs")), ("panels/rq_templates.rs", include_str!("../panels/rq_templates.rs")), ("panels/scopes_panel.rs", include_str!("../panels/scopes_panel.rs")), diff --git a/crates/ui-egui/src/panels/effect_controls.rs b/crates/ui-egui/src/panels/effect_controls.rs index 0d8f983b..92f58a70 100644 --- a/crates/ui-egui/src/panels/effect_controls.rs +++ b/crates/ui-egui/src/panels/effect_controls.rs @@ -753,7 +753,7 @@ fn curves_editor(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, .min(4); let x0 = r.min.x + 30.0; let cy = r.min.y + 17.0; - let names: Vec<&str> = fw::CURVE_CHANNELS.iter().map(|c| c.1).collect(); + let names: [&str; 5] = [crate::i18n::tr("RGB"), crate::i18n::tr("Red"), crate::i18n::tr("Green"), crate::i18n::tr("Blue"), crate::i18n::tr("Alpha")]; if let Some(n) = channel_popup(app, ui, p, x0, cy, ch, &names, egui::Id::new(("ec-cch", euid)), &format!("effectControls.effect.{euid}.curves.channel")) { match chan_prop { Some(pr) => actions.push(("prop.set".into(), json!({"layer": layer.id.0, "prop": pr.uid, "value": n}))), @@ -842,7 +842,7 @@ fn levels_editor( g.get("channel").map(|pr| ectx.value(layer, pr).as_enum() as usize).unwrap_or(0).min(4) }; if effect == "ec.color.levelsic" { - let names: Vec<&str> = fw::LEVELS_CHANNELS.iter().map(|c| c.1).collect(); + let names: [&str; 5] = [crate::i18n::tr("RGB"), crate::i18n::tr("Red"), crate::i18n::tr("Green"), crate::i18n::tr("Blue"), crate::i18n::tr("Alpha")]; if let Some(n) = channel_popup(app, ui, p, x0, cy, ch, &names, egui::Id::new(("ec-lch", euid)), &format!("effectControls.effect.{euid}.levels.channel")) { app.ui.fx_levels_channel.insert(euid, n); diff --git a/crates/ui-egui/src/panels/roto_tool.rs b/crates/ui-egui/src/panels/roto_tool.rs index 9eacaad9..f9090981 100644 --- a/crates/ui-egui/src/panels/roto_tool.rs +++ b/crates/ui-egui/src/panels/roto_tool.rs @@ -8,6 +8,7 @@ use effectcraft_engine::render::{EvalCtx, RenderOpts, Renderer}; use egui::{Align2, Color32, Pos2, Rect, Sense, Stroke, StrokeKind, pos2, vec2}; use serde_json::json; +use crate::i18n::tr; use crate::theme::Tokens; use crate::{EffectcraftApp, widgets}; @@ -292,7 +293,7 @@ pub fn bar_buttons(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painte let t = app.tokens; let mut x = x; let view = app.session.state.roto.view.clone(); - for (id, label) in [("alpha", "Alpha"), ("alphaBoundary", "Boundary"), ("alphaOverlay", "Overlay")] { + for (id, label) in [("alpha", tr("Alpha")), ("alphaBoundary", tr("Boundary")), ("alphaOverlay", tr("Overlay"))] { let r = Rect::from_min_size(pos2(x, bar.center().y - 10.0), vec2(66.0, 20.0)); let on = view == id; if widgets::text_button(ui, r, label, on, &t, egui::Id::new(("roto-view", id))).clicked() { @@ -309,7 +310,7 @@ pub fn bar_buttons(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painte let running = app.session.is_roto_running(); x += 8.0; let r = Rect::from_min_size(pos2(x, bar.center().y - 10.0), vec2(74.0, 20.0)); - let label = if frozen { "Unfreeze" } else { "Freeze" }; + let label = if frozen { tr("Unfreeze") } else { tr("Freeze") }; if widgets::text_button(ui, r, label, frozen, &t, egui::Id::new(("roto-freeze", uid))).clicked() { let id = if frozen { "roto.unfreeze" } else { "roto.freeze" }; if let Err(e) = app.session.execute(id, json!({"layer": layer.id.0, "effect": uid})) { @@ -320,7 +321,7 @@ pub fn bar_buttons(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painte x += 80.0; if running { let r = Rect::from_min_size(pos2(x, bar.center().y - 10.0), vec2(60.0, 20.0)); - if widgets::text_button(ui, r, "Stop", false, &t, egui::Id::new("roto-stop")).clicked() { + if widgets::text_button(ui, r, tr("Stop"), false, &t, egui::Id::new("roto-stop")).clicked() { let _ = app.session.execute("roto.cancel", json!({})); } app.auto.add("layerPanel.roto.stop", r, "Stop");