diff --git a/po/es.po b/po/es.po index 3e1e5a9..7eeb101 100644 --- a/po/es.po +++ b/po/es.po @@ -960,6 +960,10 @@ msgstr "Revisar ahora" msgid "Synchronization was paused before {count} files could be deleted from Nextcloud." msgstr "La sincronización se pausó antes de que {count} archivos pudieran eliminarse de Nextcloud." +#: src/ui/main_window.rs +msgid "Synchronization was paused before {count} file could be deleted from Nextcloud." +msgstr "La sincronización se pausó antes de que {count} archivo pudiera eliminarse de Nextcloud." + #: src/ui/folder_status.rs msgid "Review deletions" msgstr "Revisar borrados masivos" @@ -2659,14 +2663,26 @@ msgstr "Sincronización bloqueada: no hay credenciales guardadas" msgid "{action} · {count} files" msgstr "{action} · {count} archivos" +#: src/ui/folder_status.rs +msgid "{action} · {count} file" +msgstr "{action} · {count} archivo" + #: src/ui/main_window.rs msgid "{count} files disappeared from the local folder." msgstr "{count} archivos desaparecieron de la carpeta local." +#: src/ui/main_window.rs +msgid "{count} file disappeared from the local folder." +msgstr "Desapareció {count} archivo de la carpeta local." + #: src/ui/main_window.rs msgid "{count} files" msgstr "{count} archivos" +#: src/ui/main_window.rs +msgid "{count} file" +msgstr "{count} archivo" + #: src/ui/main_window.rs msgid "{count} more…" msgstr "{count} más…" @@ -2719,8 +2735,8 @@ msgstr "Versión del servidor conservada en {count} archivo(s)" msgid "Resolve All Conflicts" msgstr "Resolver todos los conflictos" -msgid "Synced in local {folder}" -msgstr "Sincronizado en local {folder}" +msgid "Local copy of {folder}" +msgstr "Copia local de {folder}" msgid "Waiting to synchronize" msgstr "A la espera" diff --git a/src/ui/folder_status.rs b/src/ui/folder_status.rs index ac12f76..ab086a8 100644 --- a/src/ui/folder_status.rs +++ b/src/ui/folder_status.rs @@ -17,7 +17,7 @@ use libadwaita::prelude::*; use crate::core::account_runtime::FolderRuntime; use crate::state::{AppState, StateController, StateSnapshot}; use crate::storage::config::FolderConfig; -use crate::util::i18n::t; +use crate::util::i18n::{plural, t}; /// The `(icon_name, status_label)` pair for a folder state, mirroring the /// Python `STATE_PRESENTATION` table. The label is translated through @@ -59,7 +59,7 @@ pub fn folder_status_color(state: AppState) -> Option<&'static str> { /// slash: the name is what matters, the slash is noise. pub fn remote_label(remote_path: &str) -> String { let folder = remote_path.trim_start_matches('/'); - t("Synced in local {folder}").replace("{folder}", folder) + t("Local copy of {folder}").replace("{folder}", folder) } /// The translated suffix for the folder's local used space ("{size} local", @@ -85,9 +85,13 @@ pub fn progress_line_text( } .replace("{file}", file); if progress.processed > 0 { - t("{action} · {count} files") - .replace("{action}", &action) - .replace("{count}", &progress.processed.to_string()) + plural( + u64::from(progress.processed), + t("{action} · {count} file"), + t("{action} · {count} files"), + ) + .replace("{action}", &action) + .replace("{count}", &progress.processed.to_string()) } else { action } @@ -269,10 +273,16 @@ impl FolderStatusRow { // operations done, never a total), gone when it ends. The row, the // bar and the progress caption share a vertical slot that the folder // list appends. + // Issue #248: the bar keeps its 3 px height but is inset from the card + // corners and separated from the row border, which used to disappear + // under it while a run was in flight. let progress_bar = gtk4::ProgressBar::builder() .hexpand(true) .valign(gtk4::Align::End) .height_request(3) + .margin_start(12) + .margin_end(12) + .margin_top(2) .visible(false) .build(); progress_bar.add_css_class("nextsync-run-bar"); @@ -569,7 +579,7 @@ fn render( progress_bar.pulse(); } // The synced-in-local segment belongs to the synchronized state only and - // already says "Synced in local {folder}", so the redundant "Synchronized" + // already says "Local copy of {folder}", so the redundant "Synchronized" // status is dropped there (issue #108): the green check already conveys it. // Attention states surface the scheduler's specific message (which says // what the attention is and what to do) instead of a generic label (#95). @@ -689,8 +699,8 @@ mod tests { assert_eq!(label, "Sincronizado"); let (_, label) = folder_status_presentation(AppState::Offline); assert_eq!(label, "Sin conexión"); - assert_eq!(remote_label("/docs"), "Sincronizado en local docs"); - assert_eq!(remote_label("docs"), "Sincronizado en local docs"); + assert_eq!(remote_label("/docs"), "Copia local de docs"); + assert_eq!(remote_label("docs"), "Copia local de docs"); // Live progress line (issue #86): translated action plus counter. let mut progress = crate::nextcloud::nextcloudcmd_progress::SyncProgress::new("upload", "/tmp/a/b.txt"); @@ -703,6 +713,31 @@ mod tests { reset_locale(); } + /// Issue #247: a single file must not read as a plural. + #[test] + fn progress_counter_uses_the_singular_for_one_file() { + set_locale(Locale::English); + let mut progress = + crate::nextcloud::nextcloudcmd_progress::SyncProgress::new("upload", "/tmp/a/b.txt"); + progress.processed = 1; + assert_eq!( + progress_line_text(&progress, "b.txt"), + "uploading b.txt · 1 file" + ); + progress.processed = 2; + assert_eq!( + progress_line_text(&progress, "b.txt"), + "uploading b.txt · 2 files" + ); + set_locale(Locale::Spanish); + progress.processed = 1; + assert_eq!( + progress_line_text(&progress, "b.txt"), + "subiendo b.txt · 1 archivo" + ); + reset_locale(); + } + #[test] fn sync_stamp_formats_iso_dates_locally() { set_locale(Locale::English); @@ -825,14 +860,14 @@ mod tests { None, ); assert_eq!(row.row.title(), "a"); - assert_eq!(row.row.subtitle().as_deref(), Some("Synced in local docs")); + assert_eq!(row.row.subtitle().as_deref(), Some("Local copy of docs")); // The local-size suffix stays hidden until a size arrives and // leaves the subtitle alone (issue #43). assert!(!row.local_size.is_visible()); row.set_local_size("12.4 KiB local"); assert_eq!(row.local_size.label(), "12.4 KiB local"); assert!(row.local_size.is_visible()); - assert_eq!(row.row.subtitle().as_deref(), Some("Synced in local docs")); + assert_eq!(row.row.subtitle().as_deref(), Some("Local copy of docs")); row.set_local_size(""); assert!(!row.local_size.is_visible()); // Outside the synchronized state the synced-in-local segment @@ -853,7 +888,7 @@ mod tests { Some("Waiting for another folder to finish…") ); state.set(AppState::IdleOk, "ok"); - assert_eq!(row.row.subtitle().as_deref(), Some("Synced in local docs")); + assert_eq!(row.row.subtitle().as_deref(), Some("Local copy of docs")); // Live progress (issue #86): a progress event shows the line, // clearing it hides the label again. state.set_progress(Some(crate::nextcloud::sync_engine::SyncProgress { diff --git a/src/ui/main_window.rs b/src/ui/main_window.rs index 9c45d88..fb15875 100644 --- a/src/ui/main_window.rs +++ b/src/ui/main_window.rs @@ -25,7 +25,7 @@ use crate::ui::settings::{ page as settings_page, present_add_folder_dialog, ExclusionsDialog, SettingsCallbacks, SettingsHost, SettingsView, }; -use crate::util::i18n::t; +use crate::util::i18n::{plural, t}; /// Translated window title (also used as a machine-readable page name). pub fn window_title() -> &'static str { @@ -931,8 +931,10 @@ impl MainWindow { }; let count = alert.missing_paths.len(); let summary = t("Review Deletions").to_string(); - let body = t( - "Synchronization was paused before {count} files could be deleted from Nextcloud.", + let body = plural( + count as u64, + t("Synchronization was paused before {count} file could be deleted from Nextcloud."), + t("Synchronization was paused before {count} files could be deleted from Nextcloud."), ) .replace("{count}", &count.to_string()); let on_review: Box = Box::new({ @@ -1371,8 +1373,12 @@ impl MainWindow { let missing_len = missing.len(); let body = if missing_len > 0 { - t("{count} files disappeared from the local folder.") - .replace("{count}", &missing_len.to_string()) + plural( + missing_len as u64, + t("{count} file disappeared from the local folder."), + t("{count} files disappeared from the local folder."), + ) + .replace("{count}", &missing_len.to_string()) } else { alert.message.clone() }; @@ -1408,7 +1414,10 @@ impl MainWindow { } => { let group_row = libadwaita::ExpanderRow::builder() .title(prefix) - .subtitle(t("{count} files").replace("{count}", &count.to_string())) + .subtitle( + plural(*count as u64, t("{count} file"), t("{count} files")) + .replace("{count}", &count.to_string()), + ) .build(); group_row.add_prefix(>k4::Image::from_icon_name("folder-symbolic")); group_row.set_enable_expansion(false); @@ -1425,7 +1434,10 @@ impl MainWindow { if loose_count > 0 { let loose_row = libadwaita::ExpanderRow::builder() .title(t("At the top level")) - .subtitle(t("{count} files").replace("{count}", &loose_count.to_string())) + .subtitle( + plural(loose_count as u64, t("{count} file"), t("{count} files")) + .replace("{count}", &loose_count.to_string()), + ) .build(); for path in &loose_preview { let child = libadwaita::ActionRow::builder() @@ -2881,10 +2893,7 @@ mod tests { state.set(crate::state::AppState::Syncing, ""); assert_eq!(action_row.subtitle().as_deref(), Some("Synchronizing…")); state.set(crate::state::AppState::IdleOk, ""); - assert_eq!( - action_row.subtitle().as_deref(), - Some("Synced in local docs") - ); + assert_eq!(action_row.subtitle().as_deref(), Some("Local copy of docs")); reset_locale(); }); } diff --git a/src/util/i18n.rs b/src/util/i18n.rs index d665ff0..7898791 100644 --- a/src/util/i18n.rs +++ b/src/util/i18n.rs @@ -66,6 +66,19 @@ pub fn t(msgid: &str) -> &str { } } +/// Pick the singular or plural form of a counted message (issue #247). +/// +/// The catalog is a flat map with no plural rules, so every counted message +/// ships two English msgids and their two translations and the caller passes +/// both already resolved through [`t`]. +pub fn plural(count: u64, one: &'static str, many: &'static str) -> &'static str { + if count == 1 { + one + } else { + many + } +} + /// Binary-search the sorted Spanish catalog. fn lookup_spanish(msgid: &str) -> Option<&str> { es::CATALOG diff --git a/src/util/translations/es.rs b/src/util/translations/es.rs index 3f30ff0..267bac1 100644 --- a/src/util/translations/es.rs +++ b/src/util/translations/es.rs @@ -203,6 +203,7 @@ pub static CATALOG: &[(&str, &str)] = &[ ("Local Detection", "Detección local"), ("Local Folder", "Carpeta local"), ("Local changes since the last synchronization, compared against the local tree and the synchronization journal. Remote changes are not included.", "Cambios locales desde la última sincronización, comparando el árbol local con el registro de sincronización. Los cambios remotos no se incluyen."), + ("Local copy of {folder}", "Copia local de {folder}"), ("Local folder", "Carpeta local"), ("Local interval (minutes)", "Intervalo local (minutos)"), ("Log", "Registro"), @@ -396,7 +397,6 @@ pub static CATALOG: &[(&str, &str)] = &[ ("Sync Now", "Sincronizar ahora"), ("Sync Once", "Sincronizar una vez"), ("Sync provider", "Proveedor de sincronización"), - ("Synced in local {folder}", "Sincronizado en local {folder}"), ("Synchronization", "Sincronización"), ("Synchronization Engine", "Motor de sincronización"), ("Synchronization Error", "Error de sincronización"), @@ -414,6 +414,7 @@ pub static CATALOG: &[(&str, &str)] = &[ ("Synchronization failed: credentials rejected", "Sincronización fallida: credenciales rechazadas"), ("Synchronization is paused", "La sincronización está en pausa"), ("Synchronization is paused on battery", "La sincronización está en pausa por batería"), + ("Synchronization was paused before {count} file could be deleted from Nextcloud.", "La sincronización se pausó antes de que {count} archivo pudiera eliminarse de Nextcloud."), ("Synchronization was paused before {count} files could be deleted from Nextcloud.", "La sincronización se pausó antes de que {count} archivos pudieran eliminarse de Nextcloud."), ("Synchronize this account now", "Sincroniza esta cuenta ahora"), ("Synchronized", "Sincronizado"), @@ -519,9 +520,12 @@ pub static CATALOG: &[(&str, &str)] = &[ ("uploading {file}", "subiendo {file}"), ("{account}: open NextSync to check the account credential.", "{account}: abre NextSync para comprobar la credencial de la cuenta."), ("{action} · {count}", "{action} · {count}"), + ("{action} · {count} file", "{action} · {count} archivo"), ("{action} · {count} files", "{action} · {count} archivos"), ("{count} changes in this release", "{count} cambios en esta versión"), ("{count} conflicted copy(ies) found in {folder}.", "Se encontraron {count} copia(s) en conflicto en {folder}."), + ("{count} file", "{count} archivo"), + ("{count} file disappeared from the local folder.", "Desapareció {count} archivo de la carpeta local."), ("{count} files", "{count} archivos"), ("{count} files disappeared from the local folder.", "{count} archivos desaparecieron de la carpeta local."), ("{count} more…", "{count} más…"),