Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions po/es.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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…"
Expand Down Expand Up @@ -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"
Expand Down
57 changes: 46 additions & 11 deletions src/ui/folder_status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand All @@ -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
}
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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");
Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down
31 changes: 20 additions & 11 deletions src/ui/main_window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<dyn Fn(&str) + Send + 'static> = Box::new({
Expand Down Expand Up @@ -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()
};
Expand Down Expand Up @@ -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(&gtk4::Image::from_icon_name("folder-symbolic"));
group_row.set_enable_expansion(false);
Expand All @@ -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()
Expand Down Expand Up @@ -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();
});
}
Expand Down
13 changes: 13 additions & 0 deletions src/util/i18n.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion src/util/translations/es.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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"),
Expand All @@ -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"),
Expand Down Expand Up @@ -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…"),
Expand Down
Loading