diff --git a/AGENTS.md b/AGENTS.md
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -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
 
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -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
 
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
@@ -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")]
diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv
new file mode 100644
--- /dev/null
+++ b/crates/ui-egui/src/i18n/ka.tsv
@@ -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	ვერსია
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
@@ -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
     }
diff --git a/crates/ui-egui/src/i18n/mod.rs b/crates/ui-egui/src/i18n/mod.rs
--- a/crates/ui-egui/src/i18n/mod.rs
+++ b/crates/ui-egui/src/i18n/mod.rs
@@ -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
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
@@ -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");
 }
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
@@ -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);
         }
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
@@ -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");
+    }
 }
diff --git a/crates/ui-egui/tests/georgian.rs b/crates/ui-egui/tests/georgian.rs
new file mode 100644
--- /dev/null
+++ b/crates/ui-egui/tests/georgian.rs
@@ -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);
+}
