mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Settings: Language ▸ Match System follows the system language (#229)
The interface language was English unless Japanese was chosen in Settings ▸ General ▸ Language. Language now has a Match System choice (`general.language` = `system`), the new default: the operating system's interface language where EffectCraft has a translation (Japanese), else English, as After Effects installs in the system's language. English and Japanese can still be chosen; a language chosen before stays. The browser build stays in English with Match System, since it has no Japanese font of its own. The system language comes from sys-locale (MIT OR Apache-2.0, no dependencies on the desktop platforms), on desktop builds only. Tests: match_system_uses_the_system_language_where_there_is_a_catalog (i18n), interface_language_is_validated_persisted_and_backward_compatible (engine). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
bb3e63e8f3
commit
fd572949f3
7 files changed
+84
-13
No files matched your search
Generated
+10
@@ -1624,6 +1624,7 @@ dependencies = [
|
||||
"rayon",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sys-locale",
|
||||
"toml",
|
||||
"web-time",
|
||||
]
|
||||
@@ -4949,6 +4950,15 @@ dependencies = [
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sys-locale"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tag_ptr"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -103,6 +103,8 @@ eframe = { version = "0.36", default-features = false }
|
||||
egui_kittest = "0.36"
|
||||
rfd = "0.17"
|
||||
cpal = "0.16"
|
||||
# The operating system's interface language (MIT/Apache-2.0, no dependencies on desktop).
|
||||
sys-locale = "0.3"
|
||||
|
||||
# Local co-development against a FilmCraft checkout next to this repo: uncomment.
|
||||
# [patch."https://github.com/storytold/filmcraft"]
|
||||
|
||||
@@ -22,6 +22,13 @@ pub const PREFS_VERSION: u32 = 2;
|
||||
/// Settings file name in the config store.
|
||||
pub const PREFS_FILE: &str = "prefs.json";
|
||||
|
||||
/// Settings ▸ General ▸ Language: (label, `general.language` value).
|
||||
pub const LANGUAGES: &[(&str, &str)] = &[("Match System", "system"), ("English", "en"), ("日本語", "ja")];
|
||||
|
||||
fn is_language(v: &str) -> bool {
|
||||
LANGUAGES.iter().any(|(_, l)| *l == v)
|
||||
}
|
||||
|
||||
/// Unknown keys inside a page, kept so a newer version's settings survive a round trip.
|
||||
type Extra = BTreeMap<String, Value>;
|
||||
|
||||
@@ -44,8 +51,9 @@ macro_rules! page {
|
||||
}
|
||||
|
||||
page!(General {
|
||||
/// Interface language (`en` or `ja`).
|
||||
language: String = "en".into(),
|
||||
/// Interface language: `system` (the operating system's, where EffectCraft has it, else
|
||||
/// English), `en` or `ja`.
|
||||
language: String = "system".into(),
|
||||
/// Levels of Undo (1–99).
|
||||
undo_levels: u32 = 32,
|
||||
/// Path Point and Handle Size (px).
|
||||
@@ -417,8 +425,8 @@ impl Prefs {
|
||||
pub fn normalize(&mut self) {
|
||||
self.version = PREFS_VERSION;
|
||||
let g = &mut self.general;
|
||||
if !matches!(g.language.as_str(), "en" | "ja") {
|
||||
g.language = "en".into();
|
||||
if !is_language(&g.language) {
|
||||
g.language = General::default().language;
|
||||
}
|
||||
g.undo_levels = g.undo_levels.clamp(1, 99);
|
||||
g.path_point_size = g.path_point_size.clamp(3, 20);
|
||||
@@ -458,8 +466,9 @@ impl Prefs {
|
||||
|
||||
/// Set the value at a dotted key. The key must exist and the value must have its type.
|
||||
pub fn set(&mut self, key: &str, value: Value) -> Result<(), String> {
|
||||
if key == "general.language" && !matches!(value.as_str(), Some("en" | "ja")) {
|
||||
return Err("`general.language` expects `en` or `ja`".into());
|
||||
if key == "general.language" && !value.as_str().is_some_and(is_language) {
|
||||
let all: Vec<&str> = LANGUAGES.iter().map(|(_, l)| *l).collect();
|
||||
return Err(format!("`general.language` expects one of {}", all.join(", ")));
|
||||
}
|
||||
let mut v = serde_json::to_value(&*self).map_err(|e| e.to_string())?;
|
||||
let ptr = format!("/{}", key.replace('.', "/"));
|
||||
@@ -885,7 +894,7 @@ pub fn pages() -> Vec<Page> {
|
||||
id: "general",
|
||||
title: "General",
|
||||
items: vec![
|
||||
s("general.language", "Language", Kind::Choice(&[("English", "en"), ("日本語", "ja")]), true),
|
||||
s("general.language", "Language", Kind::Choice(LANGUAGES), true),
|
||||
s("general.undoLevels", "Levels of Undo", Kind::Int(1, 99, ""), true),
|
||||
s("general.pathPointSize", "Path Point and Handle Size", Kind::Int(3, 20, "px"), true),
|
||||
s("general.recentItems", "Recent Projects Shown", Kind::Int(1, 30, ""), true),
|
||||
|
||||
@@ -491,7 +491,8 @@ fn memory_tick_never_waits_for_the_system() {
|
||||
fn interface_language_is_validated_persisted_and_backward_compatible() {
|
||||
let store = Arc::new(MemoryConfig::default());
|
||||
let mut s = Session { config: Some(store.clone()), ..Default::default() };
|
||||
assert_eq!(s.prefs.general.language, "en");
|
||||
assert_eq!(s.prefs.general.language, "system", "Match System by default (#229)");
|
||||
s.execute("prefs.set", json!({"key": "general.language", "value": "en"})).unwrap();
|
||||
s.execute("prefs.set", json!({"key": "general.language", "value": "ja"})).unwrap();
|
||||
assert_eq!(s.execute("prefs.get", json!({"key": "general.language"})).unwrap(), json!("ja"));
|
||||
let saved = store.read(PREFS_FILE).unwrap();
|
||||
@@ -504,9 +505,10 @@ fn interface_language_is_validated_persisted_and_backward_compatible() {
|
||||
reloaded.load_settings();
|
||||
assert_eq!(reloaded.prefs.general.language, "ja");
|
||||
reloaded.execute("prefs.reset", json!({"page": "general"})).unwrap();
|
||||
assert_eq!(reloaded.prefs.general.language, "en");
|
||||
assert_eq!(Prefs::from_json(r#"{"general":{"undoLevels":17}}"#).general.language, "en");
|
||||
assert_eq!(Prefs::from_json(r#"{"general":{"language":"unknown"}}"#).general.language, "en");
|
||||
assert_eq!(reloaded.prefs.general.language, "system");
|
||||
assert_eq!(Prefs::from_json(r#"{"general":{"undoLevels":17}}"#).general.language, "system");
|
||||
assert_eq!(Prefs::from_json(r#"{"general":{"language":"unknown"}}"#).general.language, "system");
|
||||
assert_eq!(Prefs::from_json(r#"{"general":{"language":"en"}}"#).general.language, "en", "a chosen language stays");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -30,6 +30,9 @@ web-time = { workspace = true }
|
||||
kurbo = { workspace = true }
|
||||
image = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
sys-locale = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
egui_kittest = { workspace = true, features = ["eframe", "wgpu"] }
|
||||
pollster = "0.4"
|
||||
|
||||
@@ -7,7 +7,31 @@ use crate::EffectcraftApp;
|
||||
use effectcraft_engine::menus::MenuEntry;
|
||||
|
||||
pub(crate) fn japanese(app: &EffectcraftApp) -> bool {
|
||||
app.session.prefs.general.language == "ja"
|
||||
match app.session.prefs.general.language.as_str() {
|
||||
"system" => system_language() == "ja",
|
||||
l => l == "ja",
|
||||
}
|
||||
}
|
||||
|
||||
/// Settings ▸ General ▸ Language ▸ Match System: the operating system's interface language
|
||||
/// where EffectCraft has it, else English (#229), as After Effects installs in the system's
|
||||
/// language. The browser build stays in English: it has no Japanese font of its own.
|
||||
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()))
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
"en"
|
||||
}
|
||||
|
||||
/// The language EffectCraft shows for a BCP 47 locale (`ja-JP` → `ja`).
|
||||
fn supported(locale: Option<&str>) -> &'static str {
|
||||
match locale.and_then(|l| l.split(['-', '_']).next()) {
|
||||
Some(l) if l.eq_ignore_ascii_case("ja") => "ja",
|
||||
_ => "en",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn label<'a>(app: &EffectcraftApp, command: &str, source: &'a str) -> &'a str {
|
||||
@@ -723,6 +747,7 @@ mod tests {
|
||||
#[test]
|
||||
fn language_changes_native_labels_without_changing_commands_or_parameters() {
|
||||
let mut app = EffectcraftApp::new(effectcraft_engine::Session::default());
|
||||
app.session.execute("prefs.set", json!({"key":"general.language", "value":"en"})).unwrap();
|
||||
let en = crate::native_menu::build(&app);
|
||||
let state = crate::native_menu::state_key(&app);
|
||||
app.session.execute("prefs.set", json!({"key":"general.language", "value":"ja"})).unwrap();
|
||||
@@ -749,6 +774,26 @@ mod tests {
|
||||
app.session.execute("prefs.set", json!({"key":"general.language", "value":"en"})).unwrap();
|
||||
assert_eq!(label(&app, "", "Composition"), "Composition");
|
||||
}
|
||||
#[test]
|
||||
fn match_system_uses_the_system_language_where_there_is_a_catalog() {
|
||||
for (locale, language) in [
|
||||
(Some("ja-JP"), "ja"),
|
||||
(Some("ja"), "ja"),
|
||||
(Some("JA_jp.UTF-8"), "ja"),
|
||||
(Some("en-US"), "en"),
|
||||
(Some("jv-ID"), "en"),
|
||||
(Some("de-DE"), "en"),
|
||||
(None, "en"),
|
||||
] {
|
||||
assert_eq!(supported(locale), language, "{locale:?}");
|
||||
}
|
||||
let mut app = EffectcraftApp::new(effectcraft_engine::Session::default());
|
||||
assert_eq!(app.session.prefs.general.language, "system", "the default");
|
||||
assert_eq!(japanese(&app), system_language() == "ja");
|
||||
app.session.execute("prefs.set", json!({"key":"general.language", "value":"ja"})).unwrap();
|
||||
assert!(japanese(&app));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn general_settings_exposes_the_language_automation_id() {
|
||||
let mut app = EffectcraftApp::new(effectcraft_engine::Session::default());
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ Commands (CLI, MCP, control channel):
|
||||
|
||||
### Settings that change behaviour
|
||||
|
||||
- `general.language`: interface language (`en` / `ja`), Settings ▸ General ▸ Language; menu labels change immediately. Native Japanese UI uses installed system fonts; the web host must supply a Japanese font.
|
||||
- `general.language`: interface language (`system` / `en` / `ja`), Settings ▸ General ▸ Language; menu labels change immediately. `system` (Match System, the default) follows the operating system's interface language where EffectCraft has a translation and is English otherwise; the browser build stays in English with it. Native Japanese UI uses installed system fonts; the web host must supply a Japanese font.
|
||||
- `general.undoLevels`: Levels of Undo
|
||||
- `general.pathPointSize`: Path Point and Handle Size
|
||||
- `general.recentItems`: Recent Projects Shown
|
||||
|
||||
Reference in new issue
Block a user