Add a French interface (#85)

* Add a French interface

fr.tsv translates the whole interface, the tray menu and tooltip, each app's
tagline and the backend's install/update messages, and Settings > Language
offers Français. fr, fr-FR, fr_CA.UTF-8 and the like select it. French counts
0 and 1 as singular, so it gets its own plural rule. tests/french.rs checks the
grid, the table, dates and a backend message.

* Rebase onto German: French last in the registry, new update strings, lowercase months

French writes month abbreviations in lower case (12 août 2026). Tests use a
never-supported tag (xx-XX) next to fr-FR.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-11 02:34:32 -04:00
1 parent bd4fd187c7
commit 89854ff8f2
5 files changed
+489 -6

No files matched your search

+2 -2
View File
@@ -84,10 +84,10 @@ The web version shows the same catalog in the browser, with the latest download
## Interface languages
The interface is in English, German (Deutsch), Russian (Русский) or Simplified Chinese
The interface is in English, French (Français), German (Deutsch), Russian (Русский) or Simplified Chinese
(简体中文). **Auto**, the default, follows the system or browser language; **Settings → Language**
picks one and switches at once, and the choice is kept between runs. German locales (`de-DE`,
`de-AT`, `de-CH`, `de_DE.UTF-8`…) select German. Simplified Chinese locales (`zh-CN`, `zh-SG`,
`de-AT`, `de-CH`, `de_DE.UTF-8`…) select German, and French ones (`fr-FR`, `fr-CA`…) French. 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=zh-CN`, or add `?lang=ru` to the web address.
+324
View File
@@ -0,0 +1,324 @@
# French (fr) catalog for ArtCraft Launcher.
# Format: context<TAB>source<TAB>translation
# Empty context = plain UI string (tl!, t).
# @id = command-id keyed (tr_id).
# @plural = plural forms: one|other (0 and 1 are "one" in French).
# @msg = status/error message template (message, msg).
# Other context = disambiguation for tr_ctx.
#
# Clean-room: written from the meaning of ArtCraft Launcher's English labels, in neutral
# international French (the interface addresses the user as "vous").
#
# 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), "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 Des outils open source pour les créatifs, les makers et les bâtisseurs
Settings Réglages
Check for updates Rechercher des mises à jour
Grid Grille
Table Tableau
Show apps as cards Afficher les applications en cartes
Show apps as a table Afficher les applications en tableau
All Toutes
Updates Mises à jour
Not installed Non installées
Search apps Rechercher des applications
Sort Tri
Suite order Ordre de la suite
Name Nom
Inspired by Inspirée de
Status État
Installed version Version installée
Latest version Dernière version
Newest release Version la plus récente
Discord Discord
GitHub GitHub
Website Site web
Classic apps Applications classiques
New apps Nouvelles applications
NEW NOUVEAU
Install Installer
Update to {v} Mettre à jour vers {v}
Open Ouvrir
Download for {os} Télécharger pour {os}
Download Télécharger
Coming soon Bientôt disponible
Checking… Vérification…
Not available Indisponible
Offline Hors ligne
Downloading {done} of {total} Téléchargement de {done} sur {total}
Downloading… Téléchargement…
Reconnecting… Reconnexion…
Verifying… Vérification…
Installing, then restarting… Installation, puis redémarrage…
Installing… Installation…
Removing… Suppression…
Pause download Mettre le téléchargement en pause
Cancel download Annuler le téléchargement
Resume download Reprendre le téléchargement
Discard download Supprimer le téléchargement
Release notes Notes de version
Show in Finder Afficher dans le Finder
Show in Explorer Afficher dans l'Explorateur
Show in Files Afficher dans Fichiers
Reinstall Réinstaller
Uninstall… Désinstaller…
All releases Toutes les versions
ArtCraft Launcher {version} is available. ArtCraft Launcher {version} est disponible.
Dismiss Ignorer
Update and restart Mettre à jour et redémarrer
A new version is available. Would you like to relaunch with the updated version? Une nouvelle version est disponible. Redémarrer avec la nouvelle version ?
Later Plus tard
What's new Nouveautés
No notes were published with this release. Aucune note n'a été publiée avec cette version.
Released {date} Publiée le {date}
Open on GitHub Ouvrir sur GitHub
Close Fermer
ArtCraft Launcher {version} ArtCraft Launcher {version}
Only when I ask Seulement à ma demande
Automatically download and install updates Télécharger et installer les mises à jour automatiquement
Notify me when an installed app has an update Me prévenir quand une application installée a une mise à jour
Include pre-releases (betas and release candidates) Inclure les préversions (bêtas et versions candidates)
Installing on Linux Installation sous Linux
Install new apps as Installer les nouvelles applications en tant que
System package (recommended) Paquet système (recommandé)
Updates always use the format an app was installed with. Les mises à jour utilisent toujours le format avec lequel l'application a été installée.
Running in the background Exécution en arrière-plan
Keep running in the tray when the window is closed Rester actif dans la zone de notification à la fermeture de la fenêtre
Show the window on the current desktop Afficher la fenêtre sur le bureau actuel
Start ArtCraft Launcher when I log in Lancer ArtCraft Launcher à l'ouverture de session
Community Communauté
Questions, ideas and news from the ArtCraft team Questions, idées et actualités de l'équipe ArtCraft
Appearance Apparence
Quit ArtCraft Launcher Quitter ArtCraft Launcher
Done Terminé
Uninstall {name}? Désinstaller {name} ?
The app will be moved to the Trash. Your documents and settings stay. L'application sera placée dans la Corbeille. Vos documents et réglages sont conservés.
Windows will remove it like any installed program. Your documents and settings stay. Windows la supprimera comme n'importe quel programme installé. Vos documents et réglages sont conservés.
Your system's package manager will remove it, and may ask for your password. Your documents and settings stay. Le gestionnaire de paquets de votre système la supprimera et pourra vous demander votre mot de passe. Vos documents et réglages sont conservés.
The app will be removed. Your documents and settings stay. L'application sera supprimée. Vos documents et réglages sont conservés.
Uninstall Désinstaller
Cancel Annuler
Checked {ago} Vérifié {ago}
just now à l'instant
yesterday hier
{month} {day}, {year} {day} {month} {year}
Automatically Automatiquement
Every Craft app Toutes les applications Craft
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
Checked {ago} · using {sources} Vérifié {ago} · via {sources}
{a} and {b} {a} et {b}
Join the Discord Rejoindre le Discord
Learn more En savoir plus
Minimize Réduire
Maximize Agrandir
Restore Restaurer
New Nouveau
OK OK
Open link Ouvrir le lien
Update Mettre à jour
{name} icon Icône de {name}
Loading ArtCraft Launcher… Chargement d'ArtCraft Launcher…
Cancel update Annuler la mise à jour
Stop waiting; the update can be started again later Ne plus attendre ; la mise à jour pourra être relancée plus tard
Download again Télécharger à nouveau
Install cancelled Installation annulée
Install cancelled. The download is kept for the next try. Installation annulée. Le téléchargement est conservé pour la prochaine tentative.
Update cancelled Mise à jour annulée
Uninstall cancelled Désinstallation annulée
Retry install Réessayer l'installation
Retry update to {version} Réessayer la mise à jour vers {version}
The download is still here. It's checked again, then installed. Le téléchargement est toujours là. Il sera vérifié à nouveau, puis installé.
Waiting for another app… En attente d'une autre application…
Waiting for other installs… En attente d'autres installations…
# Package formats offered for download (PackageKind::label)
Installer (.msi) Programme d'installation (.msi)
Portable (.zip) Version portable (.zip)
Disk image (.dmg) Image disque (.dmg)
Debian package (.deb) Paquet Debian (.deb)
RPM package (.rpm) Paquet RPM (.rpm)
Flatpak bundle Paquet Flatpak
Tarball (.tar.gz) Archive (.tar.gz)
# Contextual: each app's tagline, keyed by app id
photocraft Image editing Retouche d'images
vectorcraft Vector illustration Illustration vectorielle
filmcraft Video editing, color and sound Montage vidéo, étalonnage et son
lightcraft Photo library and raw development Photothèque et développement RAW
pdfcraft Reading, organizing and protecting PDFs Lecture, organisation et protection de PDF
effectcraft Motion graphics and visual effects Motion design et effets visuels
designcraft Page layout and publishing Mise en page et édition
wordcraft Writing and document design Rédaction et mise en forme de documents
deckcraft Presentations and slide shows Présentations et diaporamas
gridcraft Spreadsheets and data Tableurs et données
cadcraft Computer-aided design and drafting Conception et dessin assistés par ordinateur
soundcraft Recording, editing and mixing audio Enregistrement, édition et mixage audio
# Contextual: month abbreviations for date rendering
month Jan janv.
month Feb févr.
month Mar mars
month Apr avr.
month May mai
month Jun juin
month Jul juil.
month Aug août
month Sep sept.
month Oct oct.
month Nov nov.
month Dec déc.
Not checked yet Pas encore vérifié
GitHub's hourly limit was reached; full details return in {min} min La limite horaire de GitHub a été atteinte ; les détails complets reviennent dans {min} min
Installing for {platform} Installation pour {platform}
Downloads for {platform} Téléchargements pour {platform}
No downloads for this system Aucun téléchargement pour ce système
Join the ArtCraft community on Discord Rejoindre la communauté ArtCraft sur Discord
ArtCraft on {name} ArtCraft sur {name}
Couldn't open {name}: {e} Impossible d'ouvrir {name} : {e}
Download cancelled Téléchargement annulé
Available to install Disponible à l'installation
Available Disponible
Installed · up to date Installé · à jour
Update available Mise à jour disponible
Installed Installé
installed installé
Not available here Indisponible ici
Updating… Mise à jour…
Update paused Mise à jour en pause
Download paused Téléchargement en pause
{version} installed → {latest} released {version} installée → {latest} publiée
Version {version} Version {version}
{installed} installed · latest {latest} {installed} installée · dernière {latest}
Version {version} installed Version {version} installée
Version {latest} Version {latest}
No release published yet Aucune version publiée pour l'instant
ArtCraft Launcher ArtCraft Launcher
App Application
Latest Dernière
Released Publiée
Auto Auto
Language Langue
Ascending Croissant
Descending Décroissant
Click to reverse the order Cliquez pour inverser l'ordre
Click to sort by this column Cliquez pour trier selon cette colonne
Like {name} Comme {name}
{done} of {total} {done} sur {total}
Downloading Téléchargement
Verifying checksum… Vérification de la somme de contrôle…
Resume Reprendre
{done} of {total} downloaded {done} sur {total} téléchargés
{done} downloaded {done} téléchargés
Checking for updates… Recherche de mises à jour…
Downloads the new version, replaces this one and opens it again Télécharge la nouvelle version, remplace celle-ci et la rouvre
Couldn't check for updates: {f} Impossible de rechercher les mises à jour : {f}
Website (coming soon) Site web (bientôt disponible)
GitHub repository Dépôt GitHub
ArtCraft ArtCraft
Update 1 app Mettre à jour 1 application
Update all ({count}) Tout mettre à jour ({count})
⊞ Grid ⊞ Grille
☰ Table ☰ Tableau
🔍 Search apps 🔍 Rechercher des applications
{name} {version} {name} {version}
Match system Comme le système
Dark Sombre
Light Clair
Full changelog Journal des modifications complet
link lien
No apps match Aucune application ne correspond
Open ArtCraft Launcher to update. Ouvrez ArtCraft Launcher pour mettre à jour.
{name} {v} is available {name} {v} est disponible
{count} updates are available {count} mises à jour sont disponibles
These notes weren't fetched (GitHub's request limit was reached). They're on the release page. Ces notes n'ont pas pu être récupérées (la limite de requêtes de GitHub a été atteinte). Elles sont sur la page de la version.
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. Si cette option est activée, ouvrir le launcher depuis la barre des menus amène sa fenêtre sur le bureau où vous êtes. Sinon, macOS vous renvoie sur le bureau où se trouvait la fenêtre.
Couldn't change the login item: {e} Impossible de modifier l'élément d'ouverture de session : {e}
# Contextual: table column headers
column App Application
column Installed Installée
column Latest Dernière
column Released Date de publication
# Contextual: the filter tab names a group (the status pill says "Installed")
filter Installed Installées
# Plural forms for French (one|other)
@plural {n} app|{n} apps {n} application|{n} applications
@plural {n} min ago|{n} min ago il y a {n} min|il y a {n} min
@plural {n} h ago|{n} h ago il y a {n} h|il y a {n} h
@plural {n} day ago|{n} days ago il y a {n} jour|il y a {n} jours
# Message templates (@msg): toasts from the backend. {placeholders} match any text.
@msg Couldn't open {name}: {e} Impossible d'ouvrir {name} : {e}
@msg Download cancelled Téléchargement annulé
@msg {name} updated to {version} {name} mis à jour vers {version}
@msg {name} installed: {version} {name} installé : {version}
@msg {name} updated to {version} (this release publishes no checksum, so only its size was checked) {name} mis à jour vers {version} (cette version ne publie pas de somme de contrôle : seule sa taille a été vérifiée)
@msg {name} installed: {version} (this release publishes no checksum, so only its size was checked) {name} installé : {version} (cette version ne publie pas de somme de contrôle : seule sa taille a été vérifiée)
@msg {name} was removed {name} a été supprimé
@msg {name} is running. Quit it, then try again. {name} est en cours d'exécution. Quittez-le, puis réessayez.
# Command IDs for menu items (@id)
# Format: @id<TAB>command_id<TAB>translation
@id app.install Installer
@id app.update Mettre à jour vers {v}
@id app.open Ouvrir
@id app.download Télécharger pour {os}
@id app.reveal Afficher dans le Finder
@id app.reinstall Réinstaller
@id app.uninstall Désinstaller…
@id app.notes Notes de version
@id app.website Site web
@id app.github Dépôt GitHub
@id app.releases Toutes les versions
@id launcher.update Mettre à jour et redémarrer
@id launcher.download Télécharger
@id launcher.notes Nouveautés
@id launcher.dismiss Ignorer
@id settings.open Réglages
@id settings.quit Quitter ArtCraft Launcher
@id view.grid Grille
@id view.table Tableau
@id filter.all Toutes
@id filter.installed Installées
@id filter.updates Mises à jour
@id filter.notinstalled Non installées
@id sort.suite Ordre de la suite
@id sort.name Nom
@id sort.inspired Inspirée de
@id sort.status État
@id sort.installed Version installée
@id sort.latest Dernière version
@id sort.released Version la plus récente
@id help.discord Discord
@id help.github GitHub
@id help.website Site web
@id update.check Rechercher des mises à jour
@id update.check_auto Rechercher automatiquement les mises à jour
@id update.notify Me prévenir quand une application installée a une mise à jour
@id update.prereleases Inclure les préversions (bêtas et versions candidates)
@id linux.preferred_kind Installer les nouvelles applications en tant que
@id tray.close_to_tray Rester actif dans la zone de notification à la fermeture de la fenêtre
@id tray.show_on_current_desktop Afficher la fenêtre sur le bureau actuel
@id tray.login_item Lancer ArtCraft Launcher à l'ouverture de session
@id appearance.theme Apparence
# Tray (menu bar) menu and tooltip
Show ArtCraft Launcher Afficher ArtCraft Launcher
Check for Updates Rechercher des mises à jour
Visit getartcraft.com Visiter getartcraft.com
ArtCraft on GitHub ArtCraft sur GitHub
Join the ArtCraft Discord Rejoindre le Discord d'ArtCraft
@plural ArtCraft Launcher · {n} update available|ArtCraft Launcher · {n} updates available ArtCraft Launcher · {n} mise à jour disponible|ArtCraft Launcher · {n} mises à jour disponibles
Can't render this file because it contains an unexpected character in line 5 and column 50.
+21 -4
View File
@@ -7,7 +7,7 @@ use super::detect::system_lang;
/// One supported UI language.
pub struct LangInfo {
/// Canonical BCP 47 code (`en`, `ru`, `zh-CN`, `de`). Also the `interfaceLanguage` value.
/// Canonical BCP 47 code (`en`, `ru`, `zh-CN`, `de`, `fr`). Also the `interfaceLanguage` value.
pub code: &'static str,
/// The language's name in itself, shown in the Preferences dropdown.
pub name: &'static str,
@@ -18,8 +18,8 @@ pub struct LangInfo {
pub cjk: bool,
/// Catalog file contents (empty for the built-in English).
pub source: &'static str,
/// Plural form index for a count (English and German: 0 = one, 1 = other; Russian: 0 = one, 1 = few,
/// 2 = many; Chinese: always 0).
/// Plural form index for a count (English and German: 0 = one, 1 = other; Russian: 0 = one,
/// 1 = few, 2 = many; French: 0 = one for 0 and 1; Chinese: always 0).
pub plural: fn(u64) -> usize,
/// Number of plural forms the catalog's `@plural` rows carry (checked by tests).
pub plural_forms: usize,
@@ -32,6 +32,11 @@ fn plural_one_other(n: u64) -> usize {
usize::from(n != 1)
}
/// French: 0 and 1 are singular ("0 mise à jour"), everything else plural.
fn plural_zero_one(n: u64) -> usize {
usize::from(n > 1)
}
/// Languages whose nouns don't inflect for number (Chinese): one form for every count.
fn plural_none(_: u64) -> usize {
0
@@ -51,7 +56,7 @@ pub(super) fn plural_east_slavic(n: u64) -> usize {
}
/// The registry. English first: it is the fallback and the source language.
pub static LANGUAGES: [LangInfo; 4] = [
pub static LANGUAGES: [LangInfo; 5] = [
LangInfo {
code: "en",
name: "English",
@@ -96,6 +101,17 @@ pub static LANGUAGES: [LangInfo; 4] = [
complete_menus: true,
catalog: OnceLock::new(),
},
LangInfo {
code: "fr",
name: "Français",
english_name: "French",
cjk: false,
source: include_str!("fr.tsv"),
plural: plural_zero_one,
plural_forms: 2,
complete_menus: true,
catalog: OnceLock::new(),
},
];
impl LangInfo {
@@ -127,6 +143,7 @@ impl Lang {
pub const RU: Lang = Lang(&LANGUAGES[1]);
pub const ZH_CN: Lang = Lang(&LANGUAGES[2]);
pub const DE: Lang = Lang(&LANGUAGES[3]);
pub const FR: Lang = Lang(&LANGUAGES[4]);
pub fn code(self) -> &'static str {
self.0.code
+13
View File
@@ -249,6 +249,19 @@ fn german_counts_dates_and_messages_work() {
assert_eq!(tr(Lang::DE, "Untranslated diagnostic"), "Untranslated diagnostic");
}
#[test]
fn french_locales_and_plurals_work() {
for tag in ["fr", "fr-FR", "fr_CA.UTF-8", "fr-BE", "FR_ch"] {
assert_eq!(lang_from_tag(tag), Some(Lang::FR), "{tag}");
}
assert_eq!(super::detect::first_supported(["xx-XX", "fr-FR", "en-US"]), Some(Lang::FR));
// 0 and 1 are singular in French, unlike English.
assert_eq!(trn(Lang::FR, 0, "{n} app", "{n} apps"), "0 application");
assert_eq!(trn(Lang::FR, 1, "{n} app", "{n} apps"), "1 application");
assert_eq!(trn(Lang::FR, 2, "{n} app", "{n} apps"), "2 applications");
assert_eq!(trn(Lang::FR, 3, "{n} day ago", "{n} days ago"), "il y a 3 jours");
}
#[test]
fn simplified_chinese_locales_respect_the_script() {
for tag in ["zh", "zh-CN", "zh_CN.UTF-8", "zh-SG", "zh-Hans", "zh-Hans-CN", "zh-Hans-HK", "ZH_cn"] {
+129
View File
@@ -0,0 +1,129 @@
//! The French 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>>,
}
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: Os::MacOs, arch: Arch::Arm64 })
}
fn now(&self) -> i64 {
self.clock.get()
}
}
struct NoHost;
impl Host for NoHost {}
fn release(id: &str, v: &str, at: i64) -> Release {
let name = format!("{id}-{v}-macos-universal.dmg");
Release {
tag: format!("v{v}"),
version: version::parse(v),
name: format!("v{v}"),
published_at: Some(at),
html_url: format!("https://github.com/storytold/{id}/releases/tag/v{v}"),
notes: 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>> {
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)) });
let settings =
Settings { view, notify: false, language: "fr".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 NoHost);
l.ui(ui, &mut NoHost);
}
},
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 french_ui_translates_labels_dates_and_messages() {
let mut h = harness(ViewMode::Grid);
h.run_steps(8);
snapshot(&mut h, "grid-fr-dark");
// Product names stay in English.
h.get_by_label("Launcher");
h.get_by_label("Craft App Suite");
h.get_by_label("Retouche d'images");
h.get_by_label("Mettre à jour vers 0.5.0");
h.get_by_label_contains("Vérification de la somme de contrôle…");
h.get_by_label_contains("Vérifié il y a 3 min · via ArtCraft API");
h.get_by_label("LightCraft mis à jour vers 0.5.0");
let mut table = harness(ViewMode::Table);
table.run_steps(8);
snapshot(&mut table, "table-fr-dark");
table.get_by_label("Installée");
assert!(table.query_all_by_label_contains("août 2026").count() >= 1);
}