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
@@ -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('…');@@ -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.new file mode 100644@@ -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 גרסה@@ -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() } @@ -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".@@ -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); }@@ -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,