diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -86,16 +86,17 @@ The web version shows the same catalog in the browser, with the latest download
 
 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.
+(Русский), Simplified Chinese (简体中文), Spanish (Español) or Turkish (Türkçe). **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, Turkish ones (`tr`, `tr-TR`, `tr_TR.UTF-8`) Turkish, 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
diff --git a/crates/ui-egui/src/details.rs b/crates/ui-egui/src/details.rs
--- a/crates/ui-egui/src/details.rs
+++ b/crates/ui-egui/src/details.rs
@@ -306,7 +306,7 @@ impl LauncherUi {
             ui.painter().rect_stroke(pic, CornerRadius::ZERO, Stroke::new(1.0, theme::backdrop_ink(40)), egui::StrokeKind::Outside);
             let caption = if count > 1 { format!("{} · {}/{count}", shot.caption, at + 1) } else { shot.caption.to_string() };
             let ink = theme::backdrop_ink(190);
-            let galley = widgets::hud_galley(ui, &caption, ink);
+            let galley = widgets::hud_galley_english(ui, &caption, ink);
             let caption_at = pos2(rect.center().x - galley.size().x / 2.0, pic.bottom() + 14.0);
             ui.painter().galley(caption_at, galley, ink);
 
@@ -359,7 +359,7 @@ impl LauncherUi {
         ui.painter().rect_stroke(pic, CornerRadius::ZERO, t.hairline(), egui::StrokeKind::Inside);
         let count = details.shots.len();
         if count < 2 {
-            widgets::hud(ui, current.caption, t.faint);
+            widgets::hud_english(ui, current.caption, t.faint);
             return;
         }
         // Previous and next, over the sides of the picture.
@@ -372,7 +372,7 @@ impl LauncherUi {
         if arrow_button(ui, next, Id::new("shot-next"), false, tl!("Next picture (→)"), t) {
             self.flip_shot(count, true);
         }
-        widgets::hud(ui, &format!("{} · {}/{count}", current.caption, self.detail_shot.min(count - 1) + 1), t.faint);
+        widgets::hud_english(ui, &format!("{} · {}/{count}", current.caption, self.detail_shot.min(count - 1) + 1), t.faint);
         let gap = 8.0;
         let thumb_w = ((w - gap * (count as f32 - 1.0)) / count as f32).floor().max(1.0);
         ui.horizontal(|ui| {
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
@@ -22,7 +22,8 @@ impl Script {
 
 /// One supported UI language.
 pub struct LangInfo {
-    /// Canonical BCP 47 code (`en`, `ru`, `zh-CN`, `de`, `fr`, `pt-BR`, `es`, `cs`, `pl`, `hu`, `ja`).
+    /// Canonical BCP 47 code (`en`, `ru`, `zh-CN`, `de`, `fr`, `pt-BR`, `es`, `cs`, `pl`, `hu`, `ja`,
+    /// `tr`).
     /// Also the `interfaceLanguage` value.
     pub code: &'static str,
     /// The language's name in itself, shown in the Preferences dropdown.
@@ -36,7 +37,7 @@ pub struct LangInfo {
     /// Plural form index for a count (English, German, Spanish and Hungarian: 0 = one, 1 = other;
     /// Russian: 0 = one, 1 = few, 2 = many; Czech: 0 = one, 1 = few, 2 = other; Polish: 0 = one only
     /// for 1, 1 = few, 2 = many; French: 0 = one for 0 and 1; a language whose nouns don't inflect
-    /// for number: always 0).
+    /// for number, or that keeps them singular after a count (Japanese, Chinese, Turkish): always 0).
     pub plural: fn(u64) -> usize,
     /// Number of plural forms the catalog's `@plural` rows carry (checked by tests).
     pub plural_forms: usize,
@@ -94,7 +95,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 +217,17 @@ pub static LANGUAGES: [LangInfo; 11] = [
         complete_menus: true,
         catalog: OnceLock::new(),
     },
+    LangInfo {
+        code: "tr",
+        name: "Türkçe",
+        english_name: "Turkish",
+        script: None,
+        source: include_str!("tr.tsv"),
+        plural: plural_none,
+        plural_forms: 1,
+        complete_menus: true,
+        catalog: OnceLock::new(),
+    },
 ];
 
 impl LangInfo {
@@ -254,6 +266,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 TR: Lang = Lang(&LANGUAGES[11]);
 
     pub fn code(self) -> &'static str {
         self.0.code
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
@@ -36,4 +36,4 @@ mod tests;
 
 pub use detect::{detect_system_lang_in_background, set_system_locales, set_test_lang, system_lang};
 pub use lang::{LANGUAGES, Lang, LangInfo, Script, lang_from_tag};
-pub use translate::{current, fmt, has, message, msg, set_current, t, t_id, tn, tr, tr_ctx, tr_id, trn};
+pub use translate::{current, fmt, has, message, msg, set_current, t, t_id, tn, tr, tr_ctx, tr_id, trn, upper};
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
@@ -523,3 +523,46 @@ fn complete_catalogs_translate_package_formats() {
         }
     }
 }
+
+#[test]
+fn turkish_locales_resolve() {
+    for tag in ["tr", "tr-TR", "tr_TR.UTF-8", "TR_tr"] {
+        assert_eq!(lang_from_tag(tag), Some(Lang::TR), "{tag}");
+    }
+    assert_eq!(super::detect::first_supported(["xx-XX", "tr-TR", "en-US"]), Some(Lang::TR));
+}
+
+#[test]
+fn turkish_counts_dates_and_messages_work() {
+    // After a number the noun stays singular, for 1 as for every other count.
+    assert_eq!(trn(Lang::TR, 1, "{n} app", "{n} apps"), "1 uygulama");
+    assert_eq!(trn(Lang::TR, 0, "{n} app", "{n} apps"), "0 uygulama");
+    assert_eq!(trn(Lang::TR, 21, "{n} day ago", "{n} days ago"), "21 gün önce");
+    assert_eq!(super::tr_ctx(Lang::TR, "month", "Aug"), "Ağu");
+    assert_eq!(tr(Lang::TR, "{month} {day}, {year}"), "{day} {month} {year}");
+    assert_eq!(message(Lang::TR, "PhotoCraft updated to 0.5.0"), "PhotoCraft, 0.5.0 sürümüne güncellendi");
+    assert_eq!(message(Lang::TR, "LightCraft was removed"), "LightCraft kaldırıldı");
+}
+
+#[test]
+fn turkish_upper_case_keeps_the_dotted_i() {
+    use super::translate::upper_turkish;
+    assert_eq!(upper_turkish("Bilgi"), "BİLGİ");
+    assert_eq!(upper_turkish("İndir"), "İNDİR");
+    assert_eq!(upper_turkish("Kaldırıldı"), "KALDIRILDI");
+    assert_eq!(upper_turkish("Uygulama ayrıntıları"), "UYGULAMA AYRINTILARI");
+    // Names written in English keep English upper case, suffixes after them follow Turkish.
+    assert_eq!(upper_turkish("LightCraft benzeri"), "LIGHTCRAFT BENZERİ");
+    assert_eq!(upper_turkish("GitHub'da aç"), "GITHUB'DA AÇ");
+    assert_eq!(upper_turkish("Craft App Suite"), "CRAFT APP SUITE");
+    assert_eq!(upper_turkish("Windows için indir"), "WINDOWS İÇİN İNDİR");
+    assert_eq!(upper_turkish("Discord'a katıl"), "DISCORD'A KATIL");
+}
+
+#[test]
+fn upper_follows_the_language() {
+    use super::translate::upper_in;
+    assert_eq!(upper_in(Lang::TR, "Bilgi"), "BİLGİ");
+    assert_eq!(upper_in(Lang::EN, "Bilgi"), "BILGI");
+    assert_eq!(upper_in(Lang::DE, "Größe"), "GRÖSSE");
+}
diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv
new file mode 100644
--- /dev/null
+++ b/crates/ui-egui/src/i18n/tr.tsv
@@ -0,0 +1,366 @@
+# Turkish (tr) 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: Turkish has one (a noun stays singular after a number: "3 uygulama").
+# @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 standard Turkish,
+# using the terms Turkish users of Windows, macOS and creative software know (Yükle, Güncelle,
+# Kaldır, Sürüm, Ayarlar). Sentences address the user with the polite "siz", as Windows and macOS
+# do in Turkish; buttons and menu items use the plain imperative.
+# Suffixes are never attached to a {placeholder} (Turkish suffixes follow the vowels of the word
+# before them, which a placeholder hides): "{v} sürümüne", "{os} için", "Yayımlanma: {date}".
+#
+# 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".
+# Trailing "…" (U+2026) must be preserved. Placeholders {name}, {n}, {version}, {count}, {v},
+# {what}, {unit}, {real}, {e} must appear in translation.
+
+	Launcher	Launcher
+	Craft App Suite	Craft App Suite
+	Open Source Tools for Creatives, Makers, and Builders	Yaratıcılar, üreticiler ve yapımcılar için açık kaynak araçlar
+	Settings	Ayarlar
+	Check for updates	Güncellemeleri denetle
+	Grid	Izgara
+	Table	Tablo
+	Show apps as cards	Uygulamaları kart olarak göster
+	Show apps as a table	Uygulamaları tablo olarak göster
+	All	Tümü
+	Updates	Güncellemeler
+	Not installed	Yüklü değil
+	Search apps	Uygulama ara
+	Sort	Sırala
+	Suite order	Paket sırası
+	Name	Ad
+	Inspired by	Esin kaynağı
+	Status	Durum
+	Installed version	Yüklü sürüm
+	Latest version	En son sürüm
+	Newest release	En yeni sürüm
+	Discord	Discord
+	GitHub	GitHub
+	Website	Web sitesi
+	Classic apps	Klasik uygulamalar
+	New apps	Yeni uygulamalar
+	NEW	YENİ
+	Install	Yükle
+	Update to {v}	Güncelle: {v}
+	Open	Aç
+	Download for {os}	{os} için indir
+	Download	İndir
+	Coming soon	Yakında
+	Checking…	Denetleniyor…
+	Not available	Kullanılamıyor
+	Offline	Çevrimdışı
+	Downloading {done} of {total}	İndiriliyor: {done} / {total}
+	Downloading…	İndiriliyor…
+	Reconnecting…	Yeniden bağlanılıyor…
+	Verifying…	Doğrulanıyor…
+	Installing, then restarting…	Yükleniyor, ardından yeniden başlatılacak…
+	Installing…	Yükleniyor…
+	Removing…	Kaldırılıyor…
+	Pause download	İndirmeyi duraklat
+	Cancel download	İndirmeyi iptal et
+	Resume download	İndirmeyi sürdür
+	Discard download	İndirileni sil
+	Release notes	Sürüm notları
+	Show in Finder	Finder'da göster
+	Show in Explorer	Dosya Gezgini'nde göster
+	Show in Files	Dosyalar'da göster
+	Reinstall	Yeniden yükle
+	Uninstall…	Kaldır…
+	All releases	Tüm sürümler
+	ArtCraft Launcher {version} is available.	ArtCraft Launcher {version} kullanıma sunuldu.
+	Dismiss	Yoksay
+	Update and restart	Güncelle ve yeniden başlat
+	A new version is available. Would you like to relaunch with the updated version?	Yeni bir sürüm var. Güncel sürümle yeniden başlatmak ister misiniz?
+	Later	Sonra
+	What's new	Yenilikler
+	No notes were published with this release.	Bu sürümle birlikte not yayımlanmadı.
+	Released {date}	Yayımlanma: {date}
+	Open on GitHub	GitHub'da aç
+	Close	Kapat
+	ArtCraft Launcher {version}	ArtCraft Launcher {version}
+	Only when I ask	Yalnızca ben istediğimde
+	Automatically download and install updates	Güncellemeleri otomatik olarak indir ve yükle
+	Notify me when an installed app has an update	Yüklü bir uygulamanın güncellemesi çıktığında bildir
+	Include pre-releases (betas and release candidates)	Ön sürümleri de dahil et (beta ve sürüm adayları)
+	Installing on Linux	Linux'ta yükleme
+	Installing on Windows	Windows'ta yükleme
+	Install new apps as	Yeni uygulamaları şu biçimde yükle
+	System package (recommended)	Sistem paketi (önerilen)
+	Updates always use the format an app was installed with.	Güncellemeler her zaman uygulamanın yüklendiği biçimi kullanır.
+	Running in the background	Arka planda çalışma
+	Keep running in the tray when the window is closed	Pencere kapatıldığında sistem tepsisinde çalışmaya devam et
+	Show the window on the current desktop	Pencereyi geçerli masaüstünde göster
+	Start ArtCraft Launcher when I log in	Oturum açtığımda ArtCraft Launcher'ı başlat
+	Community	Topluluk
+	Questions, ideas and news from the ArtCraft team	Sorular, fikirler ve ArtCraft ekibinden haberler
+	Appearance	Görünüm
+	Quit ArtCraft Launcher	ArtCraft Launcher'dan çık
+	Done	Bitti
+	Uninstall {name}?	{name} kaldırılsın mı?
+	The app will be moved to the Trash. Your documents and settings stay.	Uygulama Çöp Sepeti'ne taşınacak. Belgeleriniz ve ayarlarınız korunur.
+	Windows will remove it like any installed program. Your documents and settings stay.	Windows uygulamayı diğer yüklü programlar gibi kaldıracak. Belgeleriniz ve ayarlarınız korunur.
+	Your system's package manager will remove it, and may ask for your password. Your documents and settings stay.	Uygulamayı sisteminizin paket yöneticisi kaldıracak ve parolanızı sorabilir. Belgeleriniz ve ayarlarınız korunur.
+	The app will be removed. Your documents and settings stay.	Uygulama kaldırılacak. Belgeleriniz ve ayarlarınız korunur.
+	Uninstall	Kaldır
+	Cancel	İptal
+	Checked {ago}	Son denetim: {ago}
+	just now	az önce
+	yesterday	dün
+	{month} {day}, {year}	{day} {month} {year}
+
+	Automatically	Otomatik olarak
+	Every Craft app	Tüm Craft uygulamaları
+	Apps on this computer	Bu bilgisayardaki uygulamalar
+	Installed apps with a newer version	Yeni sürümü çıkmış yüklü uygulamalar
+	Apps you can install	Yükleyebileceğiniz uygulamalar
+	Hide App	Uygulamayı Gizle
+	Unhide App	Uygulamayı Göster
+	Hidden Apps	Gizli Uygulamalar
+	Apps you hid	Gizlediğiniz uygulamalar
+	Checked {ago} · using {sources}	Son denetim: {ago} · kaynak: {sources}
+	{a} and {b}	{a} ve {b}
+	Join the Discord	Discord'a katıl
+	Learn more	Daha fazla bilgi
+	Minimize	Simge durumuna küçült
+	Maximize	Ekranı kapla
+	Restore	Geri yükle
+	New	Yeni
+	OK	Tamam
+	Open link	Bağlantıyı aç
+	Update	Güncelle
+	{name} icon	{name} simgesi
+	Loading ArtCraft Launcher…	ArtCraft Launcher yükleniyor…
+	Cancel update	Güncellemeyi iptal et
+	Stop waiting; the update can be started again later	Beklemeyi bırak; güncelleme daha sonra yeniden başlatılabilir
+	Download again	Yeniden indir
+	Install cancelled	Yükleme iptal edildi
+	Install cancelled. The download is kept for the next try.	Yükleme iptal edildi. İndirilen dosya bir sonraki deneme için saklanıyor.
+	Update cancelled	Güncelleme iptal edildi
+	Uninstall cancelled	Kaldırma iptal edildi
+	Retry install	Yüklemeyi yeniden dene
+	Retry update to {version}	{version} sürümüne güncellemeyi yeniden dene
+	The download is still here. It's checked again, then installed.	İndirilen dosya hâlâ duruyor. Yeniden denetlenip yüklenecek.
+	Waiting for another app…	Başka bir uygulama bekleniyor…
+	Waiting for other installs…	Diğer yüklemeler bekleniyor…
+
+# Package formats offered for download (PackageKind::label)
+	Installer (.msi)	Yükleyici (.msi)
+	Portable (.zip)	Taşınabilir (.zip)
+	Disk image (.dmg)	Disk görüntüsü (.dmg)
+	Debian package (.deb)	Debian paketi (.deb)
+	RPM package (.rpm)	RPM paketi (.rpm)
+	Flatpak bundle	Flatpak paketi
+	Tarball (.tar.gz)	Tar arşivi (.tar.gz)
+
+# Contextual: each app's tagline, keyed by app id
+photocraft	Image editing	Görüntü düzenleme
+vectorcraft	Vector illustration	Vektör çizim
+filmcraft	Video editing, color and sound	Video kurgu, renk ve ses
+lightcraft	Photo library and raw development	Fotoğraf arşivi ve RAW işleme
+pdfcraft	Reading, organizing and protecting PDFs	PDF okuma, düzenleme ve koruma
+effectcraft	Motion graphics and visual effects	Hareketli grafik ve görsel efektler
+designcraft	Page layout and publishing	Sayfa düzeni ve yayıncılık
+wordcraft	Writing and document design	Yazı ve belge tasarımı
+deckcraft	Presentations and slide shows	Sunumlar ve slayt gösterileri
+gridcraft	Spreadsheets and data	Elektronik tablolar ve veri
+cadcraft	Computer-aided design and drafting	Bilgisayar destekli tasarım ve teknik çizim
+soundcraft	Recording, editing and mixing audio	Ses kaydı, düzenleme ve miksaj
+figcraft	Interface design and prototyping	Arayüz tasarımı ve prototipleme
+
+# Contextual: month abbreviations for date rendering
+month	Jan	Oca
+month	Feb	Şub
+month	Mar	Mar
+month	Apr	Nis
+month	May	May
+month	Jun	Haz
+month	Jul	Tem
+month	Aug	Ağu
+month	Sep	Eyl
+month	Oct	Eki
+month	Nov	Kas
+month	Dec	Ara
+	Not checked yet	Henüz denetlenmedi
+	GitHub's hourly limit was reached; full details return in {min} min	GitHub'ın saatlik sınırına ulaşıldı; ayrıntıların tamamı {min} dk sonra geri gelecek
+	GitHub limit hit; back in {min} min	GitHub sınırına ulaşıldı; {min} dk sonra yeniden denenecek
+	Installing for {platform}	Yükleme hedefi: {platform}
+	Downloads for {platform}	İndirmeler: {platform}
+	No downloads for this system	Bu sistem için indirme yok
+	Join the ArtCraft community on Discord	Discord'daki ArtCraft topluluğuna katıl
+	ArtCraft on {name}	{name} üzerinde ArtCraft
+	Couldn't open {name}: {e}	{name} açılamadı: {e}
+	Download cancelled	İndirme iptal edildi
+	Available to install	Yüklenebilir
+	Available	Mevcut
+	Installed · up to date	Yüklü · güncel
+	Update available	Güncelleme var
+	Installed	Yüklü
+	installed	yüklü
+	Not available here	Bu sistemde yok
+	Updating…	Güncelleniyor…
+	Update paused	Güncelleme duraklatıldı
+	Download paused	İndirme duraklatıldı
+	{version} installed → {latest} released	{version} yüklü → {latest} yayımlandı
+	Version {version}	Sürüm {version}
+	{installed} installed · latest {latest}	{installed} yüklü · en son {latest}
+	Installed · latest {latest}	Yüklü · en son {latest}
+	Version {version} installed	Sürüm {version} yüklü
+	Version {latest}	Sürüm {latest}
+	No release published yet	Henüz sürüm yayımlanmadı
+	ArtCraft Launcher	ArtCraft Launcher
+	App	Uygulama
+	Latest	En son
+	Released	Yayımlanma
+	Auto	Otomatik
+	Language	Dil
+	Ascending	Artan
+	Descending	Azalan
+	Click to reverse the order	Sırayı tersine çevirmek için tıklayın
+	Click to sort by this column	Bu sütuna göre sıralamak için tıklayın
+	Like {name}	{name} benzeri
+	{done} of {total}	{done} / {total}
+	Downloading	İndiriliyor
+	Verifying checksum…	Sağlama toplamı doğrulanıyor…
+	Resume	Sürdür
+	{done} of {total} downloaded	{done} / {total} indirildi
+	{done} downloaded	{done} indirildi
+	Checking for updates…	Güncellemeler denetleniyor…
+	Downloads the new version, replaces this one and opens it again	Yeni sürümü indirir, bu sürümün yerine koyar ve yeniden açar
+	Couldn't check for updates: {f}	Güncellemeler denetlenemedi: {f}
+	GitHub repository	GitHub deposu
+	ArtCraft	ArtCraft
+	Update 1 app	1 uygulamayı güncelle
+	Update all ({count})	Tümünü güncelle ({count})
+	⊞  Grid	⊞  Izgara
+	☰  Table	☰  Tablo
+	🔍  Search apps	🔍  Uygulama ara
+	{name} {version}	{name} {version}
+	Match system	Sisteme uy
+	Dark	Koyu
+	Light	Açık
+	Full changelog	Tüm değişiklik günlüğü
+	link	bağlantı
+	No apps match	Eşleşen uygulama yok
+	Open ArtCraft Launcher to update.	Güncellemek için ArtCraft Launcher'ı açın.
+	{name} {v} is available	{name} {v} kullanıma sunuldu
+	{count} updates are available	{count} güncelleme var
+	These notes weren't fetched (GitHub's request limit was reached). They're on the release page.	Bu notlar alınamadı (GitHub'ın istek sınırına ulaşıldı). Sürüm sayfasında okuyabilirsiniz.
+	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.	Açıkken, ArtCraft Launcher'ı menü çubuğundan açtığınızda penceresi bulunduğunuz masaüstüne gelir. Kapalıyken macOS sizi pencerenin bulunduğu masaüstüne geçirir.
+	Couldn't change the login item: {e}	Oturum açma öğesi değiştirilemedi: {e}
+
+# Contextual: table column headers
+column	App	Uygulama
+column	Installed	Yüklü
+column	Latest	En son
+column	Released	Yayımlanma
+
+# Contextual: the filter tab names a group (the status pill says "Installed")
+filter	Installed	Yüklü
+	{name} details	{name} ayrıntıları
+
+# Plural forms for Turkish (one form)
+@plural	{n} app|{n} apps	{n} uygulama
+@plural	{n} min ago|{n} min ago	{n} dk önce
+@plural	{n} h ago|{n} h ago	{n} sa önce
+@plural	{n} day ago|{n} days ago	{n} gün önce
+
+# Message templates (@msg): toasts from the backend. {placeholders} match any text.
+@msg	Couldn't open {name}: {e}	{name} açılamadı: {e}
+@msg	Download cancelled	İndirme iptal edildi
+@msg	{name} updated to {version}	{name}, {version} sürümüne güncellendi
+@msg	{name} installed: {version}	{name} yüklendi: {version}
+@msg	{name} updated to {version} (this release publishes no checksum, so only its size was checked)	{name}, {version} sürümüne güncellendi (bu sürüm sağlama toplamı yayımlamıyor, bu yüzden yalnızca boyutu denetlendi)
+@msg	{name} installed: {version} (this release publishes no checksum, so only its size was checked)	{name} yüklendi: {version} (bu sürüm sağlama toplamı yayımlamıyor, bu yüzden yalnızca boyutu denetlendi)
+@msg	{name} was removed	{name} kaldırıldı
+@msg	{name} is running. Quit it, then try again.	{name} çalışıyor. Kapatıp yeniden deneyin.
+
+# Command IDs for menu items (@id)
+# Format: @id<TAB>command_id<TAB>translation
+@id	app.install	Yükle
+@id	app.update	Güncelle: {v}
+@id	app.open	Aç
+@id	app.download	{os} için indir
+@id	app.reveal	Klasörde göster
+@id	app.reinstall	Yeniden yükle
+@id	app.uninstall	Kaldır
+@id	app.notes	Sürüm notları
+@id	app.website	Web sitesi
+@id	app.github	GitHub'da aç
+@id	app.releases	Tüm sürümler
+@id	launcher.update	Güncelle ve yeniden başlat
+@id	launcher.download	İndir
+@id	launcher.notes	Yenilikler
+@id	launcher.dismiss	Yoksay
+@id	settings.open	Ayarlar
+@id	settings.quit	ArtCraft Launcher'dan çık
+@id	view.grid	Izgara
+@id	view.table	Tablo
+@id	filter.all	Tümü
+@id	filter.installed	Yüklü
+@id	filter.updates	Güncellemeler
+@id	filter.notinstalled	Yüklü değil
+@id	sort.suite	Paket sırası
+@id	sort.name	Ad
+@id	sort.inspired	Esin kaynağı
+@id	sort.status	Durum
+@id	sort.installed	Yüklü sürüm
+@id	sort.latest	En son sürüm
+@id	sort.released	En yeni sürüm
+@id	help.discord	Discord
+@id	help.github	GitHub
+@id	help.website	Web sitesi
+@id	update.check	Güncellemeleri denetle
+@id	update.check_auto	Otomatik olarak
+@id	update.notify	Yüklü bir uygulamanın güncellemesi çıktığında bildir
+@id	update.prereleases	Ön sürümleri de dahil et (beta ve sürüm adayları)
+@id	linux.preferred_kind	Yeni uygulamaları şu biçimde yükle
+@id	tray.close_to_tray	Pencere kapatıldığında sistem tepsisinde çalışmaya devam et
+@id	tray.show_on_current_desktop	Pencereyi geçerli masaüstünde göster
+@id	tray.login_item	Oturum açtığımda ArtCraft Launcher'ı başlat
+@id	appearance.theme	Görünüm
+
+# Tray (menu bar) menu and tooltip
+	Show ArtCraft Launcher	ArtCraft Launcher'ı göster
+	Check for Updates	Güncellemeleri Denetle
+	Visit getartcraft.com	getartcraft.com'u ziyaret et
+	ArtCraft on GitHub	GitHub'da ArtCraft
+	Join the ArtCraft Discord	ArtCraft Discord'una katıl
+	App details	Uygulama ayrıntıları
+	Close (Esc)	Kapat (Esc)
+	Features	Özellikler
+	Latest release	En son sürüm
+	Version	Sürüm
+	Platforms	Platformlar
+	Releases	Sürümler
+	Enlarge picture	Resmi büyüt
+	Click to enlarge	Büyütmek için tıklayın
+	Previous picture (←)	Önceki resim (←)
+	Next picture (→)	Sonraki resim (→)
+	Loading pictures…	Resimler yükleniyor…
+	Pictures couldn't be loaded. Reopen this page to try again.	Resimler yüklenemedi. Yeniden denemek için bu sayfayı tekrar açın.
+@plural	ArtCraft Launcher · {n} update available|ArtCraft Launcher · {n} updates available	ArtCraft Launcher · {n} güncelleme var
+	Install folder	Yükleme klasörü
+	Default install folder	Varsayılan yükleme klasörü
+	System default	Sistem varsayılanı
+	Browse…	Gözat…
+	Use default	Varsayılanı kullan
+	New installs use this folder. Updates stay in their current location.	Yeni yüklemeler bu klasörü kullanır. Güncellemeler bulundukları konumda kalır.
+	Custom folders use AppImage on Linux.	Linux'ta özel klasörler AppImage kullanır.
+	The system manages install locations here.	Bu sistemde yükleme konumlarını sistem yönetir.
+@msg	couldn't use install folder {path}: {reason}	yükleme klasörü {path} kullanılamadı: {reason}
+@msg	this release has no AppImage for a custom install folder	bu sürümde özel yükleme klasörü için AppImage yok
+@msg	custom install folders aren't supported on this system	bu sistemde özel yükleme klasörleri desteklenmiyor
+@msg	{path} already exists and wasn't installed by ArtCraft Launcher	{path} zaten var ve ArtCraft Launcher tarafından yüklenmedi
+@msg	couldn't move {path} to Trash ({reason}). Remove it in Finder instead.	{path} Çöp Sepeti'ne taşınamadı ({reason}). Bunun yerine Finder'dan silin.
+	Apps are placed inside this folder.	Uygulamalar bu klasörün içine yerleştirilir.
+	Replace what's there?	Mevcut olan değiştirilsin mi?
+	{path} already exists. Installing {name} will overwrite it.	{path} zaten var. {name} yüklenirse üzerine yazılacak.
+	Replace	Değiştir
diff --git a/crates/ui-egui/src/i18n/translate.rs b/crates/ui-egui/src/i18n/translate.rs
--- a/crates/ui-egui/src/i18n/translate.rs
+++ b/crates/ui-egui/src/i18n/translate.rs
@@ -48,6 +48,54 @@ pub fn t_id<'a>(id: &str, label: &'a str) -> &'a str {
     tr_id(current(), id, label)
 }
 
+/// `s` in upper case for the language the UI is drawn in ([`upper_in`] with [`current`]).
+pub fn upper(s: &str) -> String {
+    upper_in(current(), s)
+}
+
+/// `s` in upper case for `lang`. Rust's `to_uppercase` knows no language, and Turkish has a
+/// dotted and a dotless i (`i` → `İ`, `ı` → `I`), so Turkish gets its own rule.
+pub fn upper_in(lang: Lang, s: &str) -> String {
+    if lang.code() == "tr" { upper_turkish(s) } else { s.to_uppercase() }
+}
+
+/// Words that stay English inside Turkish text and keep the English `i` → `I` (product and platform
+/// names). Mixed-case words (`PhotoCraft`, `GitHub`, `macOS`) are recognised as names
+/// without being listed.
+const ENGLISH_WORDS: &[&str] = &["Craft", "Suite", "Launcher", "Discord", "Linux", "Windows", "Finder"];
+
+/// Turkish upper case, word by word: names written in English (see [`ENGLISH_WORDS`]) keep the
+/// English rule, so "LightCraft benzeri" becomes "LIGHTCRAFT BENZERİ". A suffix after an
+/// apostrophe ("GitHub'da") is its own word and follows Turkish.
+pub(super) fn upper_turkish(s: &str) -> String {
+    let mut out = String::with_capacity(s.len() + 8);
+    let mut rest = s;
+    while let Some(start) = rest.find(char::is_alphanumeric) {
+        let (gap, tail) = rest.split_at(start);
+        out.push_str(gap);
+        let end = tail.find(|c: char| !c.is_alphanumeric()).unwrap_or(tail.len());
+        let (word, after) = tail.split_at(end);
+        if is_english_name(word) {
+            out.push_str(&word.to_uppercase());
+        } else {
+            for c in word.chars() {
+                match c {
+                    'i' => out.push('İ'),
+                    'ı' => out.push('I'),
+                    c => out.extend(c.to_uppercase()),
+                }
+            }
+        }
+        rest = after;
+    }
+    out.push_str(rest);
+    out
+}
+
+fn is_english_name(word: &str) -> bool {
+    ENGLISH_WORDS.contains(&word) || word.chars().skip(1).any(char::is_uppercase)
+}
+
 /// Fill `{name}` placeholders. Unknown placeholders are left as written.
 pub fn fmt(template: &str, args: &[(&str, &str)]) -> String {
     let mut out = template.to_string();
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
@@ -22,8 +22,12 @@ pub fn tone_color(t: &Tokens, tone: Tone) -> Color32 {
 
 /// `text` laid out upper case in `family`, `spacing` points between letters.
 pub fn caps(ui: &Ui, text: &str, size: f32, family: egui::FontFamily, spacing: f32, color: Color32) -> Arc<Galley> {
+    laid_out(ui, crate::i18n::upper(text), size, family, spacing, color)
+}
+
+fn laid_out(ui: &Ui, upper: String, 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, format))
 }
 
 /// A `.hud-label`: 11 px Geist Mono, upper case, widely spaced.
@@ -31,6 +35,21 @@ pub fn hud_galley(ui: &Ui, text: &str, color: Color32) -> Arc<Galley> {
     caps(ui, text, theme::HUD_SIZE, theme::mono(), theme::HUD_SPACING, color)
 }
 
+/// [`hud_galley`] for text that is never translated (the apps' screenshot captions): English
+/// upper case in every UI language, so Turkish doesn't dot its i's.
+pub fn hud_galley_english(ui: &Ui, text: &str, color: Color32) -> Arc<Galley> {
+    laid_out(ui, text.to_uppercase(), theme::HUD_SIZE, theme::mono(), theme::HUD_SPACING, color)
+}
+
+/// [`hud`] for text that is never translated (see [`hud_galley_english`]).
+pub fn hud_english(ui: &mut Ui, text: &str, color: Color32) -> Response {
+    let galley = hud_galley_english(ui, text, color);
+    let (rect, resp) = ui.allocate_exact_size(galley.size(), Sense::hover());
+    paint_left(ui, rect, galley, color);
+    resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Label, true, text));
+    resp
+}
+
 /// Paints `galley` at the left of `rect`, centred vertically.
 fn paint_left(ui: &Ui, rect: Rect, galley: Arc<Galley>, color: Color32) {
     let pos = egui::pos2(rect.left(), rect.center().y - galley.size().y / 2.0);
@@ -60,7 +79,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(crate::i18n::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;
diff --git a/crates/ui-egui/tests/turkish.rs b/crates/ui-egui/tests/turkish.rs
new file mode 100644
--- /dev/null
+++ b/crates/ui-egui/tests/turkish.rs
@@ -0,0 +1,195 @@
+//! The Turkish UI, in its own test binary: the UI language is process-wide, so it can't share one
+//! with the English UI tests. `ARTCRAFT_LAUNCHER_SNAPSHOTS=<dir>` also saves screenshots (run with
+//! `-- --test-threads=1`, see `ui.rs`).
+
+use std::cell::Cell;
+use std::rc::Rc;
+
+use artcraft_launcher_core::protocol::{Backend, Capabilities, Command, Event, JobPhase, LatestRelease, Origin, 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>,
+    clock: Rc<Cell<i64>>,
+    os: Os,
+}
+
+impl Backend for Fake {
+    fn send(&mut self, _command: Command) {}
+    fn poll(&mut self) -> Vec<Event> {
+        std::mem::take(&mut self.events)
+    }
+    fn capabilities(&self) -> Capabilities {
+        Capabilities { manage_apps: true, reveal_label: "Show in Finder", self_update: true }
+    }
+    fn platform(&self) -> Option<Platform> {
+        Some(Platform { os: self.os, arch: Arch::Arm64 })
+    }
+    fn now(&self) -> i64 {
+        self.clock.get()
+    }
+}
+
+struct DesktopHost;
+impl Host for DesktopHost {
+    fn has_tray(&self) -> bool {
+        true
+    }
+    fn login_item(&self) -> Option<bool> {
+        Some(false)
+    }
+}
+
+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: String::new(),
+        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,
+        }],
+    }
+}
+
+fn events() -> Vec<Event> {
+    let mut ev = Vec::new();
+    for (i, app) in SUITE.iter().enumerate() {
+        let latest = LatestRelease::Found(release(app.id, "0.5.0", NOW - 40 * 86_400 - i as i64 * 600));
+        ev.push(Event::Latest { app: app.id.into(), latest, from: Origin::ArtCraftApi });
+        let installed = matches!(app.id, "photocraft" | "lightcraft").then(|| Installed {
+            version: version::parse(if app.id == "photocraft" { "0.4.1" } else { "0.5.0" }),
+            source: InstallSource::MacApp,
+            location: format!("/Applications/{}.app", app.name),
+            package: None,
+        });
+        ev.push(Event::Detected { app: app.id.into(), installed });
+    }
+    ev.push(Event::Progress { app: "filmcraft".into(), phase: JobPhase::Verifying });
+    ev.push(Event::CheckFinished { at: NOW - 180, source: Source::Feed, rate_limited_until: None });
+    // A backend message, translated through its `@msg` template where it's shown.
+    ev.push(Event::JobFinished { app: "lightcraft".into(), result: Ok("LightCraft updated to 0.5.0".into()) });
+    ev
+}
+
+fn harness(view: ViewMode) -> Harness<'static, Option<LauncherUi>> {
+    harness_on(view, Os::MacOs)
+}
+
+fn harness_on(view: ViewMode, os: Os) -> Harness<'static, Option<LauncherUi>> {
+    artcraft_launcher_ui_egui::i18n::set_test_lang(artcraft_launcher_ui_egui::i18n::Lang::EN);
+    let mut backend = Some(Fake { events: events(), clock: Rc::new(Cell::new(NOW)), os });
+    let settings =
+        Settings { view, notify: false, language: "tr".into(), theme: artcraft_launcher_ui_egui::theme::ThemeChoice::Dark, ..Settings::default() };
+    Harness::builder().with_size(egui::vec2(1180.0, 900.0)).with_theme(egui::Theme::Dark).build_ui_state(
+        move |ui, state: &mut Option<LauncherUi>| {
+            if state.is_none()
+                && let Some(b) = backend.take()
+            {
+                *state = Some(LauncherUi::new(ui.ctx(), Box::new(b), settings.clone()));
+            }
+            if let Some(l) = state.as_mut() {
+                l.logic(&ui.ctx().clone(), &mut DesktopHost);
+                l.ui(ui, &mut DesktopHost);
+            }
+        },
+        None,
+    )
+}
+
+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 turkish_ui_translates_labels_dates_and_messages() {
+    let mut h = harness(ViewMode::Grid);
+    h.run_steps(8);
+    snapshot(&mut h, "grid-tr-dark");
+    // Product names stay in English.
+    h.get_by_label("Launcher");
+    h.get_by_label("Craft App Suite");
+    h.get_by_label("Görüntü düzenleme");
+    h.get_by_label("Güncelle: 0.5.0");
+    h.get_by_label_contains("Sağlama toplamı doğrulanıyor…");
+    h.get_by_label_contains("Son denetim: 3 dk önce · kaynak: ArtCraft API");
+    h.get_by_label("LightCraft, 0.5.0 sürümüne güncellendi");
+
+    let mut table = harness(ViewMode::Table);
+    table.run_steps(8);
+    snapshot(&mut table, "table-tr-dark");
+    assert!(table.query_all_by_label("Yüklü").count() >= 1);
+    assert!(table.query_all_by_label_contains("Ağu 2026").count() >= 1);
+    check_settings_layout();
+}
+
+/// Checks the actual modal after resizing it while open, including Linux's extra controls.
+fn check_settings_layout() {
+    let mut h = harness_on(ViewMode::Grid, Os::Linux);
+    h.run_steps(8);
+    h.get_by_label("Ayarlar").click();
+    h.run_steps(8);
+    for (width, height) in [(1180.0, 900.0), (720.0, 500.0), (480.0, 500.0), (1180.0, 900.0)] {
+        h.set_size(egui::vec2(width, height));
+        h.run_steps(8);
+        let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(width, height));
+        let done = h.get_by_label("Bitti").rect();
+        let quit = h.get_by_label("ArtCraft Launcher'dan çık").rect();
+        assert!(screen.contains_rect(done), "{width}x{height}: Done is clipped: {done:?}");
+        assert!(screen.contains_rect(quit), "{width}x{height}: Quit is clipped: {quit:?}");
+        assert!(!done.intersects(quit));
+        let title = h.get_all_by_label("Ayarlar").last().unwrap().rect();
+        assert!(screen.contains_rect(title), "{width}x{height}: title is clipped: {title:?}");
+        h.get_by_label("Sisteme uy").scroll_to_me();
+        h.run_steps(8);
+        // The footer has the same services, and the cards behind the modal have Web sitesi and GitHub
+        // links as one 14px line of text; keep only the settings body's buttons (34px) and icons (24px).
+        let discord = h.get_by_label("Discord'a katıl").rect();
+        let theme = h.get_by_label("Sisteme uy").rect();
+        assert!(screen.contains_rect(discord), "community links must be reachable by scrolling");
+        assert!(theme.bottom() < done.top(), "appearance controls must scroll above the fixed actions");
+        let mut links = vec![discord];
+        for label in ["Web sitesi", "GitHub", "X", "Instagram", "LinkedIn", "YouTube", "TikTok"] {
+            let candidates: Vec<_> = h
+                .query_all_by_label(label)
+                .map(|n| n.rect())
+                .filter(|r| r.top() >= discord.top() - 1.0 && r.bottom() < theme.top() && r.height() > 20.0)
+                .collect();
+            assert_eq!(candidates.len(), 1, "{width}x{height}: settings link {label}: {candidates:?}");
+            links.extend(candidates);
+        }
+        for (i, r) in links.iter().enumerate() {
+            assert!(screen.contains_rect(*r), "{width}x{height}: link is clipped: {r:?}");
+            for other in links.iter().skip(i + 1) {
+                assert!(!r.intersects(*other), "{width}x{height}: links overlap: {r:?}, {other:?}");
+            }
+        }
+        snapshot(&mut h, &format!("settings-tr-{width}x{height}-bottom"));
+        h.get_by_label("Dil").scroll_to_me();
+        h.run_steps(8);
+        let language = h.get_by_label("Dil").rect();
+        assert!(screen.contains_rect(language), "the top of settings must remain reachable");
+        snapshot(&mut h, &format!("settings-tr-{width}x{height}-top"));
+    }
+    h.get_by_label("Bitti").click();
+    h.run_steps(4);
+    assert!(h.query_by_label("Bitti").is_none(), "Done must still close the resized dialog");
+}
