From 09db3f4c163ebe19fcbd4ed202371ad5f89ef8e6 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 22 Jun 2025 22:15:49 +0200 Subject: [PATCH] l10n: port mv for translation + add french --- src/uu/mv/locales/en-US.ftl | 47 +++++++++++ src/uu/mv/locales/fr-FR.ftl | 63 ++++++++++++++ src/uu/mv/src/error.rs | 28 +++--- src/uu/mv/src/mv.rs | 164 ++++++++++++++++++++++++------------ 4 files changed, 232 insertions(+), 70 deletions(-) create mode 100644 src/uu/mv/locales/fr-FR.ftl diff --git a/src/uu/mv/locales/en-US.ftl b/src/uu/mv/locales/en-US.ftl index 7e8b2bd28e8..07d1155b89b 100644 --- a/src/uu/mv/locales/en-US.ftl +++ b/src/uu/mv/locales/en-US.ftl @@ -14,3 +14,50 @@ mv-after-help = When specifying more than one of -i, -f, -n, only the final one - all This is the default operation when an --update option is not specified, and results in all existing files in the destination being replaced. - none This is similar to the --no-clobber option, in that no files in the destination are replaced, but also skipping a file does not induce a failure. - older This is the default operation when --update is specified, and results in files being replaced if they’re older than the corresponding source file. + +# Error messages +mv-error-no-such-file = cannot stat {$path}: No such file or directory +mv-error-cannot-stat-not-directory = cannot stat {$path}: Not a directory +mv-error-same-file = {$source} and {$target} are the same file +mv-error-self-target-subdirectory = cannot move {$source} to a subdirectory of itself, {$target} +mv-error-directory-to-non-directory = cannot overwrite directory {$path} with non-directory +mv-error-non-directory-to-directory = cannot overwrite non-directory {$target} with directory {$source} +mv-error-not-directory = target {$path}: Not a directory +mv-error-target-not-directory = target directory {$path}: Not a directory +mv-error-failed-access-not-directory = failed to access {$path}: Not a directory +mv-error-backup-with-no-clobber = cannot combine --backup with -n/--no-clobber or --update=none-fail +mv-error-extra-operand = mv: extra operand {$operand} +mv-error-backup-might-destroy-source = backing up {$target} might destroy source; {$source} not moved +mv-error-will-not-overwrite-just-created = will not overwrite just-created '{$target}' with '{$source}' +mv-error-not-replacing = not replacing {$target} +mv-error-cannot-move = cannot move {$source} to {$target} +mv-error-directory-not-empty = Directory not empty +mv-error-dangling-symlink = can't determine symlink type, since it is dangling +mv-error-no-symlink-support = your operating system does not support symlinks +mv-error-permission-denied = Permission denied +mv-error-inter-device-move-failed = inter-device move failed: '{$from}' to '{$to}'; unable to remove target: {$err} + +# Help messages +mv-help-force = do not prompt before overwriting +mv-help-interactive = prompt before override +mv-help-no-clobber = do not overwrite an existing file +mv-help-strip-trailing-slashes = remove any trailing slashes from each SOURCE argument +mv-help-target-directory = move all SOURCE arguments into DIRECTORY +mv-help-no-target-directory = treat DEST as a normal file +mv-help-verbose = explain what is being done +mv-help-progress = Display a progress bar. + Note: this feature is not supported by GNU coreutils. +mv-help-debug = explain how a file is copied. Implies -v + +# Verbose messages +mv-verbose-renamed = renamed {$from} -> {$to} +mv-verbose-renamed-with-backup = renamed {$from} -> {$to} (backup: {$backup}) + +# Debug messages +mv-debug-skipped = skipped {$target} + +# Prompt messages +mv-prompt-overwrite = overwrite {$target}? + +# Progress messages +mv-progress-moving = moving diff --git a/src/uu/mv/locales/fr-FR.ftl b/src/uu/mv/locales/fr-FR.ftl new file mode 100644 index 00000000000..25d82631418 --- /dev/null +++ b/src/uu/mv/locales/fr-FR.ftl @@ -0,0 +1,63 @@ +mv-about = Déplacer SOURCE vers DEST, ou plusieurs SOURCE(s) vers RÉPERTOIRE. +mv-usage = mv [OPTION]... [-T] SOURCE DEST + mv [OPTION]... SOURCE... RÉPERTOIRE + mv [OPTION]... -t RÉPERTOIRE SOURCE... +mv-after-help = Lors de la spécification de plus d'une option parmi -i, -f, -n, seule la dernière prend effet. + + Ne pas déplacer un non-répertoire qui a une destination existante avec un horodatage de modification identique ou plus récent ; + au lieu de cela, ignorer silencieusement le fichier sans échouer. Si le déplacement traverse les limites du système de fichiers, la comparaison est + avec l'horodatage source tronqué aux résolutions du système de fichiers de destination et des appels système utilisés + pour mettre à jour les horodatages ; cela évite le travail en double si plusieurs commandes mv -u sont exécutées avec la même source + et destination. Cette option est ignorée si l'option -n ou --no-clobber est également spécifiée, qui donne plus de contrôle + sur quels fichiers existants dans la destination sont remplacés, et sa valeur peut être une des suivantes : + + - all C'est l'opération par défaut quand une option --update n'est pas spécifiée, et résulte en tous les fichiers existants dans la destination étant remplacés. + - none C'est similaire à l'option --no-clobber, en ce que aucun fichier dans la destination n'est remplacé, mais aussi ignorer un fichier n'induit pas un échec. + - older C'est l'opération par défaut quand --update est spécifié, et résulte en des fichiers étant remplacés s'ils sont plus anciens que le fichier source correspondant. + +# Messages d'erreur +mv-error-no-such-file = impossible de lire {$path} : Aucun fichier ou répertoire de ce nom +mv-error-cannot-stat-not-directory = impossible de lire {$path} : N'est pas un répertoire +mv-error-same-file = {$source} et {$target} sont le même fichier +mv-error-self-target-subdirectory = impossible de déplacer {$source} vers un sous-répertoire de lui-même, {$target} +mv-error-directory-to-non-directory = impossible d'écraser le répertoire {$path} avec un non-répertoire +mv-error-non-directory-to-directory = impossible d'écraser le non-répertoire {$target} avec le répertoire {$source} +mv-error-not-directory = cible {$path} : N'est pas un répertoire +mv-error-target-not-directory = répertoire cible {$path} : N'est pas un répertoire +mv-error-failed-access-not-directory = impossible d'accéder à {$path} : N'est pas un répertoire +mv-error-backup-with-no-clobber = impossible de combiner --backup avec -n/--no-clobber ou --update=none-fail +mv-error-extra-operand = mv : opérande supplémentaire {$operand} +mv-error-backup-might-destroy-source = sauvegarder {$target} pourrait détruire la source ; {$source} non déplacé +mv-error-will-not-overwrite-just-created = ne va pas écraser le fichier qui vient d'être créé '{$target}' avec '{$source}' +mv-error-not-replacing = ne remplace pas {$target} +mv-error-cannot-move = impossible de déplacer {$source} vers {$target} +mv-error-directory-not-empty = Répertoire non vide +mv-error-dangling-symlink = impossible de déterminer le type de lien symbolique, car il est suspendu +mv-error-no-symlink-support = votre système d'exploitation ne prend pas en charge les liens symboliques +mv-error-permission-denied = Permission refusée +mv-error-inter-device-move-failed = échec du déplacement inter-périphérique : '{$from}' vers '{$to}' ; impossible de supprimer la cible : {$err} + +# Messages d'aide +mv-help-force = ne pas demander avant d'écraser +mv-help-interactive = demander avant d'écraser +mv-help-no-clobber = ne pas écraser un fichier existant +mv-help-strip-trailing-slashes = supprimer toutes les barres obliques de fin de chaque argument SOURCE +mv-help-target-directory = déplacer tous les arguments SOURCE dans RÉPERTOIRE +mv-help-no-target-directory = traiter DEST comme un fichier normal +mv-help-verbose = expliquer ce qui est fait +mv-help-progress = Afficher une barre de progression. + Note : cette fonctionnalité n'est pas supportée par GNU coreutils. +mv-help-debug = expliquer comment un fichier est copié. Implique -v + +# Messages verbeux +mv-verbose-renamed = renommé {$from} -> {$to} +mv-verbose-renamed-with-backup = renommé {$from} -> {$to} (sauvegarde : {$backup}) + +# Messages de débogage +mv-debug-skipped = ignoré {$target} + +# Messages de confirmation +mv-prompt-overwrite = écraser {$target} ? + +# Messages de progression +mv-progress-moving = déplacement diff --git a/src/uu/mv/src/error.rs b/src/uu/mv/src/error.rs index 5049725a67e..e365c3666ce 100644 --- a/src/uu/mv/src/error.rs +++ b/src/uu/mv/src/error.rs @@ -2,36 +2,30 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +use std::collections::HashMap; use thiserror::Error; use uucore::error::UError; +use uucore::locale::get_message_with_args; #[derive(Debug, Error)] pub enum MvError { - #[error("cannot stat {0}: No such file or directory")] + #[error("{}", get_message_with_args("mv-error-no-such-file", HashMap::from([("path".to_string(), .0.clone())])))] NoSuchFile(String), - - #[error("cannot stat {0}: Not a directory")] + #[error("{}", get_message_with_args("mv-error-cannot-stat-not-directory", HashMap::from([("path".to_string(), .0.clone())])))] CannotStatNotADirectory(String), - - #[error("{0} and {1} are the same file")] + #[error("{}", get_message_with_args("mv-error-same-file", HashMap::from([("source".to_string(), .0.clone()), ("target".to_string(), .1.clone())])))] SameFile(String, String), - - #[error("cannot move {0} to a subdirectory of itself, {1}")] + #[error("{}", get_message_with_args("mv-error-self-target-subdirectory", HashMap::from([("source".to_string(), .0.clone()), ("target".to_string(), .1.clone())])))] SelfTargetSubdirectory(String, String), - - #[error("cannot overwrite directory {0} with non-directory")] + #[error("{}", get_message_with_args("mv-error-directory-to-non-directory", HashMap::from([("path".to_string(), .0.clone())])))] DirectoryToNonDirectory(String), - - #[error("cannot overwrite non-directory {1} with directory {0}")] + #[error("{}", get_message_with_args("mv-error-non-directory-to-directory", HashMap::from([("source".to_string(), .0.clone()), ("target".to_string(), .1.clone())])))] NonDirectoryToDirectory(String, String), - - #[error("target {0}: Not a directory")] + #[error("{}", get_message_with_args("mv-error-not-directory", HashMap::from([("path".to_string(), .0.clone())])))] NotADirectory(String), - - #[error("target directory {0}: Not a directory")] + #[error("{}", get_message_with_args("mv-error-target-not-directory", HashMap::from([("path".to_string(), .0.clone())])))] TargetNotADirectory(String), - - #[error("failed to access {0}: Not a directory")] + #[error("{}", get_message_with_args("mv-error-failed-access-not-directory", HashMap::from([("path".to_string(), .0.clone())])))] FailedToAccessNotADirectory(String), } diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index 82af08b051b..38ad150c74b 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -11,7 +11,7 @@ use clap::builder::ValueParser; use clap::{Arg, ArgAction, ArgMatches, Command, error::ErrorKind}; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::env; use std::ffi::OsString; use std::fs; @@ -48,7 +48,7 @@ use fs_extra::dir::{ }; use crate::error::MvError; -use uucore::locale::get_message; +use uucore::locale::{get_message, get_message_with_args}; /// Options contains all the possible behaviors and flags for mv. /// @@ -167,7 +167,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { { return Err(UUsageError::new( 1, - "cannot combine --backup with -n/--no-clobber or --update=none-fail", + get_message("mv-error-backup-with-no-clobber"), )); } @@ -214,7 +214,7 @@ pub fn uu_app() -> Command { Arg::new(OPT_FORCE) .short('f') .long(OPT_FORCE) - .help("do not prompt before overwriting") + .help(get_message("mv-help-force")) .overrides_with_all([OPT_INTERACTIVE, OPT_NO_CLOBBER]) .action(ArgAction::SetTrue), ) @@ -222,7 +222,7 @@ pub fn uu_app() -> Command { Arg::new(OPT_INTERACTIVE) .short('i') .long(OPT_INTERACTIVE) - .help("prompt before override") + .help(get_message("mv-help-interactive")) .overrides_with_all([OPT_FORCE, OPT_NO_CLOBBER]) .action(ArgAction::SetTrue), ) @@ -230,14 +230,14 @@ pub fn uu_app() -> Command { Arg::new(OPT_NO_CLOBBER) .short('n') .long(OPT_NO_CLOBBER) - .help("do not overwrite an existing file") + .help(get_message("mv-help-no-clobber")) .overrides_with_all([OPT_FORCE, OPT_INTERACTIVE]) .action(ArgAction::SetTrue), ) .arg( Arg::new(OPT_STRIP_TRAILING_SLASHES) .long(OPT_STRIP_TRAILING_SLASHES) - .help("remove any trailing slashes from each SOURCE argument") + .help(get_message("mv-help-strip-trailing-slashes")) .action(ArgAction::SetTrue), ) .arg(backup_control::arguments::backup()) @@ -249,7 +249,7 @@ pub fn uu_app() -> Command { Arg::new(OPT_TARGET_DIRECTORY) .short('t') .long(OPT_TARGET_DIRECTORY) - .help("move all SOURCE arguments into DIRECTORY") + .help(get_message("mv-help-target-directory")) .value_name("DIRECTORY") .value_hint(clap::ValueHint::DirPath) .conflicts_with(OPT_NO_TARGET_DIRECTORY) @@ -259,24 +259,21 @@ pub fn uu_app() -> Command { Arg::new(OPT_NO_TARGET_DIRECTORY) .short('T') .long(OPT_NO_TARGET_DIRECTORY) - .help("treat DEST as a normal file") + .help(get_message("mv-help-no-target-directory")) .action(ArgAction::SetTrue), ) .arg( Arg::new(OPT_VERBOSE) .short('v') .long(OPT_VERBOSE) - .help("explain what is being done") + .help(get_message("mv-help-verbose")) .action(ArgAction::SetTrue), ) .arg( Arg::new(OPT_PROGRESS) .short('g') .long(OPT_PROGRESS) - .help( - "Display a progress bar. \n\ - Note: this feature is not supported by GNU coreutils.", - ) + .help(get_message("mv-help-progress")) .action(ArgAction::SetTrue), ) .arg( @@ -290,7 +287,7 @@ pub fn uu_app() -> Command { .arg( Arg::new(OPT_DEBUG) .long(OPT_DEBUG) - .help("explain how a file is copied. Implies -v") + .help(get_message("mv-help-debug")) .action(ArgAction::SetTrue), ) } @@ -326,10 +323,12 @@ fn handle_two_paths(source: &Path, target: &Path, opts: &Options) -> UResult<()> if opts.backup == BackupMode::Simple && source_is_target_backup(source, target, &opts.suffix) { return Err(io::Error::new( io::ErrorKind::NotFound, - format!( - "backing up {} might destroy source; {} not moved", - target.quote(), - source.quote() + get_message_with_args( + "mv-error-backup-might-destroy-source", + HashMap::from([ + ("target".to_string(), target.quote().to_string()), + ("source".to_string(), source.quote().to_string()), + ]), ), ) .into()); @@ -359,7 +358,13 @@ fn handle_two_paths(source: &Path, target: &Path, opts: &Options) -> UResult<()> if opts.no_target_dir { if source.is_dir() { rename(source, target, opts, None).map_err_context(|| { - format!("cannot move {} to {}", source.quote(), target.quote()) + get_message_with_args( + "mv-error-cannot-move", + HashMap::from([ + ("source".to_string(), source.quote().to_string()), + ("target".to_string(), target.quote().to_string()), + ]), + ) }) } else { Err(MvError::DirectoryToNonDirectory(target.quote().to_string()).into()) @@ -371,7 +376,13 @@ fn handle_two_paths(source: &Path, target: &Path, opts: &Options) -> UResult<()> match opts.overwrite { OverwriteMode::NoClobber => return Ok(()), OverwriteMode::Interactive => { - if !prompt_yes!("overwrite {}? ", target.quote()) { + if !prompt_yes!( + "{}", + get_message_with_args( + "mv-prompt-overwrite", + HashMap::from([("target".to_string(), target.quote().to_string())]), + ) + ) { return Err(io::Error::other("").into()); } } @@ -473,7 +484,10 @@ fn handle_multiple_paths(paths: &[PathBuf], opts: &Options) -> UResult<()> { if opts.no_target_dir { return Err(UUsageError::new( 1, - format!("mv: extra operand {}", paths[2].quote()), + get_message_with_args( + "mv-error-extra-operand", + HashMap::from([("operand".to_string(), paths[2].quote().to_string())]), + ), )); } let target_dir = paths.last().unwrap(); @@ -511,11 +525,17 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, options: &Options) let count_progress = if let Some(ref multi_progress) = multi_progress { if files.len() > 1 { - Some(multi_progress.add( - ProgressBar::new(files.len().try_into().unwrap()).with_style( - ProgressStyle::with_template("moving {msg} {wide_bar} {pos}/{len}").unwrap(), + Some( + multi_progress.add( + ProgressBar::new(files.len().try_into().unwrap()).with_style( + ProgressStyle::with_template(&format!( + "{} {{msg}} {{wide_bar}} {{pos}}/{{len}}", + get_message("mv-progress-moving") + )) + .unwrap(), + ), ), - )) + ) } else { None } @@ -545,10 +565,12 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, options: &Options) // If the target file was already created in this mv call, do not overwrite show!(USimpleError::new( 1, - format!( - "will not overwrite just-created '{}' with '{}'", - targetpath.display(), - sourcepath.display() + get_message_with_args( + "mv-error-will-not-overwrite-just-created", + HashMap::from([ + ("target".to_string(), targetpath.display().to_string()), + ("source".to_string(), sourcepath.display().to_string()) + ]) ), )); continue; @@ -565,10 +587,12 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, options: &Options) Err(e) if e.to_string().is_empty() => set_exit_code(1), Err(e) => { let e = e.map_err_context(|| { - format!( - "cannot move {} to {}", - sourcepath.quote(), - targetpath.quote() + get_message_with_args( + "mv-error-cannot-move", + HashMap::from([ + ("source".to_string(), sourcepath.quote().to_string()), + ("target".to_string(), targetpath.quote().to_string()), + ]), ) }); match multi_progress { @@ -597,7 +621,13 @@ fn rename( if to.exists() { if opts.update == UpdateMode::None { if opts.debug { - println!("skipped {}", to.quote()); + println!( + "{}", + get_message_with_args( + "mv-debug-skipped", + HashMap::from([("target".to_string(), to.quote().to_string())]) + ) + ); } return Ok(()); } @@ -609,19 +639,34 @@ fn rename( } if opts.update == UpdateMode::NoneFail { - let err_msg = format!("not replacing {}", to.quote()); + let err_msg = get_message_with_args( + "mv-error-not-replacing", + HashMap::from([("target".to_string(), to.quote().to_string())]), + ); return Err(io::Error::other(err_msg)); } match opts.overwrite { OverwriteMode::NoClobber => { if opts.debug { - println!("skipped {}", to.quote()); + println!( + "{}", + get_message_with_args( + "mv-debug-skipped", + HashMap::from([("target".to_string(), to.quote().to_string())]) + ) + ); } return Ok(()); } OverwriteMode::Interactive => { - if !prompt_yes!("overwrite {}?", to.quote()) { + if !prompt_yes!( + "{}", + get_message_with_args( + "mv-prompt-overwrite", + HashMap::from([("target".to_string(), to.quote().to_string())]), + ) + ) { return Err(io::Error::other("")); } } @@ -641,7 +686,9 @@ fn rename( if is_empty_dir(to) { fs::remove_dir(to)?; } else { - return Err(io::Error::other("Directory not empty")); + return Err(io::Error::other(get_message( + "mv-error-directory-not-empty", + ))); } } } @@ -650,13 +697,21 @@ fn rename( if opts.verbose { let message = match backup_path { - Some(path) => format!( - "renamed {} -> {} (backup: {})", - from.quote(), - to.quote(), - path.quote() + Some(path) => get_message_with_args( + "mv-verbose-renamed-with-backup", + HashMap::from([ + ("from".to_string(), from.quote().to_string()), + ("to".to_string(), to.quote().to_string()), + ("backup".to_string(), path.quote().to_string()), + ]), + ), + None => get_message_with_args( + "mv-verbose-renamed", + HashMap::from([ + ("from".to_string(), from.quote().to_string()), + ("to".to_string(), to.quote().to_string()), + ]), ), - None => format!("renamed {} -> {}", from.quote(), to.quote()), }; match multi_progress { @@ -751,7 +806,7 @@ fn rename_symlink_fallback(from: &Path, to: &Path) -> io::Result<()> { } else { Err(io::Error::new( io::ErrorKind::NotFound, - "can't determine symlink type, since it is dangling", + get_message("mv-error-dangling-symlink"), )) } } @@ -761,7 +816,7 @@ fn rename_symlink_fallback(from: &Path, to: &Path) -> io::Result<()> { let path_symlink_points_to = fs::read_link(from)?; Err(io::Error::new( io::ErrorKind::Other, - "your operating system does not support symlinks", + get_message("mv-error-no-symlink-support"), )) } @@ -802,8 +857,7 @@ fn rename_dir_fallback( }; #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))] - let xattrs = - fsxattr::retrieve_xattrs(from).unwrap_or_else(|_| std::collections::HashMap::new()); + let xattrs = fsxattr::retrieve_xattrs(from).unwrap_or_else(|_| HashMap::new()); let result = if let Some(ref pb) = progress_bar { move_dir_with_progress(from, to, &options, |process_info: TransitProcess| { @@ -822,7 +876,7 @@ fn rename_dir_fallback( Err(err) => match err.kind { fs_extra::error::ErrorKind::PermissionDenied => Err(io::Error::new( io::ErrorKind::PermissionDenied, - "Permission denied", + get_message("mv-error-permission-denied"), )), _ => Err(io::Error::other(format!("{err:?}"))), }, @@ -837,9 +891,13 @@ fn rename_file_fallback(from: &Path, to: &Path) -> io::Result<()> { let from = from.to_string_lossy(); io::Error::new( err.kind(), - format!( - "inter-device move failed: '{from}' to '{to}'\ - ; unable to remove target: {err}" + get_message_with_args( + "mv-error-inter-device-move-failed", + HashMap::from([ + ("from".to_string(), from.to_string()), + ("to".to_string(), to.to_string()), + ("err".to_string(), err.to_string()), + ]), ), ) })?;