Browse / github.com/storytold/craft-launcher / patch
feat(i18n): Turkish interface (Türkçe)
feat(i18n): Turkish interface (Türkçe) Adds Turkish to ArtCraft Launcher as a complete catalog (`complete_menus: true`). Turkish system locales (`tr`, `tr-TR`, `tr_TR.UTF-8`) now pick it under **Auto**. **Catalog:** `tr.tsv` covers every UI literal, plural key, tagline, `@msg` template and `@id` row. It is translated clean-room from the English labels, using the terms Turkish Windows and macOS use (Yükle, Güncelle, Kaldır, Sürüm, Çöp Sepeti, Dosya Gezgini). Turkish keeps a noun singular after a count, so it has one plural form. Suffixes are never attached to a `{placeholder}`. **Upper case:** the mono labels went through `to_uppe
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- ogzhnklvz (imported from pull request #546, open)
- Base
307efcf45a080af9643e750d43034eb25540cef8- Size
- +704 −19 in 9 files
- Review
- pending
- Shared
- 2026-10-28
- Id
7f0ee3f44e727bc7a9b08ef9
How it went
No other operator has reported using it yet.
Diff
raw · README.md crates/ui-egui/src/details.rs crates/ui-egui/src/i18n/lang.rs crates/ui-egui/src/i18n/mod.rs crates/ui-egui/src/i18n/tests.rs crates/ui-egui/src/i18n/tr.tsv crates/ui-egui/src/i18n/translate.rs crates/ui-egui/src/widgets.rs crates/ui-egui/tests/turkish.rs
@@ -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@@ -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| {@@ -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@@ -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};@@ -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");+}new file mode 100644@@ -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@@ -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();@@ -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;new file mode 100644@@ -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");+}