erf(i18n): cache translations and skip unnecessary bidi analysis
tr() uses a OnceLock<HashMap>, initialized once, changing lookups from a linear scan to average O(1). Labels without RTL text, such as Chinese, Japanese, and Western-language labels, skip BidiInfo. Arabic, Hebrew, and directional controls retain their existing handling.
This commit is contained in:
parent
0f09d48a7c
commit
ffcc2ef320
|
|
@ -3,6 +3,8 @@
|
|||
//! Chinese and Arabic feature terminology is checked against public Adobe help where documented;
|
||||
//! project-specific labels are original. Sources and scope: `docs/localization-terminology.json`.
|
||||
|
||||
use std::{collections::HashMap, sync::OnceLock};
|
||||
|
||||
mod ar;
|
||||
|
||||
/// Supported interface languages: (code, name in that language).
|
||||
|
|
@ -2635,14 +2637,13 @@ fn column(lang: &str) -> Option<usize> {
|
|||
|
||||
/// `s` in `lang` (English, or the string itself, when there's no translation).
|
||||
pub fn tr<'a>(lang: &str, s: &'a str) -> &'a str {
|
||||
static TRANSLATIONS: OnceLock<HashMap<&'static str, [&'static str; 5]>> = OnceLock::new();
|
||||
static ARABIC: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
|
||||
if lang == "ar" {
|
||||
return ar::TABLE.iter().find(|(en, _)| *en == s).map_or(s, |(_, t)| *t);
|
||||
return ARABIC.get_or_init(|| ar::TABLE.iter().copied().collect()).get(s).copied().unwrap_or(s);
|
||||
}
|
||||
let Some(c) = column(lang) else { return s };
|
||||
match TABLE.iter().find(|(en, _)| *en == s) {
|
||||
Some((_, t)) => t[c],
|
||||
None => s,
|
||||
}
|
||||
TRANSLATIONS.get_or_init(|| TABLE.iter().copied().collect()).get(s).and_then(|row| row.get(c)).copied().unwrap_or(s)
|
||||
}
|
||||
|
||||
/// Localize reserved built-in style names only; user-defined names are document data.
|
||||
|
|
@ -2665,6 +2666,23 @@ pub fn workspace_name<'a>(lang: &str, name: &'a str) -> &'a str {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cached_lookup_preserves_every_translation_across_language_switches() {
|
||||
for (key, translations) in TABLE {
|
||||
for (lang, expected) in ["de", "fr", "es", "ja", "zh"].into_iter().zip(translations) {
|
||||
assert_eq!(tr(lang, key), *expected, "{lang}: {key}");
|
||||
}
|
||||
assert_eq!(tr("", key), *key);
|
||||
}
|
||||
for (key, expected) in ar::TABLE {
|
||||
assert_eq!(tr("ar", key), *expected, "ar: {key}");
|
||||
}
|
||||
let unknown = String::from("A user-defined untranslated label");
|
||||
for lang in ["ar", "zh", "ja", "de", "fr", "es", "", "unknown"] {
|
||||
assert!(std::ptr::eq(tr(lang, &unknown), unknown.as_str()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translates_known_strings_and_keeps_the_rest() {
|
||||
assert_eq!(tr("de", "File"), "Datei");
|
||||
|
|
|
|||
|
|
@ -3,13 +3,40 @@
|
|||
use egui::text::{LayoutJob, TextWrapping};
|
||||
use egui::{Align, Align2, Color32, FontFamily, FontId, Galley, Pos2, Rect, WidgetText};
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use unicode_bidi::BidiInfo;
|
||||
use unicode_bidi::{BidiClass, BidiInfo, bidi_class};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
/// Allocation-free preflight: ordinary Latin, CJK and emoji labels need no bidi analysis.
|
||||
/// Include directional controls so explicit embeddings/overrides keep their existing behavior.
|
||||
fn may_have_rtl(text: &str) -> bool {
|
||||
!text.is_ascii()
|
||||
&& text.chars().any(|c| {
|
||||
matches!(
|
||||
bidi_class(c),
|
||||
BidiClass::R
|
||||
| BidiClass::AL
|
||||
| BidiClass::AN
|
||||
| BidiClass::LRE
|
||||
| BidiClass::RLE
|
||||
| BidiClass::LRO
|
||||
| BidiClass::RLO
|
||||
| BidiClass::PDF
|
||||
| BidiClass::LRI
|
||||
| BidiClass::RLI
|
||||
| BidiClass::FSI
|
||||
| BidiClass::PDI
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn has_rtl(text: &str) -> bool {
|
||||
may_have_rtl(text) && BidiInfo::new(text, None).has_rtl()
|
||||
}
|
||||
|
||||
/// Resolve translated labels through bidi, without changing IDs, commands or stored text.
|
||||
pub fn widget(ui: &egui::Ui, text: impl Into<WidgetText>) -> WidgetText {
|
||||
let text = text.into();
|
||||
if text.text().is_ascii() || !BidiInfo::new(text.text(), None).has_rtl() {
|
||||
if !has_rtl(text.text()) {
|
||||
return text;
|
||||
}
|
||||
let mut job = (*text.into_layout_job(ui.style(), egui::FontSelection::Default, Align::Center)).clone();
|
||||
|
|
@ -21,7 +48,7 @@ pub fn widget(ui: &egui::Ui, text: impl Into<WidgetText>) -> WidgetText {
|
|||
/// Accessibility still receives the original logical string through Galley::job.
|
||||
pub fn label(ui: &mut egui::Ui, text: impl Into<WidgetText>) -> egui::Response {
|
||||
let text = text.into();
|
||||
let rtl = !text.text().is_ascii() && BidiInfo::new(text.text(), None).has_rtl();
|
||||
let rtl = has_rtl(text.text());
|
||||
if !rtl {
|
||||
return ui.label(text);
|
||||
}
|
||||
|
|
@ -112,7 +139,7 @@ fn visual_line(job: &LayoutJob, bidi: &BidiInfo<'_>, line: Range<usize>) -> Layo
|
|||
}
|
||||
|
||||
fn layout(ctx: &egui::Context, mut job: LayoutJob) -> Arc<Galley> {
|
||||
if job.text.is_ascii() || !BidiInfo::new(&job.text, None).has_rtl() {
|
||||
if !has_rtl(&job.text) {
|
||||
return ctx.fonts_mut(|fonts| fonts.layout_job(job));
|
||||
}
|
||||
let original = Arc::new(job.clone());
|
||||
|
|
@ -219,6 +246,32 @@ mod tests {
|
|||
galley.rows.iter().flat_map(|row| row.glyphs.iter().filter(|g| g.advance_width > 0.0).map(|g| g.chr)).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_multilingual_labels_skip_bidi_analysis() {
|
||||
for text in ["", "File (123)", "Édition", "中文排版", "スウォッチ", "e\u{301}", "🎨 🙂"] {
|
||||
assert!(!may_have_rtl(text), "{text:?}");
|
||||
assert!(!BidiInfo::new(text, None).has_rtl(), "{text:?}");
|
||||
}
|
||||
for text in ["ملف", "עברית", "PDF ملف 123", "\u{1e900}", "\u{200f}", "\u{202e}abc\u{202c}", "\u{2067}abc\u{2069}", "١٢٣"] {
|
||||
assert!(may_have_rtl(text), "{text:?}");
|
||||
assert_eq!(has_rtl(text), BidiInfo::new(text, None).has_rtl(), "{text:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_rtl_widgets_keep_native_text_and_formatting() {
|
||||
with_fonts(|ctx| {
|
||||
egui::Area::new(egui::Id::new("rtl-fast-path-test")).show(ctx, |ui| {
|
||||
for text in ["中文", "日本語", "Édition", "🎨"] {
|
||||
let rendered = widget(ui, egui::RichText::new(text).strong());
|
||||
assert!(matches!(rendered, WidgetText::RichText(_)));
|
||||
assert_eq!(rendered.text(), text);
|
||||
assert!(label(ui, text).rect.is_finite());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_arabic_latin_digits_and_brackets_preserve_logical_source() {
|
||||
with_fonts(|ctx| {
|
||||
|
|
|
|||
Loading…
Reference in New Issue