mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Merge pull request #312 from FeiLiuEM/font/cjk-locale
text, ui: the CJK fallback fonts follow the interface language
This commit is contained in:
commit
a990432962
11 files changed
+213
-38
No files matched your search
+62
-11
@@ -569,17 +569,46 @@ const CHINESE_FAMILIES: &[&str] = &[
|
||||
];
|
||||
const KOREAN_FAMILIES: &[&str] = &["Malgun Gothic", "Gulim", "Apple SD Gothic Neo", "Noto Sans CJK KR", "Noto Sans KR", "Source Han Sans KR", "NanumGothic"];
|
||||
|
||||
/// The CJK language to try first for Han ideographs, from the locale environment (`LANG`,
|
||||
/// `LC_ALL`, `LC_CTYPE`; `EFFECTCRAFT_CJK_LOCALE` overrides them). Japanese when nothing says
|
||||
/// otherwise: the ambiguity only matters for ideographs, and kana / hangul in the same text pick
|
||||
/// their language regardless.
|
||||
fn han_order() -> [FallbackScript; 3] {
|
||||
let tag = ["EFFECTCRAFT_CJK_LOCALE", "LC_ALL", "LC_CTYPE", "LANG"]
|
||||
.iter()
|
||||
.filter_map(|k| std::env::var(k).ok())
|
||||
.map(|v| v.trim().to_ascii_lowercase())
|
||||
.find(|v| !v.is_empty() && v != "c" && v != "posix")
|
||||
.unwrap_or_default();
|
||||
/// The CJK locale the host asked fallbacks to prefer, when it has one (see [`set_cjk_locale`]).
|
||||
static REQUESTED_CJK_LOCALE: RwLock<Option<String>> = RwLock::new(None);
|
||||
|
||||
/// Ask CJK font fallbacks to prefer `tag`'s language for Han ideographs instead of the locale
|
||||
/// environment, so a Chinese interface is drawn with a Chinese face on a machine whose locale is
|
||||
/// Japanese (and the other way round). The interface sets this from its language setting; an empty
|
||||
/// tag, `c` or `posix` clears the request and returns to the environment. Process-global, like the
|
||||
/// environment it replaces: every layout in the process follows it, so composition text is drawn
|
||||
/// with the same language's Han forms as the interface, not only the interface chrome.
|
||||
pub fn set_cjk_locale(tag: &str) {
|
||||
let tag = tag.trim().to_ascii_lowercase();
|
||||
let requested = if tag.is_empty() || tag == "c" || tag == "posix" { None } else { Some(tag) };
|
||||
*REQUESTED_CJK_LOCALE.write().unwrap_or_else(|e| e.into_inner()) = requested;
|
||||
// The fallback cache answers per character block and preferred face, so a change of language
|
||||
// must drop it.
|
||||
fallback_cache().write().unwrap_or_else(|e| e.into_inner()).clear();
|
||||
}
|
||||
|
||||
fn requested_cjk_locale() -> Option<String> {
|
||||
REQUESTED_CJK_LOCALE.read().unwrap_or_else(|e| e.into_inner()).clone()
|
||||
}
|
||||
|
||||
/// The CJK locale to use: [`set_cjk_locale`] when the host asked for one, otherwise the locale
|
||||
/// environment (`LANG`, `LC_ALL`, `LC_CTYPE`; `EFFECTCRAFT_CJK_LOCALE` overrides them).
|
||||
fn active_cjk_locale() -> String {
|
||||
requested_cjk_locale().unwrap_or_else(|| {
|
||||
["EFFECTCRAFT_CJK_LOCALE", "LC_ALL", "LC_CTYPE", "LANG"]
|
||||
.iter()
|
||||
.filter_map(|k| std::env::var(k).ok())
|
||||
.map(|v| v.trim().to_ascii_lowercase())
|
||||
.find(|v| !v.is_empty() && v != "c" && v != "posix")
|
||||
.unwrap_or_default()
|
||||
})
|
||||
}
|
||||
|
||||
/// The CJK language to try first for Han ideographs. Japanese when nothing says otherwise: the
|
||||
/// ambiguity only matters for ideographs, and kana / hangul in the same text pick their language
|
||||
/// regardless.
|
||||
fn han_order_for(tag: &str) -> [FallbackScript; 3] {
|
||||
let tag = tag.trim().to_ascii_lowercase();
|
||||
if tag.starts_with("zh") {
|
||||
[FallbackScript::Han, FallbackScript::Japanese, FallbackScript::Korean]
|
||||
} else if tag.starts_with("ko") {
|
||||
@@ -589,6 +618,11 @@ fn han_order() -> [FallbackScript; 3] {
|
||||
}
|
||||
}
|
||||
|
||||
/// The order for the locale in force ([`active_cjk_locale`]).
|
||||
fn han_order() -> [FallbackScript; 3] {
|
||||
han_order_for(&active_cjk_locale())
|
||||
}
|
||||
|
||||
fn families_for(s: FallbackScript) -> &'static [&'static str] {
|
||||
match s {
|
||||
FallbackScript::Japanese => JAPANESE_FAMILIES,
|
||||
@@ -921,6 +955,23 @@ mod tests {
|
||||
assert_eq!(fallback_for('\u{e001}', inter), id, "fallback finds the drawn one");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_han_order_follows_the_cjk_locale() {
|
||||
// Chinese first for any Chinese tag, whether or not it names a script.
|
||||
for tag in ["zh", "zh-CN", "zh_Hans", "ZH-Hant-TW.UTF-8", "zh_MO"] {
|
||||
assert_eq!(han_order_for(tag)[0], FallbackScript::Han, "{tag}");
|
||||
}
|
||||
assert_eq!(han_order_for("ko-KR"), [FallbackScript::Korean, FallbackScript::Japanese, FallbackScript::Han]);
|
||||
// Japanese, and anything else, keeps the Japanese face first for Han text.
|
||||
for tag in ["ja_JP.UTF-8", "en-US", "de", ""] {
|
||||
assert_eq!(han_order_for(tag), [FallbackScript::Japanese, FallbackScript::Han, FallbackScript::Korean], "{tag}");
|
||||
}
|
||||
// The host's request is what `han_order` reads (the ui test covers the effect on the
|
||||
// registered fallbacks; this only checks that clearing it returns to the environment).
|
||||
assert_eq!(requested_cjk_locale(), None, "tests run without a request");
|
||||
assert_eq!(han_order(), han_order_for(&active_cjk_locale()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metrics_and_coverage() {
|
||||
let f = face(resolve("Inter", "Regular").face);
|
||||
|
||||
@@ -615,7 +615,7 @@ mod tests {
|
||||
fn long_press_type_keeps_the_tool_until_vertical_is_selected() {
|
||||
let mut app = EffectcraftApp::new(effectcraft_engine::Session::default());
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install(&ctx, &app.tokens);
|
||||
crate::theme::install(&ctx, &app.tokens, crate::i18n::language(&app));
|
||||
frame(&mut app, &ctx, 0.0, vec![]);
|
||||
frame(&mut app, &ctx, 0.1, vec![]);
|
||||
let center = |element: &crate::automation::Element| {
|
||||
|
||||
@@ -2244,7 +2244,7 @@ mod tests {
|
||||
let mut app = EffectcraftApp::new(effectcraft_engine::Session::default());
|
||||
let ctx = egui::Context::default();
|
||||
let tokens = crate::theme::Tokens::for_kind(crate::theme::ThemeKind::Dark);
|
||||
crate::theme::install(&ctx, &tokens);
|
||||
crate::theme::install(&ctx, &tokens, language(&app));
|
||||
crate::panels::settings::open(&mut app, "general");
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |_| {
|
||||
crate::panels::settings::show(&mut app, &ctx, &tokens);
|
||||
|
||||
@@ -213,6 +213,8 @@ pub struct EffectcraftApp {
|
||||
last_time: f64,
|
||||
styled: bool,
|
||||
fonts_ready: bool,
|
||||
/// The interface language the fonts were installed for; `""` until the first frame.
|
||||
styled_language: &'static str,
|
||||
pub(crate) control_rx: Option<Receiver<ControlRequest>>,
|
||||
pub(crate) deferred: Vec<(ControlRequest, f64)>,
|
||||
pub(crate) synthetic: Vec<egui::Event>,
|
||||
@@ -299,6 +301,7 @@ impl EffectcraftApp {
|
||||
last_time: 0.0,
|
||||
styled: false,
|
||||
fonts_ready: false,
|
||||
styled_language: "",
|
||||
control_rx: None,
|
||||
deferred: vec![],
|
||||
synthetic: vec![],
|
||||
@@ -1573,10 +1576,16 @@ impl eframe::App for EffectcraftApp {
|
||||
|
||||
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
self.apply_gpu_failure(ctx);
|
||||
if !self.styled {
|
||||
theme::install(ctx, &self.tokens);
|
||||
fit_window(ctx);
|
||||
self.styled = true;
|
||||
// The CJK fallback fonts depend on the interface language, so they follow a change of
|
||||
// Settings ▸ General ▸ Language instead of only the first frame.
|
||||
let language = crate::i18n::language(self);
|
||||
if !self.styled || self.styled_language != language {
|
||||
theme::install(ctx, &self.tokens, language);
|
||||
self.styled_language = language;
|
||||
if !self.styled {
|
||||
fit_window(ctx);
|
||||
self.styled = true;
|
||||
}
|
||||
ctx.request_repaint();
|
||||
} else {
|
||||
self.fonts_ready = true;
|
||||
|
||||
@@ -548,7 +548,7 @@ mod tests {
|
||||
fn app() -> (EffectcraftApp, egui::Context) {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = EffectcraftApp::new(effectcraft_engine::Session::default());
|
||||
crate::theme::install(&ctx, &app.tokens);
|
||||
crate::theme::install(&ctx, &app.tokens, crate::i18n::language(&app));
|
||||
app.session.execute("comp.new", json!({"name": "Main", "width": 640, "height": 360})).unwrap();
|
||||
(app, ctx)
|
||||
}
|
||||
|
||||
@@ -844,7 +844,7 @@ mod tests {
|
||||
fn premiere_export_dialog() {
|
||||
let mut app = EffectcraftApp::new(effectcraft_engine::Session::default());
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install(&ctx, &app.tokens);
|
||||
crate::theme::install(&ctx, &app.tokens, crate::i18n::language(&app));
|
||||
app.session.execute("comp.new", json!({"name": "Spot", "width": 64, "height": 36, "frameRate": 25, "duration": 1})).unwrap();
|
||||
app.session.execute("layer.newSolid", json!({"color": "#ff0000"})).unwrap();
|
||||
crate::menus::invoke(&mut app, &ctx, "file.exportTimeline", json!({})).unwrap();
|
||||
|
||||
@@ -540,7 +540,7 @@ mod tests {
|
||||
s.prefs.push_recent("/projects/Alpha.ecproj");
|
||||
let mut app = EffectcraftApp::new(s);
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install(&ctx, &app.tokens);
|
||||
crate::theme::install(&ctx, &app.tokens, crate::i18n::language(&app));
|
||||
ctx.run_ui(Default::default(), |_| {}).textures_delta.clear();
|
||||
ctx.run_ui(Default::default(), |ui| {
|
||||
app.auto.begin_frame();
|
||||
@@ -573,7 +573,7 @@ mod tests {
|
||||
app.ui.start_screen = true;
|
||||
app.ui.home_learn = true;
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install(&ctx, &app.tokens);
|
||||
crate::theme::install(&ctx, &app.tokens, crate::i18n::language(&app));
|
||||
let frame = |app: &mut EffectcraftApp| {
|
||||
ctx.run_ui(Default::default(), |ui| {
|
||||
app.auto.begin_frame();
|
||||
|
||||
@@ -194,7 +194,7 @@ mod tests {
|
||||
fn gallery_lists_renders_and_opens_templates() {
|
||||
let mut app = app_with_store();
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install(&ctx, &app.tokens);
|
||||
crate::theme::install(&ctx, &app.tokens, crate::i18n::language(&app));
|
||||
// File ▸ New ▸ New Project from Template… opens the Templates tab.
|
||||
let ev = app.session.execute("file.newFromTemplate", json!({})).unwrap();
|
||||
assert_eq!(ev["frontend"], "file.newFromTemplate");
|
||||
@@ -250,7 +250,7 @@ mod tests {
|
||||
fn my_custom_rgb_dialog() {
|
||||
let mut app = app_with_store();
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install(&ctx, &app.tokens);
|
||||
crate::theme::install(&ctx, &app.tokens, crate::i18n::language(&app));
|
||||
// Menu check marks follow the active comp's viewer.
|
||||
app.session.execute("comp.new", json!({"name": "C", "width": 64, "height": 48})).unwrap();
|
||||
let item = crate::menus::menu_items(&app).into_iter().find(|m| m.id == "view.customRgb").expect("menu item");
|
||||
|
||||
@@ -170,7 +170,7 @@ mod tests {
|
||||
fn app() -> (EffectcraftApp, egui::Context) {
|
||||
let ctx = egui::Context::default();
|
||||
let app = EffectcraftApp::new(effectcraft_engine::Session::default());
|
||||
crate::theme::install(&ctx, &app.tokens);
|
||||
crate::theme::install(&ctx, &app.tokens, crate::i18n::language(&app));
|
||||
(app, ctx)
|
||||
}
|
||||
|
||||
|
||||
+128
-13
@@ -273,8 +273,14 @@ static INTER_MEDIUM: &[u8] = include_bytes!("../../../assets/fonts/Inter-Medium.
|
||||
static INTER_SEMIBOLD: &[u8] = include_bytes!("../../../assets/fonts/Inter-SemiBold.ttf");
|
||||
static JETBRAINS_MONO: &[u8] = include_bytes!("../../../assets/fonts/JetBrainsMono-Regular.ttf");
|
||||
|
||||
/// Install fonts (Inter, Inter Medium/SemiBold, JetBrains Mono) and egui visuals.
|
||||
pub fn install(ctx: &egui::Context, t: &Tokens) {
|
||||
/// Install fonts (Inter, Inter Medium/SemiBold, JetBrains Mono) and egui visuals. `language` is the
|
||||
/// resolved interface language from [`crate::i18n::language`] (`en`, `ja`, `zh-hans` or `zh-hant`),
|
||||
/// not the stored `general.language` preference: `system` is already resolved there, so a Chinese
|
||||
/// system draws Chinese with the default setting. The CJK fallback that draws the interface's own
|
||||
/// script is registered first, so a Chinese interface is drawn by a Chinese face and a Japanese one
|
||||
/// by a Japanese face. `install` is called again when the language changes (see the `EffectcraftApp`
|
||||
/// frame loop), so the fonts do not need a restart.
|
||||
pub fn install(ctx: &egui::Context, t: &Tokens, language: &str) {
|
||||
let mut fonts = FontDefinitions::default();
|
||||
fonts.font_data.insert("inter".into(), Arc::new(FontData::from_static(INTER_REGULAR)));
|
||||
fonts.font_data.insert("inter-medium".into(), Arc::new(FontData::from_static(INTER_MEDIUM)));
|
||||
@@ -284,26 +290,78 @@ pub fn install(ctx: &egui::Context, t: &Tokens) {
|
||||
fonts.families.entry(FontFamily::Monospace).or_default().insert(0, "jbmono".into());
|
||||
fonts.families.insert(FontFamily::Name("semibold".into()), vec!["inter-semibold".into(), "inter".into()]);
|
||||
fonts.families.insert(FontFamily::Name("medium".into()), vec!["inter-medium".into(), "inter".into()]);
|
||||
// Reuse the text engine's script-aware system fallback (#84), without embedding a CJK font.
|
||||
install_cjk_fallbacks(&mut fonts, language);
|
||||
ctx.set_fonts(fonts);
|
||||
apply_visuals(ctx, t);
|
||||
}
|
||||
|
||||
/// Register the CJK system fallbacks for `language` in every font family, and return their names in
|
||||
/// registration order (empty when nothing installed covers a probe, so a machine without CJK fonts
|
||||
/// simply keeps the bundled ones).
|
||||
///
|
||||
/// The text engine has its own script-aware fallback (#84), but which face it picks for Han
|
||||
/// ideographs depends on the locale environment: a Chinese interface on a Japanese-locale machine
|
||||
/// would be drawn with the Japanese face. The interface language is the better signal, so the probe
|
||||
/// for its script comes first — kana for Japanese, a Han character for the Chinese catalogs. Both
|
||||
/// scripts are registered, so file, layer and template names in the other one still render. The web
|
||||
/// build has no system fonts and installs nothing.
|
||||
fn install_cjk_fallbacks(family_fonts: &mut FontDefinitions, language: &str) -> Vec<String> {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let _ = (family_fonts, language);
|
||||
Vec::new()
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
use effectcraft_text::fonts;
|
||||
// Kana is unambiguously Japanese; Han ideographs are shared, so the order decides which
|
||||
// language's face draws them.
|
||||
const KANA: (char, &str) = ('あ', "japanese-system");
|
||||
const HAN: (char, &str) = ('文', "chinese-system");
|
||||
// Han text follows the interface language, not the operating system's locale.
|
||||
fonts::set_cjk_locale(cjk_locale(language));
|
||||
let base = fonts::resolve("Inter", "Regular").face;
|
||||
let face = fonts::face(fonts::fallback_for('あ', base));
|
||||
if face.has_char('あ')
|
||||
&& let Some(font) = face.font()
|
||||
{
|
||||
let probes = if is_chinese(language) { [HAN, KANA] } else { [KANA, HAN] };
|
||||
let mut names = Vec::new();
|
||||
let mut seen = Vec::new();
|
||||
for (probe, name) in probes {
|
||||
let id = fonts::fallback_for(probe, base);
|
||||
if seen.contains(&id) {
|
||||
continue;
|
||||
}
|
||||
let face = fonts::face(id);
|
||||
if !face.has_char(probe) {
|
||||
continue; // nothing installed covers this script
|
||||
}
|
||||
let Some(font) = face.font() else { continue };
|
||||
seen.push(id);
|
||||
// Use the already-read, parsed bytes rather than reading a font file twice.
|
||||
let mut data = FontData::from_owned(font.data().as_bytes().to_vec());
|
||||
data.index = face.info.index;
|
||||
fonts.font_data.insert("japanese-system".into(), Arc::new(data));
|
||||
for family in fonts.families.values_mut() {
|
||||
family.push("japanese-system".into());
|
||||
family_fonts.font_data.insert(name.to_string(), Arc::new(data));
|
||||
for family in family_fonts.families.values_mut() {
|
||||
family.push(name.to_string());
|
||||
}
|
||||
names.push(name.to_string());
|
||||
}
|
||||
names
|
||||
}
|
||||
ctx.set_fonts(fonts);
|
||||
apply_visuals(ctx, t);
|
||||
}
|
||||
|
||||
/// The CJK locale the text engine should prefer for Han text: the interface language when it names
|
||||
/// one, otherwise the empty string, which leaves the operating system's locale in charge.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn cjk_locale(language: &str) -> &str {
|
||||
match language {
|
||||
"ja" | "zh-hans" | "zh-hant" => language,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `language` is one of the Chinese catalogs.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn is_chinese(language: &str) -> bool {
|
||||
matches!(language, "zh-hans" | "zh-hant")
|
||||
}
|
||||
|
||||
pub fn apply_visuals(ctx: &egui::Context, t: &Tokens) {
|
||||
@@ -370,7 +428,7 @@ mod japanese_font_tests {
|
||||
return;
|
||||
}
|
||||
let ctx = egui::Context::default();
|
||||
super::install(&ctx, &super::Tokens::for_kind(super::ThemeKind::Dark));
|
||||
super::install(&ctx, &super::Tokens::for_kind(super::ThemeKind::Dark), "ja");
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |_| {});
|
||||
// No renderer here: drop the frame's texture uploads (egui asserts on unhandled ones in debug).
|
||||
out.textures_delta.clear();
|
||||
@@ -397,3 +455,60 @@ mod japanese_font_tests {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The last `n` font names of `family`, in order.
|
||||
fn tail(family: &[String], n: usize) -> Vec<&str> {
|
||||
family.iter().rev().take(n).rev().map(String::as_str).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cjk_fallbacks_follow_the_interface_language() {
|
||||
// A Chinese interface asks for the Han probe first, so Han text is drawn by a Chinese face
|
||||
// even when the operating system's locale is not Chinese.
|
||||
assert_eq!(cjk_locale("zh-hans"), "zh-hans");
|
||||
assert_eq!(cjk_locale("zh-hant"), "zh-hant");
|
||||
assert_eq!(cjk_locale("ja"), "ja");
|
||||
assert_eq!(cjk_locale("en"), "");
|
||||
assert_eq!(cjk_locale("system"), "");
|
||||
let mut chinese = FontDefinitions::default();
|
||||
let installed = install_cjk_fallbacks(&mut chinese, "zh-hans");
|
||||
if installed.is_empty() {
|
||||
eprintln!("no CJK font is installed: only the language plumbing can be checked here");
|
||||
} else {
|
||||
assert_eq!(installed.first().map(String::as_str), Some("chinese-system"), "the Han probe comes first");
|
||||
assert!(chinese.font_data.contains_key("chinese-system"));
|
||||
let expected: Vec<&str> = installed.iter().map(String::as_str).collect();
|
||||
for (family, stack) in &chinese.families {
|
||||
assert_eq!(tail(stack, installed.len()), expected, "{family:?}");
|
||||
}
|
||||
}
|
||||
// A Japanese interface keeps the kana probe first, as it was before the language existed.
|
||||
let mut japanese = FontDefinitions::default();
|
||||
let installed = install_cjk_fallbacks(&mut japanese, "ja");
|
||||
if let Some(first) = installed.first() {
|
||||
assert_eq!(first, "japanese-system", "the kana probe comes first");
|
||||
}
|
||||
// And an English interface leaves the operating system's locale in charge.
|
||||
let mut english = FontDefinitions::default();
|
||||
install_cjk_fallbacks(&mut english, "en");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_cjk_face_is_registered_at_most_once() {
|
||||
let mut fonts = FontDefinitions::default();
|
||||
let installed = install_cjk_fallbacks(&mut fonts, "zh-hant");
|
||||
let mut unique = installed.clone();
|
||||
unique.sort();
|
||||
unique.dedup();
|
||||
assert_eq!(unique, installed, "no face may be registered twice");
|
||||
for name in &installed {
|
||||
assert!(fonts.font_data.contains_key(name), "{name} is missing its font data");
|
||||
}
|
||||
// Leave the operating system's locale behind for whatever runs next.
|
||||
install_cjk_fallbacks(&mut fonts, "system");
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -29,7 +29,7 @@ Commands (CLI, MCP, control channel):
|
||||
|
||||
### Settings that change behaviour
|
||||
|
||||
- `general.language`: interface language (`system` / `en` / `ja` / `zh-hans` / `zh-hant`), 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: a Simplified Chinese locale (`zh`, `zh-CN`, `zh_CN.UTF-8`, `zh-Hans-CN`, `zh-SG`) selects `zh-hans`, a Traditional one (`zh-TW`, `zh-HK`, `zh-Hant`, `zh_MO`) selects `zh-hant`; the browser build stays in English with it. Native Japanese, Simplified and Traditional Chinese UI uses installed system fonts; the web host must supply a Japanese font.
|
||||
- `general.language`: interface language (`system` / `en` / `ja` / `zh-hans` / `zh-hant`), 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: a Simplified Chinese locale (`zh`, `zh-CN`, `zh_CN.UTF-8`, `zh-Hans-CN`, `zh-SG`) selects `zh-hans`, a Traditional one (`zh-TW`, `zh-HK`, `zh-Hant`, `zh_MO`) selects `zh-hant`; the browser build stays in English with it. Native Japanese, Simplified and Traditional Chinese UI uses installed system fonts; the web host must supply a Japanese font. The same setting picks the CJK fallback for the interface *and* the Han preference of the text engine, so the composition's own text is drawn with the same language's glyph forms (the text engine's preference is process-global); swapping the language reinstalls the fonts, so it applies without a restart.
|
||||
- `general.undoLevels`: Levels of Undo
|
||||
- `general.pathPointSize`: Path Point and Handle Size
|
||||
- `general.recentItems`: Recent Projects Shown
|
||||
|
||||
Reference in new issue
Block a user