diff --git a/README.md b/README.md index 5540513..c1f951f 100644 --- a/README.md +++ b/README.md @@ -85,12 +85,13 @@ The web version shows the same catalog in the browser, with the latest download ## Interface languages The interface is in English, Brazilian Portuguese (Português (Brasil)), 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, -French ones (`fr-FR`, `fr-CA`…) French, 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 +(Deutsch), 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. German locales (`de-DE`, `de-AT`, `de-CH`, +`de_DE.UTF-8`…) select German, French ones (`fr-FR`, `fr-CA`…) French, 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. diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv new file mode 100644 index 0000000..e5ff4e3 --- /dev/null +++ b/crates/ui-egui/src/i18n/es.tsv @@ -0,0 +1,307 @@ +# Spanish (es) catalog for ArtCraft Launcher. +# Format: contextsourcetranslation +# Empty context = plain UI string (tl!, t). +# @id = command-id keyed (tr_id). +# @plural = plural forms: one|other (only 1 is "one" in Spanish). +# @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 Spanish for Spain and Latin America (the interface addresses the user as "tú"; +# no regional words such as "ordenador"). +# +# 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", "Finder", "makers". +# Trailing "…" (U+2026) must be preserved. Placeholders must appear in the translation: +# {ago}, {a}, {b}, {count}, {date}, {day}, {done}, {e}, {f}, {installed}, {latest}, {min}, +# {month}, {name}, {n}, {os}, {platform}, {sources}, {total}, {version}, {v}, {year}. + + Launcher Launcher + Craft App Suite Craft App Suite + Open Source Tools for Creatives, Makers, and Builders Herramientas de código abierto para creativos, makers y constructores + Settings Ajustes + Check for updates Buscar actualizaciones + Grid Cuadrícula + Table Tabla + Show apps as cards Mostrar las aplicaciones como tarjetas + Show apps as a table Mostrar las aplicaciones como tabla + All Todas + Updates Actualizaciones + Not installed No instaladas + Search apps Buscar aplicaciones + Sort Ordenar + Suite order Orden de la suite + Name Nombre + Inspired by Inspirada en + Status Estado + Installed version Versión instalada + Latest version Última versión + Newest release Publicación más reciente + Discord Discord + GitHub GitHub + Website Sitio web + Classic apps Aplicaciones clásicas + New apps Aplicaciones nuevas + NEW NUEVA + Install Instalar + Update to {v} Actualizar a {v} + Open Abrir + Download for {os} Descargar para {os} + Download Descargar + Coming soon Próximamente + Checking… Comprobando… + Not available No disponible + Offline Sin conexión + Downloading {done} of {total} Descargando {done} de {total} + Downloading… Descargando… + Reconnecting… Reconectando… + Verifying… Verificando… + Installing, then restarting… Instalando y reiniciando… + Installing… Instalando… + Removing… Quitando… + Pause download Pausar la descarga + Cancel download Cancelar la descarga + Resume download Reanudar la descarga + Discard download Descartar la descarga + Release notes Notas de la versión + Show in Finder Mostrar en el Finder + Show in Explorer Mostrar en el Explorador + Show in Files Mostrar en Archivos + Reinstall Reinstalar + Uninstall… Desinstalar… + All releases Todas las versiones + ArtCraft Launcher {version} is available. ArtCraft Launcher {version} está disponible. + Dismiss Descartar + Update and restart Actualizar y reiniciar + A new version is available. Would you like to relaunch with the updated version? Hay una versión nueva. ¿Quieres reiniciar con la versión actualizada? + Later Más tarde + What's new Novedades + No notes were published with this release. Esta versión no se publicó con notas. + Released {date} Publicada el {date} + Open on GitHub Abrir en GitHub + Close Cerrar + ArtCraft Launcher {version} ArtCraft Launcher {version} + Only when I ask Solo cuando lo pida + Automatically download and install updates Descargar e instalar actualizaciones automáticamente + Notify me when an installed app has an update Avisarme cuando una aplicación instalada tenga una actualización + Include pre-releases (betas and release candidates) Incluir versiones preliminares (betas y versiones candidatas) + Installing on Linux Instalación en Linux + Install new apps as Instalar las aplicaciones nuevas como + System package (recommended) Paquete del sistema (recomendado) + Updates always use the format an app was installed with. Las actualizaciones siempre usan el formato con el que se instaló cada aplicación. + Running in the background En segundo plano + Keep running in the tray when the window is closed Seguir abierto en la bandeja del sistema al cerrar la ventana + Show the window on the current desktop Mostrar la ventana en el escritorio actual + Start ArtCraft Launcher when I log in Abrir ArtCraft Launcher al iniciar sesión + Community Comunidad + Questions, ideas and news from the ArtCraft team Preguntas, ideas y noticias del equipo de ArtCraft + Appearance Apariencia + Quit ArtCraft Launcher Salir de ArtCraft Launcher + Done Listo + Uninstall {name}? ¿Desinstalar {name}? + The app will be moved to the Trash. Your documents and settings stay. La aplicación se moverá a la Papelera. Tus documentos y ajustes se conservan. + Windows will remove it like any installed program. Your documents and settings stay. Windows la quitará como cualquier programa instalado. Tus documentos y ajustes se conservan. + Your system's package manager will remove it, and may ask for your password. Your documents and settings stay. El gestor de paquetes del sistema la quitará y puede pedirte la contraseña. Tus documentos y ajustes se conservan. + The app will be removed. Your documents and settings stay. La aplicación se quitará. Tus documentos y ajustes se conservan. + Uninstall Desinstalar + Cancel Cancelar + Checked {ago} Comprobado {ago} + just now ahora mismo + yesterday ayer + {month} {day}, {year} {day} {month} {year} + Automatically Automáticamente + Every Craft app Todas las aplicaciones Craft + 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 + Checked {ago} · using {sources} Comprobado {ago} · con {sources} + {a} and {b} {a} y {b} + Join the Discord Únete al Discord + Learn more Más información + Minimize Minimizar + Maximize Maximizar + Restore Restaurar + New Nueva + OK Aceptar + Open link Abrir enlace + Update Actualizar + {name} icon Icono de {name} + Loading ArtCraft Launcher… Cargando ArtCraft Launcher… + Cancel update Cancelar la actualización + Stop waiting; the update can be started again later Dejar de esperar; la actualización se puede volver a iniciar más tarde + Download again Volver a descargar + Install cancelled Instalación cancelada + Install cancelled. The download is kept for the next try. Instalación cancelada. La descarga se guarda para el próximo intento. + Update cancelled Actualización cancelada + Uninstall cancelled Desinstalación cancelada + Retry install Reintentar la instalación + Retry update to {version} Reintentar la actualización a {version} + The download is still here. It's checked again, then installed. La descarga sigue aquí. Se vuelve a comprobar y luego se instala. + Waiting for another app… Esperando a otra aplicación… + Waiting for other installs… Esperando a otras instalaciones… + Installer (.msi) Instalador (.msi) + Portable (.zip) Portátil (.zip) + Disk image (.dmg) Imagen de disco (.dmg) + Debian package (.deb) Paquete Debian (.deb) + RPM package (.rpm) Paquete RPM (.rpm) + Flatpak bundle Paquete Flatpak + Tarball (.tar.gz) Archivo tar (.tar.gz) +photocraft Image editing Edición de imágenes +vectorcraft Vector illustration Ilustración vectorial +filmcraft Video editing, color and sound Edición de video, color y sonido +lightcraft Photo library and raw development Fototeca y revelado RAW +pdfcraft Reading, organizing and protecting PDFs Leer, organizar y proteger PDF +effectcraft Motion graphics and visual effects Gráficos en movimiento y efectos visuales +designcraft Page layout and publishing Maquetación y publicación +wordcraft Writing and document design Escritura y diseño de documentos +deckcraft Presentations and slide shows Presentaciones y diapositivas +gridcraft Spreadsheets and data Hojas de cálculo y datos +cadcraft Computer-aided design and drafting CAD: diseño y dibujo técnico +soundcraft Recording, editing and mixing audio Grabación, edición y mezcla de audio +month Jan ene. +month Feb feb. +month Mar mar. +month Apr abr. +month May may. +month Jun jun. +month Jul jul. +month Aug ago. +month Sep sept. +month Oct oct. +month Nov nov. +month Dec dic. + Not checked yet Aún sin comprobar + GitHub's hourly limit was reached; full details return in {min} min Se alcanzó el límite por hora de GitHub; todos los detalles estarán disponibles de nuevo en {min} min + GitHub limit hit; back in {min} min Límite de GitHub alcanzado; disponible de nuevo en {min} min + Installing for {platform} Instalación para {platform} + Downloads for {platform} Descargas para {platform} + No downloads for this system No hay descargas para este sistema + Join the ArtCraft community on Discord Únete a la comunidad de ArtCraft en Discord + ArtCraft on {name} ArtCraft en {name} + Couldn't open {name}: {e} No se pudo abrir {name}: {e} + Download cancelled Descarga cancelada + Available to install Disponible para instalar + Available Disponible + Installed · up to date Instalada · al día + Update available Actualización disponible + Installed Instalada + installed instalada + Not available here No disponible aquí + Updating… Actualizando… + Update paused Actualización en pausa + Download paused Descarga en pausa + {version} installed → {latest} released {version} instalada → {latest} publicada + Version {version} Versión {version} + {installed} installed · latest {latest} {installed} instalada · última {latest} + Version {version} installed Versión {version} instalada + Version {latest} Versión {latest} + No release published yet Aún no hay ninguna versión publicada + ArtCraft Launcher ArtCraft Launcher + App Aplicación + Latest Última + Released Publicada + Auto Automático + Language Idioma + Ascending Ascendente + Descending Descendente + Click to reverse the order Haz clic para invertir el orden + Click to sort by this column Haz clic para ordenar por esta columna + Like {name} Al estilo de {name} + {done} of {total} {done} de {total} + Downloading Descargando + Verifying checksum… Verificando la suma de comprobación… + Resume Reanudar + {done} of {total} downloaded {done} de {total} descargados + {done} downloaded {done} descargados + Checking for updates… Buscando actualizaciones… + Downloads the new version, replaces this one and opens it again Descarga la versión nueva, reemplaza esta y vuelve a abrirla + Couldn't check for updates: {f} No se pudieron buscar actualizaciones: {f} + Website (coming soon) Sitio web (próximamente) + GitHub repository Repositorio en GitHub + ArtCraft ArtCraft + Update 1 app Actualizar 1 aplicación + Update all ({count}) Actualizar todas ({count}) + ⊞ Grid ⊞ Cuadrícula + ☰ Table ☰ Tabla + 🔍 Search apps 🔍 Buscar aplicaciones + {name} {version} {name} {version} + Match system Igual que el sistema + Dark Oscuro + Light Claro + Full changelog Registro de cambios completo + link enlace + No apps match Ninguna aplicación coincide + Open ArtCraft Launcher to update. Abre ArtCraft Launcher para actualizar. + {name} {v} is available {name} {v} está disponible + {count} updates are available Hay {count} actualizaciones disponibles + These notes weren't fetched (GitHub's request limit was reached). They're on the release page. Estas notas no se descargaron (se alcanzó el límite de solicitudes de GitHub). Están en la página de la versión. + 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 está activado, abrir ArtCraft Launcher desde la barra de menús lleva su ventana al escritorio en el que estás. Si está desactivado, macOS te cambia al escritorio en el que estaba la ventana. + Couldn't change the login item: {e} No se pudo cambiar el elemento de inicio de sesión: {e} +column App Aplicación +column Installed Instalada +column Latest Última +column Released Publicada +filter Installed Instaladas +@plural {n} app|{n} apps {n} aplicación|{n} aplicaciones +@plural {n} min ago|{n} min ago hace {n} min|hace {n} min +@plural {n} h ago|{n} h ago hace {n} h|hace {n} h +@plural {n} day ago|{n} days ago hace {n} día|hace {n} días +@msg Couldn't open {name}: {e} No se pudo abrir {name}: {e} +@msg Download cancelled Descarga cancelada +@msg {name} updated to {version} {name} actualizada a {version} +@msg {name} installed: {version} {name} instalada: {version} +@msg {name} updated to {version} (this release publishes no checksum, so only its size was checked) {name} actualizada a {version} (esta versión no publica la suma de comprobación, así que solo se comprobó su tamaño) +@msg {name} installed: {version} (this release publishes no checksum, so only its size was checked) {name} instalada: {version} (esta versión no publica la suma de comprobación, así que solo se comprobó su tamaño) +@msg {name} was removed Se quitó {name} +@msg {name} is running. Quit it, then try again. {name} está abierta. Ciérrala y vuelve a intentarlo. +@id app.install Instalar +@id app.update Actualizar a {v} +@id app.open Abrir +@id app.download Descargar para {os} +@id app.reveal Mostrar en el Finder +@id app.reinstall Reinstalar +@id app.uninstall Desinstalar… +@id app.notes Notas de la versión +@id app.website Sitio web +@id app.github Repositorio en GitHub +@id app.releases Todas las versiones +@id launcher.update Actualizar y reiniciar +@id launcher.download Descargar +@id launcher.notes Novedades +@id launcher.dismiss Descartar +@id settings.open Ajustes +@id settings.quit Salir de ArtCraft Launcher +@id view.grid Cuadrícula +@id view.table Tabla +@id filter.all Todas +@id filter.installed Instaladas +@id filter.updates Actualizaciones +@id filter.notinstalled No instaladas +@id sort.suite Orden de la suite +@id sort.name Nombre +@id sort.inspired Inspirada en +@id sort.status Estado +@id sort.installed Versión instalada +@id sort.latest Última versión +@id sort.released Publicación más reciente +@id help.discord Discord +@id help.github GitHub +@id help.website Sitio web +@id update.check Buscar actualizaciones +@id update.check_auto Buscar actualizaciones automáticamente +@id update.notify Avisarme cuando una aplicación instalada tenga una actualización +@id update.prereleases Incluir versiones preliminares (betas y versiones candidatas) +@id linux.preferred_kind Instalar las aplicaciones nuevas como +@id tray.close_to_tray Seguir abierto en la bandeja del sistema al cerrar la ventana +@id tray.show_on_current_desktop Mostrar la ventana en el escritorio actual +@id tray.login_item Abrir ArtCraft Launcher al iniciar sesión +@id appearance.theme Apariencia + Show ArtCraft Launcher Mostrar ArtCraft Launcher + Check for Updates Buscar actualizaciones + Visit getartcraft.com Visitar getartcraft.com + ArtCraft on GitHub ArtCraft en GitHub + Join the ArtCraft Discord Únete al Discord de ArtCraft +@plural ArtCraft Launcher · {n} update available|ArtCraft Launcher · {n} updates available ArtCraft Launcher · {n} actualización disponible|ArtCraft Launcher · {n} actualizaciones disponibles diff --git a/crates/ui-egui/src/i18n/lang.rs b/crates/ui-egui/src/i18n/lang.rs index dc3bab4..0ebea27 100644 --- a/crates/ui-egui/src/i18n/lang.rs +++ b/crates/ui-egui/src/i18n/lang.rs @@ -18,7 +18,7 @@ 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, + /// Plural form index for a count (English, German and Spanish: 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). @@ -56,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; 6] = [ +pub static LANGUAGES: [LangInfo; 7] = [ LangInfo { code: "en", name: "English", @@ -123,6 +123,17 @@ pub static LANGUAGES: [LangInfo; 6] = [ complete_menus: true, catalog: OnceLock::new(), }, + LangInfo { + code: "es", + name: "Español", + english_name: "Spanish", + cjk: false, + source: include_str!("es.tsv"), + plural: plural_one_other, + plural_forms: 2, + complete_menus: true, + catalog: OnceLock::new(), + }, ]; impl LangInfo { @@ -156,6 +167,7 @@ impl Lang { pub const DE: Lang = Lang(&LANGUAGES[3]); pub const FR: Lang = Lang(&LANGUAGES[4]); pub const PT_BR: Lang = Lang(&LANGUAGES[5]); + pub const ES: Lang = Lang(&LANGUAGES[6]); pub fn code(self) -> &'static str { self.0.code diff --git a/crates/ui-egui/src/i18n/tests.rs b/crates/ui-egui/src/i18n/tests.rs index a6d4b2a..e47b0d5 100644 --- a/crates/ui-egui/src/i18n/tests.rs +++ b/crates/ui-egui/src/i18n/tests.rs @@ -265,6 +265,20 @@ fn french_locales_and_plurals_work() { assert_eq!(trn(Lang::FR, 3, "{n} day ago", "{n} days ago"), "il y a 3 jours"); } +#[test] +fn spanish_locales_and_plurals_work() { + for tag in ["es", "es-ES", "es_MX.UTF-8", "es-419", "es-PA", "ES_ar"] { + assert_eq!(lang_from_tag(tag), Some(Lang::ES), "{tag}"); + } + assert_eq!(super::detect::first_supported(["xx-XX", "es-PA", "en-US"]), Some(Lang::ES)); + // Only 1 is singular in Spanish, as in English. + assert_eq!(trn(Lang::ES, 0, "{n} app", "{n} apps"), "0 aplicaciones"); + assert_eq!(trn(Lang::ES, 1, "{n} app", "{n} apps"), "1 aplicación"); + assert_eq!(trn(Lang::ES, 3, "{n} day ago", "{n} days ago"), "hace 3 días"); + assert_eq!(tr(Lang::ES, "{month} {day}, {year}"), "{day} {month} {year}"); + assert_eq!(message(Lang::ES, "PhotoCraft updated to 0.5.0"), "PhotoCraft actualizada a 0.5.0"); +} + #[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"] { diff --git a/crates/ui-egui/tests/spanish.rs b/crates/ui-egui/tests/spanish.rs new file mode 100644 index 0000000..8f85180 --- /dev/null +++ b/crates/ui-egui/tests/spanish.rs @@ -0,0 +1,129 @@ +//! The Spanish 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=` 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, + clock: Rc>, +} + +impl Backend for Fake { + fn send(&mut self, _command: Command) {} + fn poll(&mut self) -> Vec { + 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 { + 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 { + 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> { + 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: "es".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| { + 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>, 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 spanish_ui_translates_labels_dates_and_messages() { + let mut h = harness(ViewMode::Grid); + h.run_steps(8); + snapshot(&mut h, "grid-es-dark"); + // Product names stay in English. + h.get_by_label("Launcher"); + h.get_by_label("Craft App Suite"); + h.get_by_label("Edición de imágenes"); + h.get_by_label("Actualizar a 0.5.0"); + h.get_by_label_contains("Verificando la suma de comprobación…"); + h.get_by_label_contains("Comprobado hace 3 min · con ArtCraft API"); + h.get_by_label("LightCraft actualizada a 0.5.0"); + + let mut table = harness(ViewMode::Table); + table.run_steps(8); + snapshot(&mut table, "table-es-dark"); + table.get_by_label("Instalada"); + assert!(table.query_all_by_label_contains("ago. 2026").count() >= 1); +}