Browse / github.com/storytold/craft-launcher / patch
i18n: add Georgian (ka) interface language
i18n: add Georgian (ka) interface language **Depends on #805** (Georgian system font): this branch is stacked on it, and Georgian must not merge without it. Without #805 the bundled faces have no Georgian letters, so the desktop interface would draw boxes. Please merge #805 first; this PR's own commit is the last one. ## Summary Adds Georgian (`ka`, ქართული) in the same shape as Czech (#167) and Japanese (#177): - `crates/ui-egui/src/i18n/ka.tsv`: 341 rows, every key of the most complete catalogs (`cs`, `pl`): the interface, the tray menu and tooltip, each app's tagline, month names and the backend's install and update messages. `co
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- devius (imported from pull request #806, open)
- Base
4ab652afd834081ff76a47ffb752fbbf9424f9c1- Size
- +791 −34 in 10 files
- Review
- pending
- Shared
- 2026-10-28
- Id
69c0c249d08dc467c19e9caf
How it went
No other operator has reported using it yet.
Diff
raw · AGENTS.md README.md crates/ui-egui/src/cjk_font.rs crates/ui-egui/src/i18n/ka.tsv crates/ui-egui/src/i18n/lang.rs crates/ui-egui/src/i18n/mod.rs crates/ui-egui/src/i18n/tests.rs crates/ui-egui/src/settings_dialog.rs crates/ui-egui/src/widgets.rs crates/ui-egui/tests/georgian.rs
@@ -95,7 +95,8 @@ means adding its row to every catalog marked `complete_menus` (the `*.tsv` files the English text, never through another catalog. Product and app names (ArtCraft Launcher, Craft App Suite, PhotoCraft…) stay in English. A CJK language is drawn with the system's own CJK font (`crates/ui-egui/src/cjk_font.rs`, desktop-only; English where there is none): never bundle a CJK font, they are 10 MB and more.+only; English where there is none): never bundle a CJK font, they are 10 MB and more. Georgian+borrows the system's Georgian font the same way. ### Other rules @@ -92,23 +92,26 @@ The web version shows the same catalog in the browser, with the latest download ## Interface languages The interface is in English, Brazilian Portuguese (Português (Brasil)), Czech (Čeština), French-(Français), German (Deutsch), Hungarian (Magyar), Japanese (日本語), Polish (Polski), Russian-(Русский), Simplified Chinese (简体中文) or Spanish (Español). **Auto**, the default, follows the-system or browser language; **Settings → Language** picks one and switches at once, and the choice-is kept between runs. Czech locales (`cs-CZ`, `cs_CZ.UTF-8`…) select Czech, German ones (`de-DE`,-`de-AT`, `de-CH`, `de_DE.UTF-8`…) German, French ones (`fr-FR`, `fr-CA`…) French, Hungarian ones-(`hu`, `hu-HU`, `hu_HU.UTF-8`…) Hungarian, Japanese ones (`ja`, `ja-JP`, `ja_JP.UTF-8`…) Japanese,-Polish ones (`pl`, `pl-PL`, `pl_PL.UTF-8`) Polish, Spanish ones (`es-ES`, `es-MX`, `es-419`…)-Spanish, and Portuguese ones (`pt-BR`, and `pt-PT` too, for now) Brazilian Portuguese. Simplified-Chinese locales (`zh-CN`, `zh-SG`, `zh-Hans`, and POSIX forms such as `zh_CN.UTF-8`) select-Chinese; Traditional Chinese ones don't. To try a language without changing the setting, start the-desktop app with `ARTCRAFT_LAUNCHER_LOCALE=pt-BR`, or add `?lang=ru` to the web address.--The CJK languages are drawn with the system's own CJK font (Hiragino Sans or PingFang on macOS,-Yu Gothic, Meiryo or Microsoft YaHei on Windows, Noto Sans CJK, IPA, Takao or WenQuanYi on Linux and+(Français), Georgian (ქართული), German (Deutsch), Hungarian (Magyar), Japanese (日本語), Polish+(Polski), Russian (Русский), Simplified Chinese (简体中文) or Spanish (Español). **Auto**, the default,+follows the system or browser language; **Settings → Language** picks one and switches at once, and+the choice is kept between runs. Czech locales (`cs-CZ`, `cs_CZ.UTF-8`…) select Czech, Georgian ones+(`ka`, `ka-GE`, `ka_GE.UTF-8`…) Georgian, German ones (`de-DE`, `de-AT`, `de-CH`, `de_DE.UTF-8`…)+German, French ones (`fr-FR`, `fr-CA`…) French, Hungarian ones (`hu`, `hu-HU`, `hu_HU.UTF-8`…)+Hungarian, Japanese ones (`ja`, `ja-JP`, `ja_JP.UTF-8`…) Japanese, Polish ones (`pl`, `pl-PL`,+`pl_PL.UTF-8`) Polish, Spanish ones (`es-ES`, `es-MX`, `es-419`…) Spanish, and Portuguese ones+(`pt-BR`, and `pt-PT` too, for now) Brazilian Portuguese. Simplified Chinese locales (`zh-CN`,+`zh-SG`, `zh-Hans`, and POSIX forms such as `zh_CN.UTF-8`) select Chinese; Traditional Chinese ones+don't. To try a language without changing the setting, start the desktop app with+`ARTCRAFT_LAUNCHER_LOCALE=pt-BR`, or add `?lang=ru` to the web address.++The CJK languages are drawn with the system's own CJK font (Hiragino Sans or PingFang on macOS, Yu+Gothic, Meiryo or Microsoft YaHei on Windows, Noto Sans CJK, IPA, Takao or WenQuanYi on Linux and FreeBSD). Where the system has no font for one, and in the web version, the interface stays in-English and Settings says why. Product names and release notes stay as published; text without a-translation falls back to English.+English and Settings says why. Georgian is drawn the same way, with the system's Georgian font (SF+Georgian or Arial Unicode on macOS, Sylfaen on Windows, Noto Sans Georgian or DejaVu Sans on Linux+and FreeBSD). Product names and release notes stay as published; text without a translation falls+back to English. ## Where apps are installed @@ -1,6 +1,8 @@ //! A CJK font from the operating system, for the CJK interface languages. The bundled faces have //! no CJK glyphs, and a CJK font is too large to ship (10 MB or more), so the desktop build //! borrows the one every system that can show them already has. Desktop only.+//!+//! Georgian, which the bundled faces don't draw either, borrows its system face the same way. use std::path::{Path, PathBuf}; @@ -40,6 +42,7 @@ fn script_name(script: Script) -> &'static str { match script { Script::Chinese => "Chinese", Script::Japanese => "Japanese",+ Script::Georgian => "Georgian", } } @@ -48,6 +51,7 @@ 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::Georgian => "SF Georgian, Arial Unicode MS, Sylfaen, Noto Sans Georgian and DejaVu Sans", } } @@ -76,6 +80,11 @@ pub(crate) fn load(script: Script) -> Option<FontData> { y_offset_factor: (body_ascent - face_ascent) + 0.5 * (face_height - body_height), ..Default::default() };+ if matches!(script, Script::Georgian) {+ // Upper-case labels use Mtavruli only where this face draws it.+ let caps = FontRef::from_index(&bytes, index).is_ok_and(|f| ('\u{1c90}'..='\u{1cba}').all(|c| f.charmap().map(c).is_some()));+ crate::widgets::set_georgian_caps(caps);+ } let mut font = FontData::from_owned(bytes).tweak(tweak); font.index = index; return Some(font);@@ -123,6 +132,7 @@ fn candidates(script: Script) -> Vec<Candidate> { match script { Script::Chinese => chinese_candidates(), Script::Japanese => japanese_candidates(),+ Script::Georgian => georgian_candidates(), } } @@ -305,6 +315,50 @@ fn japanese_candidates() -> Vec<Candidate> { out } +/// Where each system keeps a Georgian font, best first. These faces are small (about 0.1–1 MB),+/// except Arial Unicode MS, which macOS ships too.+fn georgian_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")]+ {+ // SF Georgian is the system UI face for Georgian; its family name starts with a dot, as+ // macOS's hidden UI faces do.+ add("/System/Library/Fonts/SFGeorgian.ttf".into(), ".SF Georgian");+ add("/System/Library/Fonts/Supplemental/Arial Unicode.ttf".into(), "Arial Unicode MS");+ add("/Library/Fonts/Arial Unicode.ttf".into(), "Arial Unicode MS");+ }+ #[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("sylfaen.ttf"), "Sylfaen");+ }+ #[cfg(all(unix, not(target_os = "macos")))]+ {+ // Debian/Ubuntu, Arch, Fedora (static and variable) and FreeBSD's ports.+ for path in [+ "/usr/share/fonts/truetype/noto/NotoSansGeorgian-Regular.ttf",+ "/usr/share/fonts/noto/NotoSansGeorgian-Regular.ttf",+ "/usr/share/fonts/google-noto/NotoSansGeorgian-Regular.ttf",+ "/usr/share/fonts/google-noto-vf/NotoSansGeorgian[wght].ttf",+ "/usr/local/share/fonts/noto/NotoSansGeorgian-Regular.ttf",+ ] {+ add(path.into(), "Noto Sans Georgian");+ }+ // DejaVu Sans draws Georgian too, and most desktops have it.+ for path in [+ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",+ "/usr/share/fonts/TTF/DejaVuSans.ttf",+ "/usr/share/fonts/dejavu-sans-fonts/DejaVuSans.ttf",+ "/usr/share/fonts/dejavu/DejaVuSans.ttf",+ "/usr/local/share/fonts/dejavu/DejaVuSans.ttf",+ ] {+ add(path.into(), "DejaVu Sans");+ }+ }+ out+}+ /// `file` in macOS's downloaded font assets: /// `/System/Library/AssetsV2/com_apple_MobileAsset_Font*/*.asset/AssetData/<file>`. #[cfg(target_os = "macos")]new file mode 100644@@ -0,0 +1,389 @@+# Georgian (ka) catalog for ArtCraft Launcher.+# Format: context<TAB>source<TAB>translation+# Empty context = plain UI string (tl!, t).+# @id = command-id keyed (tr_id).+# @plural = plural forms: one|other (English and Georgian).+# @msg = status/error message template (message, msg).+# Other context = disambiguation for tr_ctx.+#+# Clean-room: written from the meaning of ArtCraft Launcher's English labels in neutral Georgian,+# with the terms the other Craft apps use in Georgian. The user is addressed with the polite+# plural (თქვენ), as in Georgian system software. Commands are verbal nouns (ინსტალაცია,+# განახლება, გახსნა). Georgian has no letter case: no Mtavruli capitals, even where the English+# uses capitals ("NEW").+#+# Coverage: the whole interface — every menu string and tl! literal (complete_menus = true).+# Left in English on purpose: product name "ArtCraft Launcher" (and its "Launcher" tag), suite+# name "Craft App Suite", app names (PhotoCraft, LightCraft, FilmCraft, DesignCraft, VectorCraft,+# PdfCraft, EffectCraft, WordCraft, DeckCraft, GridCraft, CADCraft, SoundCraft, FigCraft), "GitHub",+# "Discord". Georgian case endings attach to them with a hyphen (GitHub-ზე, ArtCraft-ის).+# Shortcut keys keep their key-cap names (Shift, Alt, Ctrl, Cmd, Esc).+# Trailing "…" (U+2026) must be preserved. Placeholders {name}, {n}, {version}, {count}, {v},+# {what}, {unit}, {real}, {e} must appear in translation.+# Georgian is drawn with the system's Georgian font (see cjk_font.rs); where the system has none,+# the interface stays in English.++ 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 კლასიკური აპლიკაციები+ New apps ახალი აპლიკაციები+ Classic კლასიკური+ 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 Explorer-ში ჩვენება+ Show in Files 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 ინსტალაცია 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 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… სხვა ინსტალაციების მოლოდინი…++# Package formats offered for download (PackageKind::label)+ 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-არქივი (.tar.gz)++# Contextual: each app's tagline, keyed by app id+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 ინტერფეისის დიზაინი და პროტოტიპირება++# Contextual: month abbreviations for date rendering+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. ჩართვისას მენიუს ზოლიდან გახსნილი ArtCraft Launcher-ის ფანჯარა იმ სამუშაო მაგიდაზე გამოჩნდება, რომელზეც ახლა ხართ. გამორთვისას macOS გადაგიყვანთ იმ სამუშაო მაგიდაზე, სადაც ფანჯარა იყო.+ Couldn't change the login item: {e} სისტემაში შესვლისას გაშვების შეცვლა ვერ მოხერხდა: {e}++# Contextual: table column headers+column App აპლიკაცია+column Installed დაინსტალირებული+column Latest უახლესი+column Released გამოშვება++# Contextual: the filter tab names a group (the status pill says "Installed")+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 forms for Georgian (one|other; a noun after a numeral stays singular, so both forms match)+@plural {n} app|{n} apps {n} აპლიკაცია|{n} აპლიკაცია+@plural {n} min ago|{n} min ago {n} წუთის წინ|{n} წუთის წინ+@plural {n} h ago|{n} h ago {n} საათის წინ|{n} საათის წინ+@plural {n} day ago|{n} days ago {n} დღის წინ|{n} დღის წინ++# Message templates (@msg): toasts from the backend. {placeholders} match any text.+@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})++# Command IDs for menu items (@id)+# Format: @id<TAB>command_id<TAB>translation+@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 იერსახე++# Tray (menu bar) menu and tooltip+ Show ArtCraft Launcher ArtCraft Launcher-ის ჩვენება+ Check for Updates განახლებების შემოწმება+ Visit getartcraft.com getartcraft.com-ის გახსნა+ ArtCraft on GitHub ArtCraft GitHub-ზე+ Join the ArtCraft Discord შემოუერთდით ArtCraft-ის Discord-ს+@plural ArtCraft Launcher · {n} update available|ArtCraft Launcher · {n} updates available 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. Linux-ზე არჩეულ საქაღალდეში ინსტალაცია AppImage-ით ხდება.+ On Linux, custom folders use AppImage format. Linux-ზე არჩეულ საქაღალდეში ინსტალაცია 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) სახელი (A–Z)+ Name (Z–A) სახელი (Z–A)+ Frequently used ხშირად გამოყენებული+ Next app შემდეგი აპლიკაცია+ Previous app წინა აპლიკაცია+ Show {n} more changes კიდევ {n} ცვლილების ჩვენება+ Last updated: {date} ბოლო განახლება: {date}+column Version ვერსია@@ -7,35 +7,37 @@ use super::detect::system_lang; /// The writing system a language is drawn in, and so the system font it needs. Languages that /// share one can share a face; Simplified and Traditional Chinese do not (they want different-/// faces), so each script is its own. The bundled faces have no CJK glyphs, so a language with a-/// script borrows a system font for it (`crate::theme::cjk_font`).+/// faces), so each script is its own. The bundled faces have no CJK or Georgian glyphs, so a+/// language with a script borrows a system font for it (`crate::theme::cjk_font`). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Script { Chinese, Japanese,+ Georgian, } 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::Georgian]; } /// One supported UI language. pub struct LangInfo {- /// Canonical BCP 47 code (`en`, `ru`, `zh-CN`, `de`, `fr`, `pt-BR`, `es`, `cs`, `pl`, `hu`, `ja`).- /// Also the `interfaceLanguage` value.+ /// Canonical BCP 47 code (`en`, `ru`, `zh-CN`, `de`, `fr`, `pt-BR`, `es`, `cs`, `pl`, `hu`, `ja`,+ /// `ka`). 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+ /// (CJK, Georgian). pub script: Option<Script>, /// 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+ /// Plural form index for a count (English, German, Spanish, Hungarian and Georgian: 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). pub plural: fn(u64) -> usize, /// Number of plural forms the catalog's `@plural` rows carry (checked by tests).@@ -94,7 +96,7 @@ pub(super) fn plural_polish(n: u64) -> usize { } /// 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",@@ -216,6 +218,17 @@ pub static LANGUAGES: [LangInfo; 11] = [ complete_menus: true, catalog: OnceLock::new(), },+ LangInfo {+ code: "ka",+ name: "ქართული",+ english_name: "Georgian",+ script: Some(Script::Georgian),+ source: include_str!("ka.tsv"),+ plural: plural_one_other,+ plural_forms: 2,+ complete_menus: true,+ catalog: OnceLock::new(),+ }, ]; impl LangInfo {@@ -254,6 +267,7 @@ 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 KA: Lang = Lang(&LANGUAGES[11]); pub fn code(self) -> &'static str { self.0.code@@ -295,12 +309,12 @@ impl Lang { self.0.english_name } - /// Does drawing this language need a CJK font from the system?+ /// Does drawing this language need a font from the system (CJK, Georgian)? pub fn needs_cjk_font(self) -> bool { 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 }@@ -8,8 +8,8 @@ //! catalog, cover every UI literal and plural key; the tests enforce that claim. A translation //! that isn't finished yet starts with `complete_menus: false`: it still shows in //! Preferences, strings without a row fall back to English, and the completeness tests-//! skip it until every row is done. A CJK language also sets `script` and takes its system-//! font from `crate::cjk_font`.+//! skip it until every row is done. A CJK or Georgian language also sets `script` and takes+//! its system font from `crate::cjk_font`. //! //! That is all: the Preferences dropdown, the system-locale match and the catalog tests //! (parse, placeholders, plural forms, coverage of every `tl!` literal) pick it up from the@@ -400,6 +400,26 @@ fn japanese_locales_and_counts_work() { assert_eq!(message(Lang::JA, "PhotoCraft updated to 0.5.0"), "PhotoCraft を 0.5.0 に更新しました"); } +#[test]+fn georgian_locales_and_counts_work() {+ for tag in ["ka", "ka-GE", "ka_GE.UTF-8", "KA_ge"] {+ assert_eq!(lang_from_tag(tag), Some(Lang::KA), "{tag}");+ }+ assert_eq!(super::detect::first_supported(["xx-XX", "ka-GE", "en-US"]), Some(Lang::KA));+ // After a number the noun stays singular, so both forms are the same; 1 is still "one".+ for n in [0, 1, 2, 21, 100] {+ assert_eq!(trn(Lang::KA, n, "{n} app", "{n} apps"), format!("{n} აპლიკაცია"));+ assert_eq!(trn(Lang::KA, n, "{n} day ago", "{n} days ago"), format!("{n} დღის წინ"));+ }+ assert_eq!(trn(Lang::KA, 2, "{n} unknown item", "{n} unknown items"), "2 unknown items");+ assert_eq!(tr(Lang::KA, "Untranslated diagnostic"), "Untranslated diagnostic");+ assert_eq!(super::tr_ctx(Lang::KA, "month", "Mar"), "მარ");+ assert_eq!(tr(Lang::KA, "{month} {day}, {year}"), "{day} {month}, {year}");+ assert_eq!(message(Lang::KA, "PhotoCraft updated to 0.5.0"), "PhotoCraft განახლდა ვერსიამდე 0.5.0");+ // Georgian has no letter case: no Mtavruli (U+1C90–U+1CBF) in the interface.+ assert!(!Lang::KA.0.source.chars().any(|c| ('\u{1c90}'..='\u{1cbf}').contains(&c)));+}+ /// With the system's Chinese font installed, every UI family can draw the whole Chinese catalog. /// Machines without one (CI, the web) skip this; the UI then stays in English. #[test]@@ -468,6 +488,40 @@ fn system_font_covers_japanese_in_every_ui_family() { } } +/// With a system Georgian font installed, every UI family can draw the whole Georgian catalog.+/// Machines without one (the web, minimal Linux installs) skip this.+#[test]+fn system_font_covers_georgian_in_every_ui_family() {+ use egui::{FontFamily, FontId};+ if !crate::theme::cjk_font_available(Lang::KA) {+ return;+ }+ let ctx = egui::Context::default();+ crate::theme::install_fonts(&ctx, Some(Lang::KA));+ let mut output = ctx.run_ui(egui::RawInput::default(), |_| {});+ output.textures_delta.clear();+ let text: String = Lang::KA+ .0+ .source+ .lines()+ .filter_map(|line| line.split('\t').nth(2))+ .flat_map(str::chars)+ .filter(|c| ('\u{10d0}'..='\u{10ff}').contains(c))+ .collect();+ assert!(text.chars().count() > 1000);+ for family in [+ FontFamily::Proportional,+ FontFamily::Monospace,+ crate::theme::medium(),+ crate::theme::semibold(),+ crate::theme::display(),+ crate::theme::mono(),+ crate::theme::mono_bold(),+ ] {+ ctx.fonts_mut(|fonts| assert!(fonts.has_glyphs(&FontId::new(14.0, family.clone()), &text), "missing glyph in {family:?}"));+ }+}+ #[test] fn language_labels_come_from_the_registry() { use crate::settings::Settings;@@ -480,6 +534,8 @@ fn language_labels_come_from_the_registry() { assert_eq!(Settings::language_label(Lang::ZH_CN.code(), Some(Lang::ZH_CN)), "Simplified Chinese (简体中文)"); assert_eq!(Settings::language_label(Lang::JA.code(), Some(Lang::ZH_CN)), "Japanese"); assert_eq!(Settings::language_label(Lang::JA.code(), Some(Lang::JA)), "Japanese (日本語)");+ assert_eq!(Settings::language_label(Lang::KA.code(), None), "Georgian");+ assert_eq!(Settings::language_label(Lang::KA.code(), Some(Lang::KA)), "Georgian (ქართული)"); assert_eq!(Settings::language_label(Settings::LANGUAGE_AUTO, None), "Auto"); assert_eq!(Settings::language_label("xx", None), "Auto"); }@@ -159,7 +159,7 @@ impl LauncherUi { 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.";+ Installing one (for example Noto Sans CJK or Noto Sans Georgian) and restarting fixes it."; ui.add(egui::Label::new(RichText::new(text).size(12.0).color(t.warn)).wrap()); ui.add_space(14.0); }@@ -4,6 +4,7 @@ //! Upper-case text is drawn upper case but announced to screen readers as written. use std::sync::Arc;+use std::sync::atomic::{AtomicBool, Ordering}; use egui::{Color32, CornerRadius, Galley, Rect, Response, RichText, Sense, Stroke, Ui, vec2}; @@ -20,10 +21,37 @@ pub fn tone_color(t: &Tokens, tone: Tone) -> Color32 { } } +/// Whether the Georgian face in use draws Mtavruli (U+1C90–U+1CBF), the capitals Unicode maps+/// Georgian letters to. Set when the face loads; until then, and without one, Georgian stays as+/// written, since a face without them (DejaVu Sans, older Sylfaen) would draw boxes.+static GEORGIAN_CAPS: AtomicBool = AtomicBool::new(false);++#[cfg_attr(target_arch = "wasm32", allow(dead_code))]+pub(crate) fn set_georgian_caps(on: bool) {+ GEORGIAN_CAPS.store(on, Ordering::Relaxed);+}++/// `text` in upper case. Georgian goes to Mtavruli only when the face draws it.+fn upper(text: &str) -> String {+ upper_with(text, GEORGIAN_CAPS.load(Ordering::Relaxed))+}++/// [`upper`] with the face's Georgian capitals given rather than read from [`GEORGIAN_CAPS`].+fn upper_with(text: &str, georgian_caps: bool) -> String {+ text.chars().fold(String::with_capacity(text.len()), |mut out, c| {+ if !georgian_caps && ('\u{10d0}'..='\u{10ff}').contains(&c) {+ out.push(c);+ } else {+ out.extend(c.to_uppercase());+ }+ out+ })+}+ /// `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))+ ui.painter().layout_job(egui::text::LayoutJob::single_section(upper(text), format)) } /// A `.hud-label`: 11 px Geist Mono, upper case, widely spaced.@@ -60,7 +88,7 @@ pub fn hud_elided(ui: &mut Ui, text: &str, color: Color32, max_width: f32) -> Re extra_letter_spacing: theme::HUD_SPACING, ..Default::default() };- let mut job = egui::text::LayoutJob::single_section(text.to_uppercase(), format);+ let mut job = egui::text::LayoutJob::single_section(upper(text), format); 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;@@ -764,4 +792,12 @@ mod tests { } } }++ #[test]+ fn upper_keeps_georgian_unless_the_face_has_capitals() {+ // The capability is passed in: `GEORGIAN_CAPS` is shared with tests that load a face.+ assert_eq!(super::upper_with("Update all (2)", false), "UPDATE ALL (2)");+ assert_eq!(super::upper_with("ყველას განახლება (2) · ArtCraft", false), "ყველას განახლება (2) · ARTCRAFT");+ assert_eq!(super::upper_with("ყველას განახლება · ArtCraft", true), "ᲧᲕᲔᲚᲐᲡ ᲒᲐᲜᲐᲮᲚᲔᲑᲐ · ARTCRAFT");+ } }new file mode 100644@@ -0,0 +1,204 @@+//! The Georgian UI, in its own test binary: the UI language is process-wide, so it can't share one+//! with the English UI tests. Georgian is drawn with the system's Georgian font; where there is+//! none (minimal Linux installs), the UI stays in English and says why.+//! `ARTCRAFT_LAUNCHER_SNAPSHOTS=<dir>` also saves screenshots (run with `-- --test-threads=1`).++use std::cell::{Cell, RefCell};+use std::rc::Rc;++use artcraft_launcher_core::protocol::{Backend, Capabilities, Command, Event, JobPhase, LatestRelease, Origin, PausedDownload, Source};+use artcraft_launcher_core::release::{Asset, Release};+use artcraft_launcher_core::status::InstallSource;+use artcraft_launcher_core::{Arch, Installed, Os, Platform, SUITE, version};+use artcraft_launcher_ui_egui::settings::ViewMode;+use artcraft_launcher_ui_egui::{Host, LauncherUi, Settings};+use egui_kittest::Harness;+use egui_kittest::kittest::Queryable;++const NOW: i64 = 1_791_500_000;++struct Fake {+ events: Vec<Event>,+ sent: Rc<RefCell<Vec<Command>>>,+ manage: bool,+ clock: Rc<Cell<i64>>,+}++impl Backend for Fake {+ fn send(&mut self, command: Command) {+ self.sent.borrow_mut().push(command);+ }+ fn poll(&mut self) -> Vec<Event> {+ std::mem::take(&mut self.events)+ }+ fn capabilities(&self) -> Capabilities {+ Capabilities { manage_apps: self.manage, reveal_label: "Show in Finder", self_update: self.manage }+ }+ fn platform(&self) -> Option<Platform> {+ Some(Platform { os: Os::MacOs, arch: Arch::Arm64 })+ }+ fn now(&self) -> i64 {+ self.clock.get()+ }+}++struct NoHost;+impl Host for NoHost {}++fn release(id: &str, v: &str, at: i64) -> Release {+ let name = format!("{id}-{v}-macos-universal.dmg");+ Release {+ tag: format!("v{v}"),+ version: version::parse(v),+ name: format!("v{v}"),+ published_at: Some(at),+ html_url: format!("https://github.com/storytold/{id}/releases/tag/v{v}"),+ notes: "## What's Changed\n* Faster things by @a in https://github.com/storytold/x/pull/1".into(),+ prerelease: false,+ assets: vec![Asset {+ name: name.clone(),+ size: 150 << 20,+ url: format!("https://github.com/storytold/{id}/releases/download/v{v}/{name}"),+ sha256: None,+ }],+ }+}++/// A realistic mix: updates, up-to-date apps, a download in progress, one app with no release.+fn events() -> Vec<Event> {+ let mut ev = Vec::new();+ let versions = [+ ("photocraft", "0.5.0"),+ ("vectorcraft", "0.7.0"),+ ("filmcraft", "0.4.0"),+ ("lightcraft", "0.4.0"),+ ("pdfcraft", "0.4.0"),+ ("effectcraft", "0.6.0"),+ ("designcraft", "0.4.0"),+ ("wordcraft", "0.3.0"),+ ("deckcraft", "0.3.0"),+ ("gridcraft", "0.3.0"),+ ("cadcraft", "0.3.0"),+ ];+ for (i, (id, v)) in versions.iter().enumerate() {+ ev.push(Event::Latest {+ app: (*id).into(),+ latest: LatestRelease::Found(release(id, v, NOW - 86_400 - i as i64 * 600)),+ from: Origin::ArtCraftApi,+ });+ }+ ev.push(Event::Latest { app: "soundcraft".into(), latest: LatestRelease::None, from: Origin::ArtCraftApi });+ let installed = [("photocraft", "0.4.1"), ("lightcraft", "0.4.0"), ("wordcraft", "0.3.0"), ("effectcraft", "0.5.0")];+ for app in &SUITE {+ let inst = installed.iter().find(|(id, _)| *id == app.id).map(|(_, v)| Installed {+ version: version::parse(v),+ source: InstallSource::MacApp,+ location: format!("/Applications/{}.app", app.name),+ package: None,+ });+ ev.push(Event::Detected { app: app.id.into(), installed: inst });+ }+ ev.push(Event::Progress { app: "filmcraft".into(), phase: JobPhase::Downloading { done: 48 << 20, total: 131 << 20 } });+ ev.push(Event::Paused { app: "deckcraft".into(), download: Some(PausedDownload { done: 30 << 20, total: 120 << 20 }) });+ ev.push(Event::Progress { app: "gridcraft".into(), phase: JobPhase::Reconnecting { done: 10 << 20, total: 90 << 20 } });+ ev.push(Event::CheckFinished { at: NOW - 180, source: Source::GitHubApi, rate_limited_until: None });+ ev+}++fn harness_language(+ view: ViewMode,+ theme: egui::Theme,+ manage: bool,+ language: &str,+) -> (Harness<'static, Option<LauncherUi>>, Rc<RefCell<Vec<Command>>>) {+ artcraft_launcher_ui_egui::i18n::set_test_lang(artcraft_launcher_ui_egui::i18n::Lang::EN);+ let sent = Rc::new(RefCell::new(Vec::new()));+ let sent2 = sent.clone();+ let mut backend = Some(Fake { events: events(), sent: sent2, manage, clock: Rc::new(Cell::new(NOW)) });+ let settings = Settings { view, notify: false, language: language.into(), ..Settings::default() };+ let harness = Harness::builder().with_size(egui::vec2(1180.0, 900.0)).with_theme(theme).build_ui_state(+ move |ui, state: &mut Option<LauncherUi>| {+ if state.is_none()+ && let Some(b) = backend.take()+ {+ let mut s = settings.clone();+ s.theme = if theme == egui::Theme::Dark {+ artcraft_launcher_ui_egui::theme::ThemeChoice::Dark+ } else {+ artcraft_launcher_ui_egui::theme::ThemeChoice::Light+ };+ *state = Some(LauncherUi::new(ui.ctx(), Box::new(b), s));+ }+ if let Some(l) = state.as_mut() {+ l.logic(&ui.ctx().clone(), &mut NoHost);+ l.ui(ui, &mut NoHost);+ }+ },+ None,+ );+ (harness, sent)+}++fn snapshot(h: &mut Harness<'_, Option<LauncherUi>>, name: &str) {+ let Some(dir) = std::env::var_os("ARTCRAFT_LAUNCHER_SNAPSHOTS") else { return };+ let img = h.render().unwrap();+ img.save(std::path::Path::new(&dir).join(format!("{name}.png"))).unwrap();+}++// One test: the UI language is process-wide.+#[test]+fn georgian_switches_persists_and_keeps_install_actions() {+ use artcraft_launcher_ui_egui::i18n::Lang;+ let (mut h, sent) = harness_language(ViewMode::Grid, egui::Theme::Dark, true, "en");+ h.run_steps(6);+ h.get_by_label("Settings").click();+ h.run_steps(3);+ h.get_by_value("English").click();+ h.run_steps(2);+ // Opening Settings doesn't load the Georgian font: the picker names it in English.+ assert!(!artcraft_launcher_ui_egui::theme::cjk_font_loaded(Lang::KA));+ // The list is long enough to scroll, so the option may be out of view: click it through+ // accesskit, which reaches widgets that aren't visible.+ h.get_by_label("Georgian").click_accesskit();+ h.run_steps(3);+ if !artcraft_launcher_ui_egui::theme::cjk_font_available(Lang::KA) {+ // No font for Georgian: choosing it keeps the UI in English, and Settings says why.+ assert_eq!(h.state().as_ref().unwrap().settings.language, "ka");+ h.get_by_label_contains("No font for this language was found");+ h.get_by_label("Notify me when an installed app has an update");+ snapshot(&mut h, "settings-ka-no-font");+ return;+ }+ // Choosing it loaded the font: now the picker shows both names.+ h.get_by_value("Georgian (ქართული)");+ h.get_by_label("შეტყობინება, როცა დაინსტალირებულ აპლიკაციას განახლება აქვს");+ snapshot(&mut h, "settings-ka-dark");+ let settings = h.state().as_ref().unwrap().settings.clone();+ assert_eq!(settings.language, "ka");+ let saved = serde_json::to_string(&settings).unwrap();+ let restored: Settings = serde_json::from_str(&saved).unwrap();+ assert_eq!(restored.language, "ka");+ // The font switch re-lays out the sheet for a frame or two: click Done through accesskit, which+ // doesn't depend on where the button was last drawn.+ h.get_by_label("მზადაა").click_accesskit();+ h.run_steps(3);+ h.get_by_label("გამოსახულებების რედაქტირება");+ h.get_by_label_contains("ბოლო შემოწმება: 3 წუთის წინ · წყარო: ArtCraft API");+ h.get_by_label("განახლება ვერსიამდე 0.5.0");+ // Product names stay in English.+ assert!(h.query_by_label("Launcher").is_none());+ snapshot(&mut h, "grid-ka-dark");+ h.get_by_label("ყველას განახლება (2)").click();+ h.run_steps(2);+ let installs: Vec<_> = sent.borrow().iter().filter_map(|c| if let Command::Install { app } = c { Some(app.clone()) } else { None }).collect();+ assert_eq!(installs, ["photocraft", "effectcraft"]);++ let (mut table, _) = harness_language(ViewMode::Table, egui::Theme::Light, true, &restored.language);+ table.run_steps(6);+ table.get_by_label("ვერსია");+ assert!(table.query_all_by_label_contains("ოქტ, 2026").count() >= 1);+ snapshot(&mut table, "table-ka-light");+ let (mut web, _) = harness_language(ViewMode::Grid, egui::Theme::Dark, false, &restored.language);+ web.run_steps(6);+ assert!(web.query_all_by_label("ჩამოტვირთვა macOS-ისთვის").count() >= 3);+}