Browse / github.com/storytold/craft-launcher / patch
feat(i18n): add Ukrainian (Українська) localization
feat(i18n): add Ukrainian (Українська) localization Ukrainian system/browser locales currently fall back to English. This adds **Українська** to Settings → Language and automatic detection for `uk`, `uk-UA`, and POSIX locale forms. The 325-entry Ukrainian catalog covers the current interface, app descriptions, tray text, dates, package labels, and status/error messages, including the new installation-folder flow. It uses the existing three-form plural rule and keeps English fallback behavior for unknown strings. Adds coverage for locale resolution, saved preferences, plural boundaries, dates, and Ukrainian glyphs in all bundled UI font famili
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- CrioNIK (imported from pull request #535, open)
- Base
307efcf45a080af9643e750d43034eb25540cef8- Size
- +654 −10 in 5 files
- Review
- pending
- Shared
- 2026-10-28
- Id
3de009742eec6a47a2f50c04
How it went
No other operator has reported using it yet.
Diff
raw · README.md crates/ui-egui/src/i18n/lang.rs crates/ui-egui/src/i18n/tests.rs crates/ui-egui/src/i18n/uk.tsv crates/ui-egui/tests/ukrainian.rs
@@ -86,15 +86,16 @@ 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+(Русский), Simplified Chinese (简体中文), Spanish (Español) or Ukrainian (Українська). **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+Spanish, and Portuguese ones (`pt-BR`, and `pt-PT` too, for now) Brazilian Portuguese.+Ukrainian locales (`uk`, `uk-UA`, `uk_UA.UTF-8`…) select Ukrainian, with one/few/many forms for+integer counts. 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,@@ -22,7 +22,7 @@ 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`, `uk`). /// Also the `interfaceLanguage` value. pub code: &'static str, /// The language's name in itself, shown in the Preferences dropdown.@@ -34,7 +34,7 @@ pub struct LangInfo { /// 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+ /// Russian and Ukrainian: 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,@@ -94,7 +94,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 +216,17 @@ pub static LANGUAGES: [LangInfo; 11] = [ complete_menus: true, catalog: OnceLock::new(), },+ LangInfo {+ code: "uk",+ name: "Українська",+ english_name: "Ukrainian",+ script: None,+ source: include_str!("uk.tsv"),+ plural: plural_east_slavic,+ plural_forms: 3,+ complete_menus: true,+ catalog: OnceLock::new(),+ }, ]; impl LangInfo {@@ -254,6 +265,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 UK: Lang = Lang(&LANGUAGES[11]); pub fn code(self) -> &'static str { self.0.code@@ -129,6 +129,7 @@ fn complete_catalogs_cover_every_ui_literal() { include_str!("../model.rs"), include_str!("../notes.rs"), include_str!("../settings.rs"),+ include_str!("../theme.rs"), include_str!("../views.rs"), include_str!("../widgets.rs"), // The desktop shell's tray menu and tooltip.@@ -250,6 +251,79 @@ fn untranslated_plurals_fall_back_to_english_by_count() { assert_eq!(trn(Lang::EN, 0, "{n} app", "{n} apps"), "0 apps"); } +#[test]+fn ukrainian_locales_and_saved_preference_resolve() {+ for tag in ["uk", "uk-UA", "uk_UA.UTF-8", "UK_ua", "uk-UA@euro"] {+ assert_eq!(lang_from_tag(tag), Some(Lang::UK), "{tag}");+ }+ assert_eq!(super::detect::first_supported(["xx-XX", "uk-UA", "en-US"]), Some(Lang::UK));+ assert_eq!(Lang::from_pref("uk"), Lang::UK);+ assert_eq!(Lang::from_code("UK"), Some(Lang::UK));+ assert!(!Lang::UK.needs_cjk_font());+ let settings = crate::settings::Settings { language: "uk".into(), ..Default::default() };+ let saved = serde_json::to_string(&settings).unwrap();+ let restored: crate::settings::Settings = serde_json::from_str(&saved).unwrap();+ assert_eq!(Lang::from_pref(&restored.language), Lang::UK);+ assert_eq!(crate::settings::Settings::language_label(&restored.language, None), "Українська");+}++#[test]+fn ukrainian_counts_dates_and_fallback_work() {+ for (n, app, day) in [+ (0, "застосунків", "днів"),+ (1, "застосунок", "день"),+ (2, "застосунки", "дні"),+ (4, "застосунки", "дні"),+ (5, "застосунків", "днів"),+ (11, "застосунків", "днів"),+ (12, "застосунків", "днів"),+ (14, "застосунків", "днів"),+ (21, "застосунок", "день"),+ (22, "застосунки", "дні"),+ (25, "застосунків", "днів"),+ (101, "застосунок", "день"),+ (111, "застосунків", "днів"),+ (112, "застосунків", "днів"),+ ] {+ assert_eq!(trn(Lang::UK, n, "{n} app", "{n} apps"), format!("{n} {app}"));+ assert_eq!(trn(Lang::UK, n, "{n} day ago", "{n} days ago"), format!("{n} {day} тому"));+ }+ assert_eq!(tr(Lang::UK, "Settings"), "Налаштування");+ assert_eq!(super::tr_ctx(Lang::UK, "month", "Mar"), "бер.");+ assert_eq!(super::tr_ctx(Lang::UK, "month", "Oct"), "жовт.");+ assert_eq!(super::fmt(tr(Lang::UK, "{month} {day}, {year}"), &[("month", "жовт."), ("day", "16"), ("year", "2026")]), "16 жовт. 2026");+ // Ukrainian's singular at 21 must not leak into an untranslated English plural.+ assert_eq!(trn(Lang::UK, 21, "{n} unknown item", "{n} unknown items"), "21 unknown items");+ assert_eq!(tr(Lang::UK, "Untranslated diagnostic"), "Untranslated diagnostic");+ assert_eq!(message(Lang::UK, "Untranslated diagnostic"), "Untranslated diagnostic");+}++#[test]+fn bundled_fonts_cover_ukrainian_in_every_ui_family() {+ use egui::{FontFamily, FontId};+ let ctx = egui::Context::default();+ crate::theme::install_fonts(&ctx, None);+ let mut output = ctx.run_ui(egui::RawInput::default(), |_| {});+ output.textures_delta.clear();+ // Include the whole alphabet, especially letters absent from Russian and the UI's uppercase labels.+ let mut text = String::from("АБВГҐДЕЄЖЗИІЇЙКЛМНОПРСТУФХЦЧШЩЬЮЯабвгґдеєжзиіїйклмнопрстуфхцчшщьюя’");+ for (_, _, _, translation) in rows(Lang::UK.0) {+ text.extend(translation.chars().filter(|c| ('\u{0400}'..='\u{04ff}').contains(c)));+ }+ text.push_str(&text.to_uppercase());+ 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 Ukrainian glyph in {family:?}"));+ }+}+ #[test] fn locale_tags_resolve() { for tag in ["cs", "cs-CZ", "cs_CZ.UTF-8", "CS_cz"] {new file mode 100644@@ -0,0 +1,362 @@+# Ukrainian (uk) 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|few|many (1, 2–4, 0/5–20; repeated by the last digits).+# @msg = status/error message template (message, msg).+# Other context = disambiguation for tr_ctx.+#+# Clean-room translation from the meaning of the English source text.+# Coverage: the whole interface (complete_menus = true), including the union of catalog keys.+# Terminology: app = застосунок; install = встановити; update = оновлення;+# settings = налаштування; download = завантажити; folder = тека.+# Product names ArtCraft Launcher, Launcher, Craft App Suite, ArtCraft and all app names,+# as well as GitHub, Discord and package format names, remain in English.+# Preserve placeholders, escaped newlines and trailing ellipses (U+2026).++ 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 Нові застосунки+ NEW НОВИНКА+ Install Встановити+ Update to {v} Оновити до {v}+ Open Відкрити+ Download for {os} Завантажити для {os}+ Download Завантажити+ Coming soon Незабаром+ Checking… Перевірка…+ Not available Недоступно+ Offline Немає з’єднання+ Downloading {done} of {total} Завантаження: {done} із {total}+ Downloading… Завантаження…+ Reconnecting… Відновлення з’єднання…+ Verifying… Перевірка…+ Installing, then restarting… Встановлення з подальшим перезапуском…+ Installing… Встановлення…+ Removing… Видалення…+ Pause download Призупинити завантаження+ Cancel download Скасувати завантаження+ Resume download Продовжити завантаження+ Discard download Видалити завантаження+ Release notes Примітки до випуску+ Show in Finder Показати у Finder+ Show in Explorer Показати у Провіднику+ Show in Files Показати у Файлах+ Reinstall Перевстановити+ Uninstall… Видалити…+ All releases Усі випуски+ ArtCraft Launcher {version} is available. Доступний ArtCraft Launcher {version}.+ Dismiss Приховати+ Update and restart Оновити й перезапустити+ A new version is available. Would you like to relaunch with the updated version? Доступна нова версія. Перезапустити застосунок, щоб перейти на оновлену версію?+ 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 Після закриття вікна продовжувати роботу в системному треї+ Show the window on the current desktop Показувати вікно на поточному робочому столі+ Start ArtCraft Launcher when I log in Запускати ArtCraft Launcher під час входу в систему+ Community Спільнота+ Questions, ideas and news from the ArtCraft team Запитання, ідеї та новини від команди ArtCraft+ Appearance Вигляд+ Quit ArtCraft Launcher Вийти з ArtCraft Launcher+ Done Готово+ Uninstall {name}? Видалити {name}?+ The app will be moved to the Trash. Your documents and settings stay. Застосунок буде переміщено в Смітник. Ваші документи й налаштування залишаться.+ Windows will remove it like any installed program. Your documents and settings stay. Windows видалить його, як будь-яку встановлену програму. Ваші документи й налаштування залишаться.+ Your system's package manager will remove it, and may ask for your password. Your documents and settings stay. Менеджер пакетів вашої системи видалить застосунок і може запитати пароль. Ваші документи й налаштування залишаться.+ The app will be removed. Your documents and settings stay. Застосунок буде видалено. Ваші документи й налаштування залишаться.+ Uninstall Видалити+ Cancel Скасувати+ Checked {ago} Перевірено {ago}+ just now щойно+ yesterday учора+ {month} {day}, {year} {day} {month} {year}++ Automatically Автоматично+ Every Craft app Усі застосунки Craft+ Apps on this computer Застосунки на цьому комп’ютері+ Installed apps with a newer version Встановлені застосунки, для яких є новіша версія+ Apps you can install Застосунки, які можна встановити+ Hide App Приховати застосунок+ Unhide App Показати застосунок+ Hidden Apps Приховані застосунки+ Apps you hid Застосунки, які ви приховали+ Checked {ago} · using {sources} Перевірено {ago} · джерела: {sources}+ {a} and {b} {a} і {b}+ Join the Discord Приєднатися до Discord+ Learn more Докладніше+ Minimize Згорнути+ Maximize Розгорнути+ Restore Відновити розмір+ New Новинка+ OK Гаразд+ Open link Відкрити посилання+ Update Оновити+ {name} icon Піктограма {name}+ Loading ArtCraft Launcher… Завантаження ArtCraft Launcher…+ Cancel update Скасувати оновлення+ Stop waiting; the update can be started again later Припинити очікування; оновлення можна буде запустити знову пізніше+ Download again Завантажити знову+ Install cancelled Встановлення скасовано+ Install cancelled. The download is kept for the next try. Встановлення скасовано. Завантажений файл збережено для наступної спроби.+ Update cancelled Оновлення скасовано+ Uninstall cancelled Видалення скасовано+ Retry install Повторити встановлення+ Retry update to {version} Повторити оновлення до {version}+ The download is still here. It's checked again, then installed. Завантажений файл збережено. Його буде перевірено ще раз, а потім встановлено.+ Waiting for another app… Очікування іншого застосунку…+ Waiting for other installs… Очікування завершення інших встановлень…++# 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)+ Flatpak bundle Пакет Flatpak+ Tarball (.tar.gz) Архів (.tar.gz)++# 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 Дизайн інтерфейсів і прототипування++# Month abbreviations for dates in day-month-year order.+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}+ 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}++# Table column headers and installed-app filter.+column App Застосунок+column Installed Наявна+column Latest Найновіша+column Released Дата випуску+filter Installed Встановлені+ {name} details Відомості про {name}++# Ukrainian plural forms: one|few|many.+@plural {n} app|{n} apps {n} застосунок|{n} застосунки|{n} застосунків+@plural {n} min ago|{n} min ago {n} хв тому|{n} хв тому|{n} хв тому+@plural {n} h ago|{n} h ago {n} год тому|{n} год тому|{n} год тому+@plural {n} day ago|{n} days ago {n} день тому|{n} дні тому|{n} днів тому++# Backend status and error message templates.+@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} зараз працює. Закрийте застосунок і спробуйте знову.++# Command IDs for menu items.+@id app.install Встановити+@id app.update Оновити+@id app.open Відкрити+@id app.download Завантажити+@id app.reveal Показати в теці+@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), details and picture viewer.+ Show ArtCraft Launcher Показати ArtCraft Launcher+ Check for Updates Перевірити оновлення+ Visit getartcraft.com Відвідати getartcraft.com+ ArtCraft on GitHub ArtCraft на GitHub+ Join the ArtCraft Discord Приєднатися до Discord ArtCraft+ App details Відомості про застосунок+ 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. Не вдалося завантажити зображення. Відкрийте цю сторінку знову, щоб повторити спробу.+@plural ArtCraft Launcher · {n} update available|ArtCraft Launcher · {n} updates available ArtCraft Launcher · доступне {n} оновлення|ArtCraft Launcher · доступні {n} оновлення|ArtCraft Launcher · доступно {n} оновлень++# Install folders and replacement confirmation.+ 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.+ 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 Замінити++# Additional English keys retained by existing catalogs.+ Classic Класика+ Website (coming soon) Вебсайт (незабаром)new file mode 100644@@ -0,0 +1,195 @@+//! Ukrainian UI flow in a separate binary because the UI language is process-wide.+//! The backend records commands and simulates replies; it never downloads or installs apps.+//! Set ARTCRAFT_LAUNCHER_SNAPSHOTS to save screenshots; run with --test-threads=1.++use std::cell::RefCell;+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;++// 16 October 2026, 12:00 UTC; releases are dated 14 October.+const NOW: i64 = 1_792_152_000;++struct Fake {+ events: Vec<Event>,+ sent: Rc<RefCell<Vec<Command>>>,+}++impl Backend for Fake {+ fn send(&mut self, command: Command) {+ if let Command::Install { app } = &command+ && let Some(craft) = SUITE.iter().find(|craft| craft.id == app)+ {+ // Simulate the reply after the user action, so its toast is still visible.+ self.events.push(Event::JobFinished { app: app.clone(), result: Ok(format!("{} installed: 0.5.0", craft.name)) });+ }+ 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: true, reveal_label: "Show in Explorer", self_update: true }+ }+ fn platform(&self) -> Option<Platform> {+ Some(Platform { os: Os::Windows, arch: Arch::X64 })+ }+ fn now(&self) -> i64 {+ NOW+ }+}++struct NoHost;+impl Host for NoHost {}++fn events() -> Vec<Event> {+ let mut events = Vec::new();+ for app in &SUITE {+ let name = format!("{}-0.5.0-windows-x64.msi", app.id);+ let release = Release {+ tag: "v0.5.0".into(),+ version: version::parse("0.5.0"),+ name: "v0.5.0".into(),+ published_at: Some(NOW - 2 * 86_400),+ html_url: format!("https://github.com/storytold/{}/releases/tag/v0.5.0", app.id),+ notes: String::new(),+ prerelease: false,+ assets: vec![Asset {+ name: name.clone(),+ size: 150 << 20,+ url: format!("https://github.com/storytold/{}/releases/download/v0.5.0/{name}", app.id),+ sha256: None,+ }],+ };+ events.push(Event::Latest { app: app.id.into(), latest: LatestRelease::Found(release), from: Origin::ArtCraftApi });+ // One fresh install and one update make their actions unambiguous to query.+ let installed = (app.id != "vectorcraft").then(|| Installed {+ version: version::parse(if app.id == "photocraft" { "0.4.1" } else { "0.5.0" }),+ source: InstallSource::Msi,+ location: format!(r"C:\Apps\{}\{}.exe", app.name, app.id),+ package: None,+ });+ events.push(Event::Detected { app: app.id.into(), installed });+ }+ events.push(Event::Progress { app: "filmcraft".into(), phase: JobPhase::Verifying });+ events.push(Event::CheckFinished { at: NOW - 180, source: Source::Feed, rate_limited_until: None });+ events+}++fn harness(settings: Settings) -> (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 mut backend = Some(Fake { events: events(), sent: sent.clone() });+ let h = Harness::builder().with_size(egui::vec2(1120.0, 900.0)).with_theme(egui::Theme::Dark).build_ui_state(+ move |ui, state: &mut Option<LauncherUi>| {+ if let Some(backend) = backend.take() {+ *state = Some(LauncherUi::new(ui.ctx(), Box::new(backend), settings.clone()));+ }+ if let Some(launcher) = state.as_mut() {+ launcher.logic(ui.ctx(), &mut NoHost);+ launcher.ui(ui, &mut NoHost);+ }+ },+ None,+ );+ (h, sent)+}++fn snapshot(h: &mut Harness<'_, Option<LauncherUi>>, name: &str) {+ let Some(dir) = std::env::var_os("ARTCRAFT_LAUNCHER_SNAPSHOTS") else { return };+ std::fs::create_dir_all(&dir).unwrap();+ h.render().unwrap().save(std::path::Path::new(&dir).join(format!("{name}.png"))).unwrap();+}++#[test]+fn ukrainian_switches_persists_and_keeps_windows_install_actions() {+ let settings =+ Settings { language: "en".into(), notify: false, theme: artcraft_launcher_ui_egui::theme::ThemeChoice::Dark, ..Settings::default() };+ let (mut h, sent) = harness(settings);+ h.run_steps(8);+ h.get_by_label("Settings").click();+ h.run_steps(3);+ h.get_by_value("English").click();+ h.run_steps(2);+ // Ukrainian is the last item, below the ComboBox's visible scroll area.+ // kittest's pointer click does not scroll a queried accessibility node into view.+ h.get_by_label("Українська").scroll_to_me();+ h.run_steps(16);+ snapshot(&mut h, "language-picker-uk-windows-dark");+ h.get_by_label("Українська").click();+ h.run_steps(3);+ assert_eq!(h.state().as_ref().unwrap().settings.language, "uk");+ h.get_by_value("Українська");+ h.get_by_label("Автоматично завантажувати й встановлювати оновлення");+ h.get_by_label("Сповіщати про оновлення встановлених застосунків");+ h.get_by_label("Встановлення у Windows");+ snapshot(&mut h, "settings-uk-windows-dark");+ h.get_by_label("Готово").click();+ h.run_steps(3);+ h.get_by_label("Launcher");+ h.get_by_label("Craft App Suite");+ h.get_by_label("Редагування зображень");+ h.get_by_label("Оновити до 0.5.0");+ h.get_by_label_contains("Перевірка контрольної суми…");+ h.get_by_label_contains("Перевірено 3 хв тому · джерела: ArtCraft API");+ assert!(h.query_all_by_label_contains("14 жовт. 2026").count() >= 1);+ snapshot(&mut h, "grid-uk-windows-dark");++ sent.borrow_mut().clear();+ h.get_by_label("Встановити").click();+ h.run_steps(3);+ assert!(!sent.borrow().iter().any(|c| matches!(c, Command::Install { .. })));+ h.get_by_label("Застосунки розміщуються в цій теці.");+ let folder = r"C:\Craft test apps";+ h.get_by_role_and_label(egui::accesskit::Role::TextInput, "Тека встановлення").click();+ h.get_by_role_and_label(egui::accesskit::Role::TextInput, "Тека встановлення").type_text(folder);+ h.run_steps(3);+ assert_eq!(h.get_by_role_and_label(egui::accesskit::Role::TextInput, "Тека встановлення").value().as_deref(), Some(folder));+ assert!(h.state().as_ref().unwrap().settings.install_locations.per_app.is_empty());+ snapshot(&mut h, "install-uk-windows-dark");+ h.get_all_by_role_and_label(egui::accesskit::Role::Button, "Встановити").last().unwrap().click();+ h.run_steps(3);+ let actions: Vec<_> = sent.borrow().iter().filter(|c| matches!(c, Command::Configure(_) | Command::Install { .. })).cloned().collect();+ assert!(matches!(actions.as_slice(), [Command::Configure(options), Command::Install { app }]+ if options.install_locations.folder("vectorcraft") == Some(folder) && app == "vectorcraft"));+ h.get_by_label("VectorCraft встановлено: 0.5.0");++ h.get_by_label("Таблиця").click();+ h.run_steps(4);+ assert_eq!(h.state().as_ref().unwrap().settings.view, ViewMode::Table);+ h.get_by_label("Наявна");+ assert!(h.query_all_by_label_contains("14 жовт. 2026").count() >= 1);+ snapshot(&mut h, "table-uk-windows-dark");++ // Recreate the UI from serialized preferences, then switch back through the real picker.+ let saved = serde_json::to_string(&h.state().as_ref().unwrap().settings).unwrap();+ let restored: Settings = serde_json::from_str(&saved).unwrap();+ assert_eq!(restored.language, "uk");+ drop(h);+ let (mut reopened, _) = harness(restored);+ reopened.run_steps(8);+ reopened.get_by_label("Наявна");+ reopened.get_by_label("Налаштування").click();+ reopened.run_steps(3);+ reopened.get_by_value("Українська").click();+ reopened.run_steps(2);+ reopened.get_by_label("English").scroll_to_me();+ reopened.run_steps(16);+ reopened.get_by_label("English").click();+ reopened.run_steps(3);+ assert_eq!(reopened.state().as_ref().unwrap().settings.language, "en");+ reopened.get_by_label("Automatically download and install updates");+ reopened.get_by_label("Installing on Windows");+ reopened.get_by_label("Done").click();+ reopened.run_steps(3);+ reopened.get_by_label("Settings");+ assert!(reopened.query_by_label("Налаштування").is_none());+ assert!(reopened.query_all_by_label_contains("Oct 14, 2026").count() >= 1);+}