collective

Browse / github.com/storytold/craft-launcher / patch

feat(i18n): add Hebrew (he) interface catalog and register Lang::HE

feat(i18n): add Hebrew (he) interface catalog and register Lang::HE ## Summary Adds the complete Hebrew (`he` / `עברית`) interface translation catalog for ArtCraft Launcher (builds on #890): - **`crates/ui-egui/src/i18n/he.tsv`**: - Clean-room Hebrew translation covering 100% of UI literals (`tl!`, `t`), `@id` commands, `@plural` rows (`one|two|other`), `@msg` status/error templates, package format labels, and all 12 Craft app taglines. - **`crates/ui-egui/src/i18n/lang.rs`**: - Registers `Lang::HE` (`code: "he"`, `name: "עברית"`, `english_name: "Hebrew"`, `script: Some(Script::Hebrew)`, `rtl: true`, `plural: plural_hebrew`, `plural_forms: 3`, `complete_m

Status
open
Score
0.00
Adopters
0 other operators
Author
hrb9 (imported from pull request #891, open)
Base
7aafad661a11594a70ae949471d481f30d8fe64a
Size
+541 −13 in 7 files
Review
pending
Shared
2026-10-28
Id
f301d8d46ff37980c3588f96

How it went

No other operator has reported using it yet.

Diff

raw · crates/ui-egui/src/card_parts.rs crates/ui-egui/src/cjk_font.rs crates/ui-egui/src/i18n/he.tsv crates/ui-egui/src/i18n/lang.rs crates/ui-egui/src/i18n/tests.rs crates/ui-egui/src/settings_dialog.rs crates/ui-egui/src/widgets.rs

diff --git a/crates/ui-egui/src/card_parts.rs b/crates/ui-egui/src/card_parts.rs--- a/crates/ui-egui/src/card_parts.rs+++ b/crates/ui-egui/src/card_parts.rs@@ -31,6 +31,9 @@ pub(crate) fn last_updated(e: &AppEntry) -> Option<String> { /// `text` in at most `rows` lines, ending in "…" when it is cut. pub(crate) fn blurb(ui: &mut Ui, text: &str, color: Color32, rows: usize) {     let mut job = egui::text::LayoutJob::simple(text.to_string(), FontId::proportional(13.0), color, ui.available_width());+    if crate::i18n::current().rtl() {+        job.direction = egui::epaint::text::TextDirection::RightToLeft;+    }     job.wrap.max_rows = rows;     job.wrap.break_anywhere = false;     job.wrap.overflow_character = Some('…');diff --git a/crates/ui-egui/src/cjk_font.rs b/crates/ui-egui/src/cjk_font.rs--- a/crates/ui-egui/src/cjk_font.rs+++ b/crates/ui-egui/src/cjk_font.rs@@ -40,6 +40,7 @@ fn script_name(script: Script) -> &'static str {     match script {         Script::Chinese => "Chinese",         Script::Japanese => "Japanese",+        Script::Hebrew => "Hebrew",     } } @@ -48,6 +49,9 @@ fn script_families(script: Script) -> &'static str {     match script {         Script::Chinese => "PingFang SC, Hiragino Sans GB, Heiti SC, Microsoft YaHei, SimSun, Noto Sans CJK SC, WenQuanYi and MiSans",         Script::Japanese => "Hiragino Sans, Yu Gothic, Meiryo, MS Gothic, Noto Sans CJK JP, Noto Sans JP, IPAGothic and TakaoGothic",+        Script::Hebrew => {+            "SF Hebrew, Arial Hebrew, Arial, Segoe UI, Noto Sans Hebrew, Heebo, Rubik, Assistant, David Libre, FreeSans and DejaVu Sans"+        }     } } @@ -84,6 +88,7 @@ pub(crate) fn load(script: Script) -> Option<FontData> {     None } +/// Reads a font file up to [`MAX_FONT_BYTES`], returning `None` if missing, not a regular file, or oversized. fn read(path: &Path) -> Option<Vec<u8>> {     let meta = std::fs::metadata(path).ok()?;     if !meta.is_file() {@@ -123,6 +128,7 @@ fn candidates(script: Script) -> Vec<Candidate> {     match script {         Script::Chinese => chinese_candidates(),         Script::Japanese => japanese_candidates(),+        Script::Hebrew => hebrew_candidates(),     } } @@ -305,6 +311,76 @@ fn japanese_candidates() -> Vec<Candidate> {     out } +/// The free OFL Hebrew faces a user may install by hand, by family preference.+const HEBREW_USER_FILES: [(&str, &str); 6] = [+    ("NotoSansHebrew-Regular.ttf", "Noto Sans Hebrew"),+    ("Rubik-Regular.ttf", "Rubik"),+    ("Heebo-Regular.ttf", "Heebo"),+    ("Assistant-Regular.ttf", "Assistant"),+    ("DavidLibre-Regular.ttf", "David Libre"),+    ("Alef-Regular.ttf", "Alef"),+];++/// Where each system keeps a Hebrew font, best first.+fn hebrew_candidates() -> Vec<Candidate> {+    let mut out = Vec::new();+    let mut add = |path: PathBuf, family: &'static str| out.push(Candidate { path, family });+    #[cfg(target_os = "macos")]+    {+        add("/System/Library/Fonts/SFHebrew.ttf".into(), "SF Hebrew");+        for path in mac_font_assets("SFHebrew.ttf") {+            add(path, "SF Hebrew");+        }+        add("/System/Library/Fonts/ArialHB.ttc".into(), "Arial Hebrew");+        add("/System/Library/Fonts/Supplemental/Arial Unicode.ttf".into(), "Arial Unicode MS");+        add("/System/Library/Fonts/Supplemental/Arial.ttf".into(), "Arial");+    }+    #[cfg(target_os = "windows")]+    {+        let root = std::env::var_os("SystemRoot").map_or_else(|| PathBuf::from(r"C:\Windows"), PathBuf::from);+        add(root.join("Fonts").join("segoeui.ttf"), "Segoe UI");+        add(root.join("Fonts").join("arial.ttf"), "Arial");+        add(root.join("Fonts").join("david.ttf"), "David");+    }+    #[cfg(all(unix, not(target_os = "macos")))]+    {+        const NOTO_DIRS: [&str; 8] = [+            "/usr/share/fonts/truetype/noto",+            "/usr/share/fonts/opentype/noto",+            "/usr/share/fonts/noto",+            "/usr/share/fonts/google-noto-vf",+            "/usr/share/fonts/google-noto-sans-hebrew-vf-fonts",+            "/usr/share/fonts/google-noto-sans-hebrew-fonts",+            "/usr/local/share/fonts/noto",+            "/usr/local/share/fonts",+        ];+        for file in ["NotoSansHebrew-Regular.ttf", "NotoSansHebrew-Regular.otf", "NotoSansHebrew[wdth,wght].ttf"] {+            for dir in NOTO_DIRS {+                add(Path::new(dir).join(file), "Noto Sans Hebrew");+            }+        }+        const FALLBACK_DIRS: [&str; 6] = [+            "/usr/share/fonts/truetype/freefont",+            "/usr/share/fonts/gnu-free",+            "/usr/share/fonts/truetype/dejavu",+            "/usr/share/fonts/TTF",+            "/usr/share/fonts/dejavu-sans-fonts",+            "/usr/local/share/fonts",+        ];+        for (file, family) in [("FreeSans.ttf", "FreeSans"), ("DejaVuSans.ttf", "DejaVu Sans")] {+            for dir in FALLBACK_DIRS {+                add(Path::new(dir).join(file), family);+            }+        }+    }+    for (file, family) in HEBREW_USER_FILES {+        for dir in user_font_dirs() {+            add(dir.join(file), family);+        }+    }+    out+}+ /// `file` in macOS's downloaded font assets: /// `/System/Library/AssetsV2/com_apple_MobileAsset_Font*/*.asset/AssetData/<file>`. #[cfg(target_os = "macos")]@@ -394,6 +470,18 @@ mod tests {         assert!(first_misans > 0, "the system's own fonts come first");     } +    /// System Hebrew fonts come before user-installed OFL Hebrew faces, and every path is absolute.+    #[test]+    fn hebrew_candidates_include_system_and_user_fonts() {+        let cands = hebrew_candidates();+        let names: Vec<String> = cands.iter().filter_map(|c| c.path.file_name().map(|n| n.to_string_lossy().into_owned())).collect();+        assert!(names.iter().any(|n| n == "NotoSansHebrew-Regular.ttf"), "{names:?}");+        assert!(names.iter().any(|n| n == "Rubik-Regular.ttf"), "{names:?}");+        assert!(cands.iter().all(|c| c.path.is_absolute() || c.path.starts_with("/")));+        let first_rubik = cands.iter().position(|c| c.family == "Rubik").unwrap();+        assert!(first_rubik > 0, "the system's own Hebrew fonts come first");+    }+     #[test]     fn face_index_finds_families_in_real_fonts() {         // The bundled Latin faces stand in for system fonts the test machine may not have.diff --git a/crates/ui-egui/src/i18n/he.tsv b/crates/ui-egui/src/i18n/he.tsvnew file mode 100644--- /dev/null+++ b/crates/ui-egui/src/i18n/he.tsv@@ -0,0 +1,351 @@+# Hebrew (he) catalog for ArtCraft Launcher.+# Format: context<TAB>source<TAB>translation+# Empty context = plain UI string (tl!, t). @id = command-id keyed. @plural = one|two|other.+# @msg = status/error message template. Other context = disambiguation for tr_ctx.+# Clean-room Hebrew translation, translated from the meaning of the English source strings.+# Product and app names, shortcuts, units, file formats, placeholders and trailing ellipses are kept.++	Launcher	Launcher+	Craft App Suite	Craft App Suite+	Open Source Tools for Creatives, Makers, and Builders	כלי קוד פתוח ליוצרים, מעצבים ובונים+	Settings	הגדרות+	Check for updates	בדוק עדכונים+	Grid	רשת+	Table	טבלה+	Show apps as cards	הצג יישומים ככרטיסים+	Show apps as a table	הצג יישומים כטבלה+	All	הכול+	Updates	עדכונים+	Not installed	לא מותקנים+	Search apps	חיפוש יישומים+	Sort	מיון+	Suite order	סדר החבילה+	Name	שם+	Inspired by	בהשראת+	Status	מצב+	Installed version	גרסה מותקנת+	Latest version	גרסה אחרונה+	Newest release	הגרסה החדשה ביותר+	Discord	Discord+	GitHub	GitHub+	Website	אתר אינטרנט+	Classic apps	יישומים קלאסיים+	Classic	קלאסי+	New apps	יישומים חדשים+	NEW	חדש+	Install	התקן+	Update to {v}	עדכן לגרסה {v}+	Open	פתח+	Download for {os}	הורד עבור {os}+	Download	הורדה+	Coming soon	בקרוב+	Checking…	בודק…+	Not available	לא זמין+	Offline	לא מקוון+	Downloading {done} of {total}	מוריד {done} מתוך {total}+	Downloading…	מוריד…+	Reconnecting…	מתחבר מחדש…+	Verifying…	מאמת…+	Installing, then restarting…	מתקין ומפעיל מחדש…+	Installing…	מתקין…+	Removing…	מסיר…+	Pause download	השהה הורדה+	Cancel download	בטל הורדה+	Resume download	חדש הורדה+	Discard download	מחק הורדה+	Release notes	הערות שחרור+	Show in Finder	הצג ב-Finder+	Show in Explorer	הצג בסייר הקבצים+	Show in Files	הצג בקבצים+	Reinstall	התקן מחדש+	Uninstall…	הסר התקנה…+	All releases	כל הגרסאות+	ArtCraft Launcher {version} is available.	ArtCraft Launcher {version} זמין.+	Dismiss	סגור+	Update and restart	עדכן והפעל מחדש+	A new version is available. Would you like to relaunch with the updated version?	גרסה חדשה זמינה. האם ברצונך להפעיל מחדש עם הגרסה המעודכנת?+	A new version is available. Download it to replace this version?	גרסה חדשה זמינה. האם להוריד אותה כדי להחליף גרסה זו?+	Later	אחר כך+	What's new	מה חדש+	No notes were published with this release.	לא פורסמו הערות שחרור עם גרסה זו.+	Released {date}	פורסם ב-{date}+	Open on GitHub	פתח ב-GitHub+	Close	סגור+	ArtCraft Launcher {version}	ArtCraft Launcher {version}+	Only when I ask	רק כשאני מבקש+	Automatically download and install updates	הורד והתקן עדכונים באופן אוטומטי+	Notify me when an installed app has an update	עדכן אותי כשיש עדכון ליישום מותקן+	Include pre-releases (betas and release candidates)	כלול גרסאות קדם-שחרור (בטא ומועמדות לשחרור)+	Installing on Linux	מתקין בלינוקס+	Installing on Windows	מתקין ב-Windows+	Install new apps as	התקן יישומים חדשים בתור+	System package (recommended)	חבילת מערכת (מומלץ)+	Updates always use the format an app was installed with.	עדכונים משתמשים תמיד בתבנית שבה היישום הותקן.+	Running in the background	פועל ברקע+	Keep running in the tray when the window is closed	המשך לפעול בשורת המשימות כשהחלון נסגר+	ArtCraft Launcher is still running	ArtCraft Launcher עדיין פועל+	It's in the tray: open it or quit it from the tray icon, or turn this off in the settings.	הוא נמצא בשורת המשימות: ניתן לפתוח אותו או לצאת ממנו דרך סמל שורת המשימות, או לכבות אפשרות זו בהגדרות.+	Show the window on the current desktop	הצג את החלון בשולחן העבודה הנוכחי+	Start ArtCraft Launcher when I log in	הפעל את ArtCraft Launcher בעת הכניסה למערכת+	Community	קהילה+	Questions, ideas and news from the ArtCraft team	שאלות, רעיונות וחדשות מצוות ArtCraft+	Appearance	מראה+	Quit ArtCraft Launcher	צא מ-ArtCraft Launcher+	Done	סיום+	Uninstall {name}?	להסיר את ההתקנה של {name}?+	The app will be moved to the Trash. Your documents and settings stay.	היישום יועבר לאשפה. המסמכים וההגדרות שלך יישארו.+	Windows will remove it like any installed program. Your documents and settings stay.	Windows תסיר אותו כמו כל תוכנית מותקנת אחרת. המסמכים וההגדרות שלך יישארו.+	Your system's package manager will remove it, and may ask for your password. Your documents and settings stay.	מנהל החבילות של המערכת יסיר אותו, ועשוי לבקש את הסיסמה שלך. המסמכים וההגדרות שלך יישארו.+	The app will be removed. Your documents and settings stay.	היישום יוסר. המסמכים וההגדרות שלך יישארו.+	Uninstall	הסר התקנה+	Cancel	ביטול+	Checked {ago}	נבדק {ago}+	just now	ממש עכשיו+	yesterday	אתמול+	{month} {day}, {year}	{day} ב{month} {year}+	Automatically	באופן אוטומטי+	Every Craft app	כל יישומי Craft+	Apps on this computer	יישומים במחשב זה+	Installed apps with a newer version	יישומים מותקנים עם גרסה חדשה יותר+	Apps you can install	יישומים שניתן להתקין+	Hide App	הסתר יישום+	Unhide App	בטל הסתרת יישום+	Hidden Apps	יישומים מוסתרים+	Apps you hid	יישומים שהסתרת+	Checked {ago} · using {sources}	נבדק {ago} · באמצעות {sources}+	{a} and {b}	{a} ו-{b}+	Join the Discord	הצטרפו ל-Discord+	Learn more	מידע נוסף+	Minimize	מזער+	Maximize	הגדל למקסימום+	Restore	שחזר+	New	חדש+	OK	אישור+	Open link	פתח קישור+	Update	עדכן+	{name} icon	סמל {name}+	Loading ArtCraft Launcher…	טוען את ArtCraft Launcher…+	Cancel update	בטל עדכון+	Stop waiting; the update can be started again later	הפסק להמתין; ניתן להתחיל את העדכון שוב מאוחר יותר+	Download again	הורד שוב+	Install cancelled	ההתקנה בוטלה+	Install cancelled. The download is kept for the next try.	ההתקנה בוטלה. ההורדה נשמרת לניסיון הבא.+	Update cancelled	העדכון בוטל+	Uninstall cancelled	הסרת ההתקנה בוטלה+	Retry install	נסה להתקין שוב+	Retry update to {version}	נסה שוב לעדכן לגרסה {version}+	The download is still here. It's checked again, then installed.	ההורדה עדיין כאן. היא תיבדק שוב ולאחר מכן תותקן.+	Waiting for another app…	ממתין ליישום אחר…+	Waiting for other installs…	ממתין להתקנות אחרות…+	Installer (.msi)	תוכנית התקנה (.msi)+	Portable (.zip)	נייד (.zip)+	Disk image (.dmg)	דמות כונן (.dmg)+	Debian package (.deb)	חבילת Debian (.deb)+	RPM package (.rpm)	חבילת RPM (.rpm)+	Arch package (.pkg.tar.zst)	חבילת Arch (.pkg.tar.zst)+	Flatpak bundle	חבילת Flatpak+	Tarball (.tar.gz)	ארכיון (.tar.gz)+photocraft	Image editing	עריכת תמונות+vectorcraft	Vector illustration	איור וקטורי+filmcraft	Video editing, color and sound	עריכת וידאו, צבע ושמע+lightcraft	Photo library and raw development	ספריית תמונות ופיתוח RAW+pdfcraft	Reading, organizing and protecting PDFs	קריאה, ארגון והגנה על קובצי PDF+effectcraft	Motion graphics and visual effects	גרפיקה בתנועה ואפקטים חזותיים+designcraft	Page layout and publishing	עימוד דפים והוצאה לאור+wordcraft	Writing and document design	כתיבה ועיצוב מסמכים+deckcraft	Presentations and slide shows	מצגות ושקופיות+gridcraft	Spreadsheets and data	גיליונות אלקטרוניים ונתונים+cadcraft	Computer-aided design and drafting	תיב״ם (תכנון ושרטוט בעזרת מחשב)+soundcraft	Recording, editing and mixing audio	הקלטה, עריכה ומיקס של שמע+figcraft	Interface design and prototyping	עיצוב ממשקים ואבות-טיפוס+month	Jan	ינו׳+month	Feb	פבר׳+month	Mar	מרץ+month	Apr	אפר׳+month	May	מאי+month	Jun	יונ׳+month	Jul	יול׳+month	Aug	אוג׳+month	Sep	ספט׳+month	Oct	אוק׳+month	Nov	נוב׳+month	Dec	דצמ׳+	Not checked yet	טרם נבדק+	GitHub's hourly limit was reached; full details return in {min} min	המגבלה השעתית של GitHub הושגה; הפרטים המלאים יחזרו בעוד {min} דק׳+	GitHub limit hit; back in {min} min	מגבלת GitHub הושגה; חזרה בעוד {min} דק׳+	Installing for {platform}	מתקין עבור {platform}+	Downloads for {platform}	הורדות עבור {platform}+	No downloads for this system	אין הורדות עבור מערכת זו+	Join the ArtCraft community on Discord	הצטרפו לקהילת ArtCraft ב-Discord+	ArtCraft on {name}	ArtCraft ב-{name}+	Couldn't open {name}: {e}	לא ניתן לפתוח את {name}: {e}+	Download cancelled	ההורדה בוטלה+	Available to install	זמין להתקנה+	Available	זמין+	Installed · up to date	מותקן · מעודכן+	Update available	עדכון זמין+	Installed	מותקן+	installed	מותקן+	Not available here	לא זמין כאן+	Updating…	מעדכן…+	Update paused	העדכון הושהה+	Download paused	ההורדה הושהתה+	{version} installed → {latest} released	{version} מותקנת ← {latest} פורסמה+	Version {version}	גרסה {version}+	{installed} installed · latest {latest}	{installed} מותקנת · אחרונה {latest}+	Installed · latest {latest}	מותקן · אחרונה {latest}+	Version {version} installed	גרסה {version} מותקנת+	Version {latest}	גרסה {latest}+	No release published yet	טרם פורסמה גרסה+	ArtCraft Launcher	ArtCraft Launcher+	App	יישום+	Latest	אחרונה+	Released	פורסם+	Auto	אוטומטי+	Language	שפה+	Ascending	סדר עולה+	Descending	סדר יורד+	Click to reverse the order	לחץ להיפוך הסדר+	Click to sort by this column	לחץ למיון לפי עמודה זו+	Like {name}	כמו {name}+	{done} of {total}	{done} מתוך {total}+	Downloading	מוריד+	Verifying checksum…	מאמת סכום ביקורת…+	Resume	המשך+	{done} of {total} downloaded	{done} מתוך {total} הורדו+	{done} downloaded	{done} הורדו+	Checking for updates…	בודק עדכונים…+	Downloads the new version, replaces this one and opens it again	מוריד את הגרסה החדשה, מחליף את הנוכחית ופותח אותה מחדש+	Couldn't check for updates: {f}	לא ניתן לבדוק עדכונים: {f}+	Website (coming soon)	אתר אינטרנט (בקרוב)+	GitHub repository	מאגר GitHub+	ArtCraft	ArtCraft+	Update 1 app	עדכן יישום 1+	Update all ({count})	עדכן הכול ({count})+	⊞  Grid	⊞  רשת+	☰  Table	☰  טבלה+	🔍  Search apps	🔍  חיפוש יישומים+	{name} {version}	{name} {version}+	Match system	התאם למערכת+	Dark	כהה+	Light	בהיר+	Full changelog	יומן שינויים מלא+	link	קישור+	No apps match	אין יישומים תואמים+	Open ArtCraft Launcher to update.	פתח את ArtCraft Launcher כדי לעדכן.+	{name} {v} is available	{name} {v} זמין+	{count} updates are available	{count} עדכונים זמינים+	These notes weren't fetched (GitHub's request limit was reached). They're on the release page.	הערות אלו לא נטענו (מגבלת הבקשות של GitHub הושגה). הן זמינות בדף הגרסה.+	When on, opening the launcher from the menu bar brings its window to the desktop you're on. When off, macOS switches you to the desktop the window was on.	כאשר אפשרות זו פעילה, פתיחת המשגר משורת התפריטים מביאה את החלון שלו לשולחן העבודה הנוכחי. כאשר היא כבויה, macOS מעבירה אותך לשולחן העבודה שבו החלון נמצא.+	Couldn't change the login item: {e}	לא ניתן לשנות את פריט ההפעלה בכניסה: {e}+column	App	יישום+column	Installed	מותקנת+column	Latest	אחרונה+column	Released	תאריך פרסום+filter	Installed	מותקנים+	App details	פרטי היישום+	More actions for {name}	פעולות נוספות עבור {name}+	Close (Esc)	סגור (Esc)+	Features	תכונות+	Latest release	גרסה אחרונה+	Version	גרסה+	Platforms	פלטפורמות+	Releases	גרסאות+	Enlarge picture	הגדל תמונה+	Click to enlarge	לחץ להגדלה+	Previous picture (←)	התמונה הקודמת (←)+	Next picture (→)	התמונה הבאה (→)+	Loading pictures…	טוען תמונות…+	Pictures couldn't be loaded. Reopen this page to try again.	לא ניתן היה לטעון את התמונות. פתח עמוד זה מחדש כדי לנסות שוב.+	{name} details	פרטי {name}+@plural	{n} app|{n} apps	יישום {n}|{n} יישומים|{n} יישומים+@plural	{n} min ago|{n} min ago	לפני דקה ({n})|לפני {n} דקות|לפני {n} דקות+@plural	{n} h ago|{n} h ago	לפני שעה ({n})|לפני {n} שעות|לפני {n} שעות+@plural	{n} day ago|{n} days ago	לפני יום {n}|לפני {n} ימים|לפני {n} ימים+@msg	Couldn't open {name}: {e}	לא ניתן לפתוח את {name}: {e}+@msg	Download cancelled	ההורדה בוטלה+@msg	{name} updated to {version}	{name} עודכן לגרסה {version}+@msg	{name} installed: {version}	{name} הותקן: {version}+@msg	{name} updated to {version} (this release publishes no checksum, so only its size was checked)	{name} עודכן לגרסה {version} (גרסה זו אינה מפרסמת סכום ביקורת, לכן רק הגודל שלה נבדק)+@msg	{name} installed: {version} (this release publishes no checksum, so only its size was checked)	{name} הותקן: {version} (גרסה זו אינה מפרסמת סכום ביקורת, לכן רק הגודל שלה נבדק)+@msg	{name} was removed	{name} הוסר+@msg	{name} is running. Quit it, then try again.	{name} פועל כעת. סגור אותו ונסה שוב.+@msg	{name} was installed from the {repo} repository or the AUR ({pkg}). Update it with {cmd} or sudo pacman -Syu	{name} הותקן ממאגר {repo} או מ-AUR ({pkg}). עדכן אותו באמצעות {cmd} או sudo pacman -Syu+@msg	{name} was installed from the {repo} repository or the AUR ({pkg}). Update it with your AUR helper (for example: yay -S {pkg}) or sudo pacman -Syu	{name} הותקן ממאגר {repo} או מ-AUR ({pkg}). עדכן אותו באמצעות כלי העזר של AUR (לדוגמה: yay -S {pkg}) או sudo pacman -Syu+@msg	{name} was installed from the AUR ({pkg}). Update it with {cmd}	{name} הותקן מ-AUR ({pkg}). עדכן אותו באמצעות {cmd}+@msg	{name} was installed from the AUR ({pkg}). Update it with your AUR helper (for example: yay -S {pkg})	{name} הותקן מ-AUR ({pkg}). עדכן אותו באמצעות כלי העזר של AUR (לדוגמה: yay -S {pkg})+@id	app.install	התקן+@id	app.update	עדכן לגרסה {v}+@id	app.open	פתח+@id	app.download	הורד עבור {os}+@id	app.reveal	הצג ב-Finder+@id	app.reinstall	התקן מחדש+@id	app.uninstall	הסר התקנה…+@id	app.notes	הערות שחרור+@id	app.website	אתר אינטרנט+@id	app.github	מאגר GitHub+@id	app.releases	כל הגרסאות+@id	launcher.update	עדכן והפעל מחדש+@id	launcher.download	הורד+@id	launcher.notes	מה חדש+@id	launcher.dismiss	סגור+@id	settings.open	הגדרות+@id	settings.quit	צא מ-ArtCraft Launcher+@id	view.grid	רשת+@id	view.table	טבלה+@id	filter.all	הכול+@id	filter.installed	מותקנים+@id	filter.updates	עדכונים+@id	filter.notinstalled	לא מותקנים+@id	sort.suite	סדר החבילה+@id	sort.name	שם+@id	sort.inspired	בהשראת+@id	sort.status	מצב+@id	sort.installed	גרסה מותקנת+@id	sort.latest	גרסה אחרונה+@id	sort.released	הגרסה החדשה ביותר+@id	help.discord	Discord+@id	help.github	GitHub+@id	help.website	אתר אינטרנט+@id	update.check	בדוק עדכונים+@id	update.check_auto	בדוק עדכונים באופן אוטומטי+@id	update.notify	עדכן אותי כשיש עדכון ליישום מותקן+@id	update.prereleases	כלול גרסאות קדם-שחרור (בטא ומועמדות לשחרור)+@id	linux.preferred_kind	התקן יישומים חדשים בתור+@id	tray.close_to_tray	המשך לפעול בשורת המשימות כשהחלון נסגר+@id	tray.show_on_current_desktop	הצג את החלון בשולחן העבודה הנוכחי+@id	tray.login_item	הפעל את ArtCraft Launcher בעת הכניסה למערכת+@id	appearance.theme	מראה+	Show ArtCraft Launcher	הצג את ArtCraft Launcher+	Check for Updates	בדוק עדכונים+	Visit getartcraft.com	בקרו ב-getartcraft.com+	ArtCraft on GitHub	ArtCraft ב-GitHub+	Join the ArtCraft Discord	הצטרפו ל-Discord של ArtCraft+@plural	ArtCraft Launcher · {n} update available|ArtCraft Launcher · {n} updates available	ArtCraft Launcher · עדכון {n} זמין|ArtCraft Launcher · {n} עדכונים זמינים|ArtCraft Launcher · {n} עדכונים זמינים+	Install folder	תיקיית התקנה+	Default install folder	תיקיית התקנה כברירת מחדל+	System default	ברירת המחדל של המערכת+	Browse…	עיון…+	Use default	השתמש בברירת מחדל+	New installs use this folder. Updates stay in their current location.	התקנות חדשות משתמשות בתיקייה זו. עדכונים נשארים במיקומם הנוכחי.+	Custom folders use AppImage on Linux.	תיקיות מותאמות אישית משתמשות ב-AppImage בלינוקס.+	On Linux, custom folders use AppImage format.	בלינוקס, תיקיות מותאמות אישית משתמשות בתבנית AppImage.+	The system manages install locations here.	המערכת מנהלת כאן את מיקומי ההתקנה.+@msg	couldn't use install folder {path}: {reason}	לא ניתן להשתמש בתיקיית ההתקנה {path}: {reason}+@msg	this release has no AppImage for a custom install folder	לגרסה זו אין AppImage עבור תיקיית התקנה מותאמת אישית+@msg	custom install folders aren't supported on this system	תיקיות התקנה מותאמות אישית אינן נתמכות במערכת זו+@msg	{path} already exists and wasn't installed by ArtCraft Launcher	{path} כבר קיים ולא הותקן על ידי ArtCraft Launcher+@msg	couldn't move {path} to Trash ({reason}). Remove it in Finder instead.	לא ניתן להעביר את {path} לאשפה ({reason}). הסר אותו ב-Finder במקום זאת.+	Apps are placed inside this folder.	היישומים ממוקמים בתוך תיקייה זו.+	Replace what's there?	להחליף את מה שקיים שם?+	{path} already exists. Installing {name} will overwrite it.	{path} כבר קיים. התקנת {name} תחליף אותו.+	Replace	החלף+	Recently updated	עודכנו לאחרונה+	Name (A–Z)	שם (א–ת)+	Name (Z–A)	שם (ת–א)+	Frequently used	בשימוש תדיר+	Next app	היישום הבא+	Previous app	היישום הקודם+	Show {n} more changes	הצג עוד {n} שינויים+	Last updated: {date}	עודכן לאחרונה: {date}+column	Version	גרסהdiff --git a/crates/ui-egui/src/i18n/lang.rs b/crates/ui-egui/src/i18n/lang.rs--- a/crates/ui-egui/src/i18n/lang.rs+++ b/crates/ui-egui/src/i18n/lang.rs@@ -13,30 +13,33 @@ use super::detect::system_lang; pub enum Script {     Chinese,     Japanese,+    Hebrew, }  impl Script {     /// Every script a language can be written in.-    pub const ALL: [Script; 2] = [Script::Chinese, Script::Japanese];+    pub const ALL: [Script; 3] = [Script::Chinese, Script::Japanese, Script::Hebrew]; }  /// One supported UI language. pub struct LangInfo {-    /// Canonical BCP 47 code (`en`, `ru`, `zh-CN`, `de`, `fr`, `pt-BR`, `es`, `cs`, `pl`, `hu`, `ja`).+    /// Canonical BCP 47 code (`en`, `ru`, `zh-CN`, `de`, `fr`, `pt-BR`, `es`, `cs`, `pl`, `hu`, `ja`, `he`).     /// Also the `interfaceLanguage` value.     pub code: &'static str,     /// The language's name in itself, shown in the Preferences dropdown.     pub name: &'static str,     /// The language's name in English, shown instead where its own script can't be drawn.     pub english_name: &'static str,-    /// The writing system this language is drawn in, or `None` if it needs no system CJK font.+    /// The writing system this language is drawn in, or `None` if it needs no system font.     pub script: Option<Script>,+    /// Whether this language is written right-to-left (Hebrew, Arabic).+    pub rtl: bool,     /// Catalog file contents (empty for the built-in English).     pub source: &'static str,     /// Plural form index for a count (English, German, Spanish and Hungarian: 0 = one, 1 = other;     /// Russian: 0 = one, 1 = few, 2 = many; Czech: 0 = one, 1 = few, 2 = other; Polish: 0 = one only-    /// for 1, 1 = few, 2 = many; French: 0 = one for 0 and 1; a language whose nouns don't inflect-    /// for number: always 0).+    /// for 1, 1 = few, 2 = many; Hebrew: 0 = one, 1 = two, 2 = other; French: 0 = one for 0 and 1;+    /// a language whose nouns don't inflect for number: always 0).     pub plural: fn(u64) -> usize,     /// Number of plural forms the catalog's `@plural` rows carry (checked by tests).     pub plural_forms: usize,@@ -93,13 +96,23 @@ pub(super) fn plural_polish(n: u64) -> usize {     } } +/// Hebrew integer counts (CLDR 44+): 1 is `one` (0), 2 is `two` (1), everything else is `other` (2).+pub(super) fn plural_hebrew(n: u64) -> usize {+    match n {+        1 => 0,+        2 => 1,+        _ => 2,+    }+}+ /// The registry. English first: it is the fallback and the source language.-pub static LANGUAGES: [LangInfo; 11] = [+pub static LANGUAGES: [LangInfo; 12] = [     LangInfo {         code: "en",         name: "English",         english_name: "English",         script: None,+        rtl: false,         source: "",         plural: plural_one_other,         plural_forms: 2,@@ -111,6 +124,7 @@ pub static LANGUAGES: [LangInfo; 11] = [         name: "Русский",         english_name: "Russian",         script: None,+        rtl: false,         source: include_str!("ru.tsv"),         plural: plural_east_slavic,         plural_forms: 3,@@ -122,6 +136,7 @@ pub static LANGUAGES: [LangInfo; 11] = [         name: "简体中文",         english_name: "Simplified Chinese",         script: Some(Script::Chinese),+        rtl: false,         source: include_str!("zh-CN.tsv"),         plural: plural_none,         plural_forms: 1,@@ -133,6 +148,7 @@ pub static LANGUAGES: [LangInfo; 11] = [         name: "Deutsch",         english_name: "German",         script: None,+        rtl: false,         source: include_str!("de.tsv"),         plural: plural_one_other,         plural_forms: 2,@@ -144,6 +160,7 @@ pub static LANGUAGES: [LangInfo; 11] = [         name: "Français",         english_name: "French",         script: None,+        rtl: false,         source: include_str!("fr.tsv"),         plural: plural_zero_one,         plural_forms: 2,@@ -155,6 +172,7 @@ pub static LANGUAGES: [LangInfo; 11] = [         name: "Português (Brasil)",         english_name: "Portuguese (Brazil)",         script: None,+        rtl: false,         source: include_str!("pt-BR.tsv"),         plural: plural_one_other,         plural_forms: 2,@@ -166,6 +184,7 @@ pub static LANGUAGES: [LangInfo; 11] = [         name: "Español",         english_name: "Spanish",         script: None,+        rtl: false,         source: include_str!("es.tsv"),         plural: plural_one_other,         plural_forms: 2,@@ -177,6 +196,7 @@ pub static LANGUAGES: [LangInfo; 11] = [         name: "Čeština",         english_name: "Czech",         script: None,+        rtl: false,         source: include_str!("cs.tsv"),         plural: plural_czech,         plural_forms: 3,@@ -188,6 +208,7 @@ pub static LANGUAGES: [LangInfo; 11] = [         name: "Polski",         english_name: "Polish",         script: None,+        rtl: false,         source: include_str!("pl.tsv"),         plural: plural_polish,         plural_forms: 3,@@ -199,6 +220,7 @@ pub static LANGUAGES: [LangInfo; 11] = [         name: "Magyar",         english_name: "Hungarian",         script: None,+        rtl: false,         source: include_str!("hu.tsv"),         plural: plural_one_other,         plural_forms: 2,@@ -210,15 +232,29 @@ pub static LANGUAGES: [LangInfo; 11] = [         name: "日本語",         english_name: "Japanese",         script: Some(Script::Japanese),+        rtl: false,         source: include_str!("ja.tsv"),         plural: plural_none,         plural_forms: 1,         complete_menus: true,         catalog: OnceLock::new(),     },+    LangInfo {+        code: "he",+        name: "עברית",+        english_name: "Hebrew",+        script: Some(Script::Hebrew),+        rtl: true,+        source: include_str!("he.tsv"),+        plural: plural_hebrew,+        plural_forms: 3,+        complete_menus: true,+        catalog: OnceLock::new(),+    }, ];  impl LangInfo {+    /// Lazily parses and returns this language's translation catalog.     pub(super) fn catalog(&self) -> &Catalog {         self.catalog.get_or_init(|| Catalog::parse(self.source))     }@@ -254,7 +290,9 @@ impl Lang {     pub const PL: Lang = Lang(&LANGUAGES[8]);     pub const HU: Lang = Lang(&LANGUAGES[9]);     pub const JA: Lang = Lang(&LANGUAGES[10]);+    pub const HE: Lang = Lang(&LANGUAGES[11]); +    /// Canonical BCP 47 language code (`en`, `he`, `zh-CN`, …).     pub fn code(self) -> &'static str {         self.0.code     }@@ -286,6 +324,7 @@ impl Lang {         std::iter::once(Lang::EN).chain(rest).collect()     } +    /// The language's autonym (native display name).     pub fn name(self) -> &'static str {         self.0.name     }@@ -300,11 +339,16 @@ impl Lang {         self.0.script.is_some()     } -    /// The writing system this language is drawn in, if it needs a system CJK font.+    /// The writing system this language is drawn in, if it needs a system font.     pub fn script(self) -> Option<Script> {         self.0.script     } +    /// Is this language written right-to-left?+    pub fn rtl(self) -> bool {+        self.0.rtl+    }+     /// Does this language's catalog claim to cover every `tl!` literal?     pub fn complete_menus(self) -> bool {         self.0.complete_menus@@ -315,6 +359,7 @@ impl Lang {         LANGUAGES.iter().position(|l| l.code == self.0.code).unwrap_or(0)     } +    /// Returns the parsed translation catalog for this language.     pub(super) fn catalog(self) -> &'static Catalog {         self.0.catalog()     }@@ -337,7 +382,11 @@ pub(super) fn candidates(tag: &str) -> Vec<String> {         return Vec::new();     }     let base = head.replace('_', "-").to_ascii_lowercase();-    let parts: Vec<&str> = base.split('-').filter(|p| !p.is_empty()).take(MAX_SUBTAGS).collect();+    let mut parts: Vec<&str> = base.split('-').filter(|p| !p.is_empty()).take(MAX_SUBTAGS).collect();+    // Normalize the deprecated ISO 639-1 code for Hebrew (`iw` -> `he`).+    if let Some(first @ &mut "iw") = parts.first_mut() {+        *first = "he";+    }     (1..=parts.len()).rev().filter_map(|n| parts.get(..n)).map(|p| p.join("-")).collect() } diff --git a/crates/ui-egui/src/i18n/tests.rs b/crates/ui-egui/src/i18n/tests.rs--- a/crates/ui-egui/src/i18n/tests.rs+++ b/crates/ui-egui/src/i18n/tests.rs@@ -4,7 +4,7 @@ use std::collections::{BTreeSet, HashSet};  use super::catalog::{Catalog, unescape};-use super::lang::{name_sort_key, plural_czech, plural_east_slavic, plural_polish};+use super::lang::{candidates, name_sort_key, plural_czech, plural_east_slavic, plural_hebrew, plural_polish}; use super::{LANGUAGES, Lang, LangInfo, lang_from_tag, message, tr, trn};  /// Each catalog's rows as (line number, context, source, translation), split exactly as the@@ -225,6 +225,7 @@ fn czech_plural_rule_works() {     assert_eq!(trn(Lang::CS, 5, "{n} app", "{n} apps"), "5 aplikací"); } +/// Polish plural forms and English fallback for untranslated plural keys. #[test] fn polish_plural_rule_works() {     // Polish says "one" only for 1 itself: 21, 31 and 101 take "many", unlike Russian.@@ -244,6 +245,29 @@ fn polish_plural_rule_works() {     assert_eq!(trn(Lang::PL, 3, "{n} unknown item", "{n} unknown items"), "3 unknown items"); } +/// CLDR 44+ Hebrew plural indices (one/two/other), legacy `iw` locale tag normalization, and `Lang::HE` resolution.+#[test]+fn hebrew_plural_rule_and_legacy_tag_candidates_work() {+    assert_eq!(plural_hebrew(1), 0);+    assert_eq!(plural_hebrew(2), 1);+    for n in [0, 3, 4, 10, 11, 20, 21, 100] {+        assert_eq!(plural_hebrew(n), 2, "{n}");+    }+    assert_eq!(candidates("iw_IL.UTF-8"), vec!["he-il", "he"]);+    assert_eq!(candidates("iw"), vec!["he"]);+    for tag in ["he", "he-IL", "he_IL.UTF-8", "iw", "iw-IL", "iw_IL.UTF-8", "HE_il"] {+        assert_eq!(lang_from_tag(tag), Some(Lang::HE), "{tag}");+    }+    assert_eq!(Lang::from_code("he"), Some(Lang::HE));+    assert!(Lang::HE.rtl());+    assert_eq!(Lang::HE.name(), "עברית");+    assert_eq!(Lang::HE.english_name(), "Hebrew");+    assert_eq!(trn(Lang::HE, 1, "{n} app", "{n} apps"), "יישום 1");+    assert_eq!(trn(Lang::HE, 2, "{n} app", "{n} apps"), "2 יישומים");+    assert_eq!(trn(Lang::HE, 5, "{n} app", "{n} apps"), "5 יישומים");+}++/// Untranslated plural keys fall back to the English singular/plural form by count. #[test] fn untranslated_plurals_fall_back_to_english_by_count() {     // Russian's rule says "one" for 21, but English says "other".diff --git a/crates/ui-egui/src/settings_dialog.rs b/crates/ui-egui/src/settings_dialog.rs--- a/crates/ui-egui/src/settings_dialog.rs+++ b/crates/ui-egui/src/settings_dialog.rs@@ -158,8 +158,14 @@ impl LauncherUi {         let pref = crate::i18n::Lang::from_pref(&self.settings.language);         if pref.needs_cjk_font() && !theme::cjk_font_available(pref) {             // In English on purpose: the language asked for can't be drawn.-            let text = "No font for this language was found on this system, so the interface stays in English. \-                        Installing one (for example Noto Sans CJK) and restarting fixes it.";+            let font_example = match pref.script() {+                Some(crate::i18n::Script::Hebrew) => "Noto Sans Hebrew",+                _ => "Noto Sans CJK",+            };+            let text = format!(+                "No font for this language was found on this system, so the interface stays in English. \+                 Installing one (for example {font_example}) and restarting fixes it."+            );             ui.add(egui::Label::new(RichText::new(text).size(12.0).color(t.warn)).wrap());             ui.add_space(14.0);         }diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs--- a/crates/ui-egui/src/widgets.rs+++ b/crates/ui-egui/src/widgets.rs@@ -23,7 +23,11 @@ pub fn tone_color(t: &Tokens, tone: Tone) -> Color32 { /// `text` laid out upper case in `family`, `spacing` points between letters. pub fn caps(ui: &Ui, text: &str, size: f32, family: egui::FontFamily, spacing: f32, color: Color32) -> Arc<Galley> {     let format = egui::TextFormat { font_id: egui::FontId::new(size, family), color, extra_letter_spacing: spacing, ..Default::default() };-    ui.painter().layout_job(egui::text::LayoutJob::single_section(text.to_uppercase(), format))+    let mut job = egui::text::LayoutJob::single_section(text.to_uppercase(), format);+    if crate::i18n::current().rtl() {+        job.direction = egui::epaint::text::TextDirection::RightToLeft;+    }+    ui.painter().layout_job(job) }  /// A `.hud-label`: 11 px Geist Mono, upper case, widely spaced.@@ -61,6 +65,9 @@ pub fn hud_elided(ui: &mut Ui, text: &str, color: Color32, max_width: f32) -> Re         ..Default::default()     };     let mut job = egui::text::LayoutJob::single_section(text.to_uppercase(), format);+    if crate::i18n::current().rtl() {+        job.direction = egui::epaint::text::TextDirection::RightToLeft;+    }     job.wrap = egui::text::TextWrapping::truncate_at_width(max_width.min(ui.available_width()).max(0.0));     let galley = ui.painter().layout_job(job);     let elided = galley.elided;@@ -659,7 +666,7 @@ pub fn wordmark(ui: &mut Ui, app: &artcraft_launcher_core::CraftApp, size: f32) pub fn wordmark_in(ui: &mut Ui, app: &artcraft_launcher_core::CraftApp, size: f32, in_colour: bool) -> Response {     let t = Tokens::get(ui.ctx());     let (prefix, craft) = app.name.strip_suffix("Craft").map_or((app.name, ""), |p| (p, "Craft"));-    let mut job = egui::text::LayoutJob::default();+    let mut job = egui::text::LayoutJob { direction: egui::epaint::text::TextDirection::LeftToRight, ..Default::default() };     let format = |color| egui::TextFormat {         font_id: egui::FontId::new(size, theme::display()),         color,