Hide Craft apps the user is not interested in. (#185)

* Add a Polish interface

- pl.tsv: the full catalog (287 rows), written clean-room from the English
  labels; plurals use the same one|few|many rule as Russian.
- Registered as the seventh language: "Polski" in the picker, locales pl,
  pl-PL and pl_PL.UTF-8, complete_menus = true so the completeness tests
  cover it.
- tests/polish.rs: the Polish UI test, modeled on the French one.
- README and AGENTS updated for the new language.

* Add Hungarian localization and fix responsive settings layout

* i18n: add Czech (cs) interface language

Czech catalog with every key of the most complete catalog (287 rows), the
cs language entry with locale detection (cs-CZ, cs_CZ.UTF-8, ?lang=cs),
a Czech plural rule (1 / 2-4 / other) with a unit test, and the README
language list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* i18n: add Japanese (ja) interface language

ja.tsv translates the whole interface, the tray menu and tooltip, each app's
tagline and the backend's install/update messages into Japanese, and
Settings > Language offers 日本語. Japanese locales (ja, ja-JP, ja_JP.UTF-8…)
select it; Japanese has one plural form.

Japanese is a CJK language, so cjk_font.rs now looks for a Japanese system
font (Hiragino Sans on macOS; Yu Gothic, Meiryo or MS Gothic on Windows;
Noto Sans CJK JP, Noto Sans JP, IPAGothic, Takao or VL Gothic on Linux and
FreeBSD), falling back to a Chinese one, which draws kana and most kanji,
where the system has none. The font is loaded per language, so switching
between Japanese and Chinese reloads the right face.

A fallback face is nudged onto the body font's baseline: egui puts a fallback
glyph's baseline at that face's own ascent and only centres the two line
boxes, which left a face whose metrics differ from Inter's riding high. On
Windows, Yu Gothic's line gap is half an em, so its kana and kanji sat about
0.29 em above the Latin text beside them (Microsoft YaHei, used for Chinese,
happens to match). The nudge, applied through a FontTweak, is the difference
in ascent and line height, and helps the Chinese faces too.

The README and AGENTS list the new language. Tests: the complete-catalog
checks cover ja.tsv (placeholders, plural forms, every tl! literal, taglines,
package formats), plus a locale/plural test, a Japanese glyph-coverage test,
a baseline test and tests/japanese.rs (grid, table, dates, a backend message).

* i18n: make the CJK font choice language-neutral

The CJK comments, the missing-font note and the plural-rule docs named the
languages, so each new one meant editing them all; they now say "CJK". Each
language's writing system (its `script`) now lives in the registry, and a
language is drawn only with a face for its own script — never another's,
which would leave characters as tofu — so a language the system has no font
for stays in English, like any other. Adding Korean or Traditional Chinese is
a registry row and a candidate list, not a special case.

* Hide Craft apps the user is not interested in.

* i18n(pl): use Polish plural rule, so 21 is "21 aplikacji"

plural_east_slavic treats 21, 31 and 101 as "one", which is right for
Russian but not Polish, where only 1 itself takes the singular.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* i18n(cs): translate "Installed · latest {latest}" (added by #195)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* i18n(pl): translate "Installed · latest {latest}" (added by #195)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* i18n(hu): translate "Installed · latest {latest}" (added by #195)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* i18n(ja): translate "Installed · latest {latest}" (added by #195)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* i18n: hide/unhide strings for the Spanish, Czech, Polish, Hungarian and Japanese catalogs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* hungarian test: count only the settings body's links, not the cards' Website links behind the modal

#198 gave every card a Website link, and Hungarian labels both Webhely.

* hungarian test: card links are 14px text, settings icons 24px; filter on that

---------

Co-authored-by: DzikStar <82441193+DzikStar@users.noreply.github.com>
Co-authored-by: ferivoq <ferikegameplay@gmail.com>
Co-authored-by: Losminek <301412321+Losminek@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: leonaci <leonacism@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
satchmo91 2026-10-12 01:48:47 -05:00 committed by GitHub
parent 93f12a5d9e
commit 250cc54335
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 121 additions and 36 deletions

View File

@ -182,9 +182,10 @@ impl LauncherUi {
self.toasts.clear();
}
/// Installed apps with an update waiting (the tray shows the count).
/// Installed apps with an update waiting (the tray shows the count). Hidden apps are left out,
/// so the badge matches the Updates list.
pub fn update_count(&self) -> usize {
self.entries.iter().filter(|e| e.status(self.platform, self.caps.manage_apps) == AppStatus::UpdateAvailable).count()
self.entries.iter().filter(|e| self.in_filter(e, Filter::Updates)).count()
}
/// Something is downloading or installing, the launcher's own update included: quitting now
@ -377,7 +378,7 @@ impl LauncherUi {
fn announce_updates(&mut self, host: &mut dyn Host) {
let mut fresh = Vec::new();
for e in &self.entries {
if e.status(self.platform, self.caps.manage_apps) != AppStatus::UpdateAvailable {
if !self.in_filter(e, Filter::Updates) {
continue;
}
let Some(v) = e.latest_label() else { continue };
@ -414,7 +415,7 @@ impl LauncherUi {
let to_update: Vec<(String, String)> = self
.entries
.iter()
.filter(|e| e.status(self.platform, true) == AppStatus::UpdateAvailable && e.job.is_none() && !e.ready)
.filter(|e| self.in_filter(e, Filter::Updates) && e.job.is_none() && !e.ready)
.filter_map(|e| {
let v = e.latest_label()?;
if self.auto_update_attempted.contains(&(e.app.id.to_string(), v.clone())) { None } else { Some((e.app.id.to_string(), v)) }
@ -517,14 +518,8 @@ impl LauncherUi {
.iter()
.enumerate()
.filter(|(_, e)| {
let s = e.status(self.platform, self.caps.manage_apps);
let keep = match self.settings.filter {
Filter::All => true,
Filter::Installed => s.is_installed(),
Filter::Updates => s == AppStatus::UpdateAvailable,
Filter::NotInstalled => !s.is_installed(),
};
keep && (needle.is_empty() || e.app.name.to_lowercase().contains(&needle) || e.app.tagline.to_lowercase().contains(&needle))
self.in_filter(e, self.settings.filter)
&& (needle.is_empty() || e.app.name.to_lowercase().contains(&needle) || e.app.tagline.to_lowercase().contains(&needle))
})
.collect();
let platform = self.platform;
@ -591,18 +586,20 @@ impl LauncherUi {
}
pub(crate) fn count(&self, filter: Filter) -> usize {
self.entries
.iter()
.filter(|e| {
let s = e.status(self.platform, self.caps.manage_apps);
match filter {
Filter::All => true,
Filter::Installed => s.is_installed(),
Filter::Updates => s == AppStatus::UpdateAvailable,
Filter::NotInstalled => !s.is_installed(),
}
})
.count()
self.entries.iter().filter(|e| self.in_filter(e, filter)).count()
}
/// Whether `entry` belongs in `filter`. Hidden apps stay out of every list except Hidden.
pub(crate) fn in_filter(&self, entry: &AppEntry, filter: Filter) -> bool {
let hidden = self.settings.hidden.contains(entry.app.id);
let status = entry.status(self.platform, self.caps.manage_apps);
match filter {
Filter::Hidden => hidden,
Filter::All => !hidden,
Filter::Installed => !hidden && status.is_installed(),
Filter::Updates => !hidden && status == AppStatus::UpdateAvailable,
Filter::NotInstalled => !hidden && !status.is_installed(),
}
}
fn draw_toasts(&mut self, ctx: &egui::Context) {

View File

@ -104,6 +104,10 @@
Apps on this computer Aplikace v tomto počítači
Installed apps with a newer version Nainstalované aplikace s novější verzí
Apps you can install Aplikace, které lze nainstalovat
Hide App Skrýt aplikaci
Unhide App Zobrazit aplikaci
Hidden Apps Skryté aplikace
Apps you hid Aplikace, které jste skryli
Checked {ago} · using {sources} Zkontrolováno {ago} · zdroje: {sources}
{a} and {b} {a} a {b}
Join the Discord Připojit se na Discord

Can't render this file because it has a wrong number of fields in line 8.

View File

@ -118,6 +118,10 @@
Apps on this computer Apps auf diesem Computer
Installed apps with a newer version Installierte Apps mit neuerer Version
Apps you can install Apps, die du installieren kannst
Hide App App ausblenden
Unhide App App einblenden
Hidden Apps Ausgeblendete Apps
Apps you hid Apps, die du ausgeblendet hast
Checked {ago} · using {sources} Zuletzt geprüft: {ago} · über {sources}
{a} and {b} {a} und {b}
Join the Discord Dem Discord beitreten

Can't render this file because it contains an unexpected character in line 11 and column 16.

View File

@ -117,6 +117,10 @@
Apps on this computer Aplicaciones en este equipo
Installed apps with a newer version Aplicaciones instaladas con una versión más reciente
Apps you can install Aplicaciones que puedes instalar
Hide App Ocultar aplicación
Unhide App Mostrar aplicación
Hidden Apps Aplicaciones ocultas
Apps you hid Aplicaciones que ocultaste
Checked {ago} · using {sources} Comprobado {ago} · con {sources}
{a} and {b} {a} y {b}
Join the Discord Únete al Discord

Can't render this file because it contains an unexpected character in line 5 and column 48.

View File

@ -116,6 +116,10 @@
Apps on this computer Applications sur cet ordinateur
Installed apps with a newer version Applications installées avec une version plus récente
Apps you can install Applications que vous pouvez installer
Hide App Masquer l'application
Unhide App Afficher l'application
Hidden Apps Applications masquées
Apps you hid Applications que vous avez masquées
Checked {ago} · using {sources} Vérifié {ago} · via {sources}
{a} and {b} {a} et {b}
Join the Discord Rejoindre le Discord

Can't render this file because it contains an unexpected character in line 5 and column 50.

View File

@ -118,6 +118,10 @@
Apps on this computer Alkalmazások ezen a számítógépen
Installed apps with a newer version Telepített alkalmazások, amelyekhez frissítés érhető el
Apps you can install Telepíthető alkalmazások
Hide App Alkalmazás elrejtése
Unhide App Alkalmazás megjelenítése
Hidden Apps Rejtett alkalmazások
Apps you hid Az általad elrejtett alkalmazások
Checked {ago} · using {sources} Ellenőrizve: {ago} · {sources}
{a} and {b} {a} és {b}
Join the Discord Csatlakozás a Discordhoz

Can't render this file because it contains an unexpected character in line 11 and column 16.

View File

@ -120,6 +120,10 @@
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 に参加

Can't render this file because it contains an unexpected character in line 14 and column 44.

View File

@ -119,6 +119,10 @@
Apps on this computer Aplikacje na tym komputerze
Installed apps with a newer version Zainstalowane aplikacje z nowszą wersją
Apps you can install Aplikacje, które możesz zainstalować
Hide App Ukryj aplikację
Unhide App Pokaż aplikację
Hidden Apps Ukryte aplikacje
Apps you hid Aplikacje ukryte przez ciebie
Checked {ago} · using {sources} Ostatnio sprawdzono {ago} · przy użyciu {sources}
{a} and {b} {a} i {b}
Join the Discord Dołącz na Discorda

Can't render this file because it contains an unexpected character in line 11 and column 15.

View File

@ -106,6 +106,10 @@
Apps on this computer Aplicativos neste computador
Installed apps with a newer version Aplicativos instalados com nova versão
Apps you can install Aplicativos que você pode instalar
Hide App Ocultar aplicativo
Unhide App Mostrar aplicativo
Hidden Apps Aplicativos ocultos
Apps you hid Aplicativos que você ocultou
Checked {ago} · using {sources} Verificado {ago} · via {sources}
{a} and {b} {a} e {b}
Join the Discord Entrar no Discord

Can't render this file because it has a wrong number of fields in line 9.

View File

@ -116,6 +116,10 @@
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

Can't render this file because it contains an unexpected character in line 13 and column 44.

View File

@ -262,6 +262,10 @@ soundcraft Recording, editing and mixing audio 音频录制、编辑与混音
Apps on this computer 此电脑上的应用
Installed apps with a newer version 有新版本的已安装应用
Apps you can install 可安装的应用
Hide App 隐藏应用
Unhide App 取消隐藏应用
Hidden Apps 已隐藏的应用
Apps you hid 你隐藏的应用
Cancel update 取消更新
Stop waiting; the update can be started again later 停止等待;稍后可重新开始更新
Download again 重新下载

Can't render this file because it contains an unexpected character in line 4 and column 3.

View File

@ -22,6 +22,8 @@ pub enum Filter {
Installed,
Updates,
NotInstalled,
/// Apps the user chose to hide from the other lists.
Hidden,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
@ -110,6 +112,8 @@ pub struct Settings {
pub notified: BTreeMap<String, String>,
/// Unix seconds of the last finished check.
pub last_check: Option<i64>,
/// Catalog ids of apps hidden from the main lists (`photocraft`, …).
pub hidden: std::collections::BTreeSet<String>,
}
impl Settings {
@ -181,6 +185,7 @@ impl Default for Settings {
dismissed_notices: std::collections::BTreeSet::new(),
notified: BTreeMap::new(),
last_check: None,
hidden: std::collections::BTreeSet::new(),
}
}
}

View File

@ -126,6 +126,9 @@ impl LauncherUi {
}
pub(crate) fn toolbar(&mut self, ui: &mut Ui) {
if self.settings.filter == Filter::Hidden && self.settings.hidden.is_empty() {
self.settings.filter = Filter::All;
}
let t = Tokens::get(ui.ctx());
if self.dialog.is_none()
&& let Some(r) = self.launcher_update.clone()
@ -195,17 +198,19 @@ impl LauncherUi {
self.announcements(ui);
ui.horizontal(|ui| {
let updates = self.count(Filter::Updates);
let filters: Vec<(Filter, Cell<'_>)> = [
let mut filters = vec![
(Filter::All, tl!("All"), tl!("Every Craft app"), None),
// The tab names a group ("installed ones"); the status pill says "Installed".
(Filter::Installed, tr_ctx(current(), "filter", "Installed"), tl!("Apps on this computer"), None),
(Filter::Updates, tl!("Updates"), tl!("Installed apps with a newer version"), Some(t.warn)),
(Filter::NotInstalled, tl!("Not installed"), tl!("Apps you can install"), None),
]
.into_iter()
.filter(|(f, ..)| self.caps.manage_apps || *f == Filter::All)
.map(|(f, label, tip, highlight)| (f, Cell { label, tip, count: Some((self.count(f), highlight)) }))
.collect();
];
filters.retain(|(f, ..)| self.caps.manage_apps || *f == Filter::All);
if !self.settings.hidden.is_empty() {
filters.push((Filter::Hidden, tl!("Hidden Apps"), tl!("Apps you hid"), None));
}
let filters: Vec<(Filter, Cell<'_>)> =
filters.into_iter().map(|(f, label, tip, highlight)| (f, Cell { label, tip, count: Some((self.count(f), highlight)) })).collect();
widgets::cells(ui, &mut self.settings.filter, &filters);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if self.caps.manage_apps && updates > 0 {
@ -215,12 +220,8 @@ impl LauncherUi {
fmt(tl!("Update all ({count})"), &[("count", &updates.to_string())])
};
if widgets::button(ui, &label, Kind::Primary, 0.0).clicked() {
let ids: Vec<&str> = self
.entries
.iter()
.filter(|e| e.status(self.platform, true) == AppStatus::UpdateAvailable && e.job.is_none())
.map(|e| e.app.id)
.collect();
let ids: Vec<&str> =
self.entries.iter().filter(|e| self.in_filter(e, Filter::Updates) && e.job.is_none()).map(|e| e.app.id).collect();
for id in ids {
self.send(Command::Install { app: id.to_string() });
}
@ -838,6 +839,19 @@ impl LauncherUi {
link(ui, tl!("Website"), entry.app.website());
link(ui, tl!("GitHub repository"), entry.app.repo_url());
link(ui, tl!("All releases"), entry.app.releases_url());
ui.separator();
let hidden = self.settings.hidden.contains(entry.app.id);
if ui.button(if hidden { tl!("Unhide App") } else { tl!("Hide App") }).clicked() {
if hidden {
self.settings.hidden.remove(entry.app.id);
if self.settings.hidden.is_empty() && self.settings.filter == Filter::Hidden {
self.settings.filter = Filter::All;
}
} else {
self.settings.hidden.insert(id);
}
ui.close();
}
});
}
}

View File

@ -390,6 +390,35 @@ fn table_view_and_light_theme() {
assert!(sent.borrow().iter().any(|c| matches!(c, Command::Launch { .. })));
}
#[test]
fn hiding_an_app_removes_it_until_it_is_unhidden() {
let (mut h, _) = harness(ViewMode::Table, egui::Theme::Dark, true);
h.run_steps(6);
// PhotoCraft has an update, so hiding it also drops the Updates count.
h.get_by_label_contains("Updates 2");
let row = h.get_all_by_label("PhotoCraft").next().unwrap().rect().center().y;
h.get_all_by_label("…").find(|n| (n.rect().center().y - row).abs() < 25.0).unwrap().click();
h.run_steps(3);
h.get_by_label("Hide App").click();
h.run_steps(4);
assert!(h.query_by_label("PhotoCraft").is_none());
assert!(h.state().as_ref().unwrap().settings.hidden.contains("photocraft"));
h.get_by_label_contains("Updates 1");
h.get_by_label_contains("Hidden Apps").click();
h.run_steps(4);
h.get_by_label("PhotoCraft");
assert!(h.query_by_label("WordCraft").is_none());
let row = h.get_all_by_label("PhotoCraft").next().unwrap().rect().center().y;
h.get_all_by_label("…").find(|n| (n.rect().center().y - row).abs() < 25.0).unwrap().click();
h.run_steps(3);
h.get_by_label("Unhide App").click();
h.run_steps(4);
h.get_by_label("PhotoCraft");
assert!(h.query_by_label_contains("Hidden Apps").is_none());
assert!(h.state().as_ref().unwrap().settings.hidden.is_empty());
assert_eq!(h.state().as_ref().unwrap().settings.filter, artcraft_launcher_ui_egui::settings::Filter::All);
}
#[test]
fn switching_views_and_filters() {
let (mut h, _) = harness(ViewMode::Grid, egui::Theme::Dark, true);