From 688215725cc2fa3129bf38139f8506c88d469db5 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 15 Jun 2025 18:59:46 +0200 Subject: [PATCH] l10n: port dd for translation + add french --- src/uu/dd/locales/en-US.ftl | 45 +++++++++ src/uu/dd/locales/fr-FR.ftl | 160 +++++++++++++++++++++++++++++++ src/uu/dd/src/dd.rs | 84 ++++++++++++---- src/uu/dd/src/parseargs.rs | 50 ++++++---- src/uu/dd/src/progress.rs | 185 +++++++++++++++++++++++++----------- tests/by-util/test_dd.rs | 6 +- 6 files changed, 435 insertions(+), 95 deletions(-) create mode 100644 src/uu/dd/locales/fr-FR.ftl diff --git a/src/uu/dd/locales/en-US.ftl b/src/uu/dd/locales/en-US.ftl index 56f391c79..8a21f1b59 100644 --- a/src/uu/dd/locales/en-US.ftl +++ b/src/uu/dd/locales/en-US.ftl @@ -113,3 +113,48 @@ dd-after-help = ### Operands - nocache : request that OS drop cache. - noctty : do not assign a controlling tty. - nofollow : do not follow system links. + +# Error messages +dd-error-failed-to-open = failed to open { $path } +dd-error-write-error = write error +dd-error-failed-to-seek = failed to seek in output file +dd-error-io-error = IO error +dd-error-cannot-skip-offset = '{ $file }': cannot skip to specified offset +dd-error-cannot-skip-invalid = '{ $file }': cannot skip: Invalid argument +dd-error-cannot-seek-invalid = '{ $output }': cannot seek: Invalid argument +dd-error-not-directory = setting flags for '{ $file }': Not a directory +dd-error-failed-discard-cache-input = failed to discard cache for: 'standard input' +dd-error-failed-discard-cache-output = failed to discard cache for: 'standard output' + +# Parse errors +dd-error-unrecognized-operand = Unrecognized operand '{ $operand }' +dd-error-multiple-format-table = Only one of conv=ascii conv=ebcdic or conv=ibm may be specified +dd-error-multiple-case = Only one of conv=lcase or conv=ucase may be specified +dd-error-multiple-block = Only one of conv=block or conv=unblock may be specified +dd-error-multiple-excl = Only one ov conv=excl or conv=nocreat may be specified +dd-error-invalid-flag = invalid input flag: ‘{ $flag }’ + Try '{ $cmd } --help' for more information. +dd-error-conv-flag-no-match = Unrecognized conv=CONV -> { $flag } +dd-error-multiplier-parse-failure = invalid number: '{ $input }' +dd-error-multiplier-overflow = Multiplier string would overflow on current system -> { $input } +dd-error-block-without-cbs = conv=block or conv=unblock specified without cbs=N +dd-error-status-not-recognized = status=LEVEL not recognized -> { $level } +dd-error-unimplemented = feature not implemented on this system -> { $feature } +dd-error-bs-out-of-range = { $param }=N cannot fit into memory +dd-error-invalid-number = invalid number: ‘{ $input }’ + +# Progress messages +dd-progress-records-in = { $complete }+{ $partial } records in +dd-progress-records-out = { $complete }+{ $partial } records out +dd-progress-truncated-record = { $count -> + [one] { $count } truncated record + *[other] { $count } truncated records +} +dd-progress-byte-copied = { $bytes } byte copied, { $duration } s, { $rate }/s +dd-progress-bytes-copied = { $bytes } bytes copied, { $duration } s, { $rate }/s +dd-progress-bytes-copied-si = { $bytes } bytes ({ $si }) copied, { $duration } s, { $rate }/s +dd-progress-bytes-copied-si-iec = { $bytes } bytes ({ $si }, { $iec }) copied, { $duration } s, { $rate }/s + +# Warnings +dd-warning-zero-multiplier = { $zero } is a zero multiplier; use { $alternative } if that is intended +dd-warning-signal-handler = Internal dd Warning: Unable to register signal handler diff --git a/src/uu/dd/locales/fr-FR.ftl b/src/uu/dd/locales/fr-FR.ftl new file mode 100644 index 000000000..fb68f809b --- /dev/null +++ b/src/uu/dd/locales/fr-FR.ftl @@ -0,0 +1,160 @@ +dd-about = Copier, et optionnellement convertir, une ressource du système de fichiers +dd-usage = dd [OPÉRANDE]... + dd OPTION +dd-after-help = ### Opérandes + + - bs=OCTETS : lire et écrire jusqu'à OCTETS octets à la fois (par défaut : 512) ; + remplace ibs et obs. + - cbs=OCTETS : la 'taille de bloc de conversion' en octets. S'applique aux + opérations conv=block et conv=unblock. + - conv=CONVS : une liste séparée par des virgules d'options de conversion ou (pour des + raisons historiques) d'indicateurs de fichier. + - count=N : arrêter la lecture de l'entrée après N opérations de lecture de taille ibs + plutôt que de continuer jusqu'à EOF. Voir iflag=count_bytes si l'arrêt après N octets + est préféré + - ibs=N : la taille du tampon utilisé pour les lectures (par défaut : 512) + - if=FICHIER : le fichier utilisé pour l'entrée. Quand non spécifié, stdin est utilisé à la place + - iflag=INDICATEURS : une liste séparée par des virgules d'indicateurs d'entrée qui spécifient comment + la source d'entrée est traitée. INDICATEURS peut être n'importe lequel des indicateurs d'entrée ou + indicateurs généraux spécifiés ci-dessous. + - skip=N (ou iseek=N) : ignorer N enregistrements de taille ibs dans l'entrée avant de commencer + les opérations de copie/conversion. Voir iflag=seek_bytes si la recherche de N octets est préférée. + - obs=N : la taille du tampon utilisé pour les écritures (par défaut : 512) + - of=FICHIER : le fichier utilisé pour la sortie. Quand non spécifié, stdout est utilisé + à la place + - oflag=INDICATEURS : liste séparée par des virgules d'indicateurs de sortie qui spécifient comment la + source de sortie est traitée. INDICATEURS peut être n'importe lequel des indicateurs de sortie ou + indicateurs généraux spécifiés ci-dessous + - seek=N (ou oseek=N) : recherche N enregistrements de taille obs dans la sortie avant de + commencer les opérations de copie/conversion. Voir oflag=seek_bytes si la recherche de N octets est + préférée + - status=NIVEAU : contrôle si les statistiques de volume et de performance sont écrites sur + stderr. + + Quand non spécifié, dd affichera les statistiques à la fin. Un exemple est ci-dessous. + + ```plain + 6+0 enregistrements en entrée + 16+0 enregistrements en sortie + 8192 octets (8.2 kB, 8.0 KiB) copiés, 0.00057009 s, + 14.4 MB/s + + Les deux premières lignes sont les statistiques de 'volume' et la dernière ligne est les + statistiques de 'performance'. + Les statistiques de volume indiquent le nombre de lectures complètes et partielles de taille ibs, + ou d'écritures de taille obs qui ont eu lieu pendant la copie. Le format des statistiques de + volume est +. Si des enregistrements ont été tronqués (voir + conv=block), les statistiques de volume contiendront le nombre d'enregistrements tronqués. + + Les valeurs possibles de NIVEAU sont : + - progress : Afficher les statistiques de performance périodiques pendant la copie. + - noxfer : Afficher les statistiques de volume finales, mais pas les statistiques de performance. + - none : N'afficher aucune statistique. + + L'affichage des statistiques de performance est aussi déclenché par le signal INFO (quand supporté), + ou le signal USR1. Définir la variable d'environnement POSIXLY_CORRECT à n'importe quelle valeur + (y compris une valeur vide) fera ignorer le signal USR1. + + ### Options de conversion + + - ascii : convertir d'EBCDIC vers ASCII. C'est l'inverse de l'option ebcdic. + Implique conv=unblock. + - ebcdic : convertir d'ASCII vers EBCDIC. C'est l'inverse de l'option ascii. + Implique conv=block. + - ibm : convertir d'ASCII vers EBCDIC, en appliquant les conventions pour [, ] + et ~ spécifiées dans POSIX. Implique conv=block. + + - ucase : convertir de minuscules vers majuscules. + - lcase : convertir de majuscules vers minuscules. + + - block : pour chaque nouvelle ligne inférieure à la taille indiquée par cbs=OCTETS, supprimer + la nouvelle ligne et remplir avec des espaces jusqu'à cbs. Les lignes plus longues que cbs sont tronquées. + - unblock : pour chaque bloc d'entrée de la taille indiquée par cbs=OCTETS, supprimer + les espaces de fin à droite et remplacer par un caractère de nouvelle ligne. + + - sparse : tente de rechercher la sortie quand un bloc de taille obs ne contient que + des zéros. + - swab : échange chaque paire d'octets adjacents. Si un nombre impair d'octets est + présent, l'octet final est omis. + - sync : remplit chaque bloc de taille ibs avec des zéros. Si block ou unblock est + spécifié, remplit avec des espaces à la place. + - excl : le fichier de sortie doit être créé. Échoue si le fichier de sortie est déjà + présent. + - nocreat : le fichier de sortie ne sera pas créé. Échoue si le fichier de sortie n'est + pas déjà présent. + - notrunc : le fichier de sortie ne sera pas tronqué. Si cette option n'est pas + présente, la sortie sera tronquée à l'ouverture. + - noerror : toutes les erreurs de lecture seront ignorées. Si cette option n'est pas présente, + dd n'ignorera que Error::Interrupted. + - fdatasync : les données seront écrites avant la fin. + - fsync : les données et les métadonnées seront écrites avant la fin. + + ### Indicateurs d'entrée + + - count_bytes : une valeur pour count=N sera interprétée comme des octets. + - skip_bytes : une valeur pour skip=N sera interprétée comme des octets. + - fullblock : attendre ibs octets de chaque lecture. les lectures de longueur zéro sont toujours + considérées comme EOF. + + ### Indicateurs de sortie + + - append : ouvrir le fichier en mode ajout. Considérez définir conv=notrunc aussi. + - seek_bytes : une valeur pour seek=N sera interprétée comme des octets. + + ### Indicateurs généraux + + - direct : utiliser les E/S directes pour les données. + - directory : échouer sauf si l'entrée donnée (si utilisée comme iflag) ou + la sortie (si utilisée comme oflag) est un répertoire. + - dsync : utiliser les E/S synchronisées pour les données. + - sync : utiliser les E/S synchronisées pour les données et les métadonnées. + - nonblock : utiliser les E/S non-bloquantes. + - noatime : ne pas mettre à jour l'heure d'accès. + - nocache : demander au système d'exploitation de supprimer le cache. + - noctty : ne pas assigner un tty de contrôle. + - nofollow : ne pas suivre les liens système. + +# Error messages +dd-error-failed-to-open = échec de l'ouverture de { $path } +dd-error-write-error = erreur d'écriture +dd-error-failed-to-seek = échec de la recherche dans le fichier de sortie +dd-error-io-error = erreur E/S +dd-error-cannot-skip-offset = '{ $file }' : impossible d'ignorer jusqu'au décalage spécifié +dd-error-cannot-skip-invalid = '{ $file }' : impossible d'ignorer : Argument invalide +dd-error-cannot-seek-invalid = '{ $output }' : impossible de rechercher : Argument invalide +dd-error-not-directory = définir les indicateurs pour '{ $file }' : N'est pas un répertoire +dd-error-failed-discard-cache-input = échec de la suppression du cache pour : 'entrée standard' +dd-error-failed-discard-cache-output = échec de la suppression du cache pour : 'sortie standard' + +# Parse errors +dd-error-unrecognized-operand = Opérande non reconnue '{ $operand }' +dd-error-multiple-format-table = Seul un seul de conv=ascii conv=ebcdic ou conv=ibm peut être spécifié +dd-error-multiple-case = Seul un seul de conv=lcase ou conv=ucase peut être spécifié +dd-error-multiple-block = Seul un seul de conv=block ou conv=unblock peut être spécifié +dd-error-multiple-excl = Seul un seul de conv=excl ou conv=nocreat peut être spécifié +dd-error-invalid-flag = indicateur d'entrée invalide : '{ $flag }' + Essayez '{ $cmd } --help' pour plus d'informations. +dd-error-conv-flag-no-match = conv=CONV non reconnu -> { $flag } +dd-error-multiplier-parse-failure = nombre invalide : ‘{ $input }‘ +dd-error-multiplier-overflow = La chaîne de multiplicateur déborderait sur le système actuel -> { $input } +dd-error-block-without-cbs = conv=block ou conv=unblock spécifié sans cbs=N +dd-error-status-not-recognized = status=NIVEAU non reconnu -> { $level } +dd-error-unimplemented = fonctionnalité non implémentée sur ce système -> { $feature } +dd-error-bs-out-of-range = { $param }=N ne peut pas tenir en mémoire +dd-error-invalid-number = nombre invalide : ‘{ $input }‘ + +# Progress messages +dd-progress-records-in = { $complete }+{ $partial } enregistrements en entrée +dd-progress-records-out = { $complete }+{ $partial } enregistrements en sortie +dd-progress-truncated-record = { $count -> + [one] { $count } enregistrement tronqué + *[other] { $count } enregistrements tronqués +} +dd-progress-byte-copied = { $bytes } octet copié, { $duration } s, { $rate }/s +dd-progress-bytes-copied = { $bytes } octets copiés, { $duration } s, { $rate }/s +dd-progress-bytes-copied-si = { $bytes } octets ({ $si }) copiés, { $duration } s, { $rate }/s +dd-progress-bytes-copied-si-iec = { $bytes } octets ({ $si }, { $iec }) copiés, { $duration } s, { $rate }/s + +# Warnings +dd-warning-zero-multiplier = { $zero } est un multiplicateur zéro ; utilisez { $alternative } si c'est voulu +dd-warning-signal-handler = Avertissement dd interne : Impossible d'enregistrer le gestionnaire de signal diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index 7351ded39..c4261c795 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -26,6 +26,7 @@ use progress::{ProgUpdate, ReadStat, StatusLevel, WriteStat, gen_prog_updater}; use uucore::io::OwnedFileDescriptorOrHandle; use std::cmp; +use std::collections::HashMap; use std::env; use std::ffi::OsString; use std::fs::{File, OpenOptions}; @@ -62,7 +63,7 @@ use uucore::error::{USimpleError, set_exit_code}; use uucore::show_if_err; use uucore::{format_usage, show_error}; -use uucore::locale::get_message; +use uucore::locale::{get_message, get_message_with_args}; const BUF_INIT_BYTE: u8 = 0xDD; /// Final settings after parsing @@ -235,7 +236,13 @@ impl Source { #[cfg(not(unix))] Self::Stdin(stdin) => match io::copy(&mut stdin.take(n), &mut io::sink()) { Ok(m) if m < n => { - show_error!("'standard input': cannot skip to specified offset"); + show_error!( + "{}", + get_message_with_args( + "dd-error-cannot-skip-offset", + HashMap::from([("file".to_string(), "standard input".to_string())]) + ) + ); Ok(m) } Ok(m) => Ok(m), @@ -247,14 +254,26 @@ impl Source { if len < n { // GNU compatibility: // this case prints the stats but sets the exit code to 1 - show_error!("'standard input': cannot skip: Invalid argument"); + show_error!( + "{}", + get_message_with_args( + "dd-error-cannot-skip-invalid", + HashMap::from([("file".to_string(), "standard input".to_string())]) + ) + ); set_exit_code(1); return Ok(len); } } match io::copy(&mut f.take(n), &mut io::sink()) { Ok(m) if m < n => { - show_error!("'standard input': cannot skip to specified offset"); + show_error!( + "{}", + get_message_with_args( + "dd-error-cannot-skip-offset", + HashMap::from([("file".to_string(), "standard input".to_string())]) + ) + ); Ok(m) } Ok(m) => Ok(m), @@ -343,7 +362,10 @@ impl<'a> Input<'a> { if settings.iflags.directory && !f.metadata()?.is_dir() { return Err(USimpleError::new( 1, - "setting flags for 'standard input': Not a directory", + get_message_with_args( + "dd-error-not-directory", + HashMap::from([("file".to_string(), "standard input".to_string())]), + ), )); } }; @@ -364,8 +386,12 @@ impl<'a> Input<'a> { opts.custom_flags(libc_flags); } - opts.open(filename) - .map_err_context(|| format!("failed to open {}", filename.quote()))? + opts.open(filename).map_err_context(|| { + get_message_with_args( + "dd-error-failed-to-open", + HashMap::from([("path".to_string(), filename.quote().to_string())]), + ) + })? }; let mut src = Source::File(src); @@ -457,10 +483,11 @@ impl Input<'_> { fn discard_cache(&self, offset: libc::off_t, len: libc::off_t) { #[cfg(target_os = "linux")] { - show_if_err!(self - .src - .discard_cache(offset, len) - .map_err_context(|| "failed to discard cache for: 'standard input'".to_string())); + show_if_err!( + self.src + .discard_cache(offset, len) + .map_err_context(|| get_message("dd-error-failed-discard-cache-input")) + ); } #[cfg(not(target_os = "linux"))] { @@ -609,7 +636,16 @@ impl Dest { if len < n { // GNU compatibility: // this case prints the stats but sets the exit code to 1 - show_error!("'standard output': cannot seek: Invalid argument"); + show_error!( + "{}", + get_message_with_args( + "dd-error-cannot-seek-invalid", + HashMap::from([( + "output".to_string(), + "standard output".to_string() + )]) + ) + ); set_exit_code(1); return Ok(len); } @@ -723,7 +759,7 @@ impl<'a> Output<'a> { fn new_stdout(settings: &'a Settings) -> UResult { let mut dst = Dest::Stdout(io::stdout()); dst.seek(settings.seek) - .map_err_context(|| "write error".to_string())?; + .map_err_context(|| get_message("dd-error-write-error"))?; Ok(Self { dst, settings }) } @@ -744,8 +780,12 @@ impl<'a> Output<'a> { opts.open(path) } - let dst = open_dst(filename, &settings.oconv, &settings.oflags) - .map_err_context(|| format!("failed to open {}", filename.quote()))?; + let dst = open_dst(filename, &settings.oconv, &settings.oflags).map_err_context(|| { + get_message_with_args( + "dd-error-failed-to-open", + HashMap::from([("path".to_string(), filename.quote().to_string())]), + ) + })?; // Seek to the index in the output file, truncating if requested. // @@ -770,7 +810,7 @@ impl<'a> Output<'a> { }; let mut dst = Dest::File(dst, density); dst.seek(settings.seek) - .map_err_context(|| "failed to seek in output file".to_string())?; + .map_err_context(|| get_message("dd-error-failed-to-seek"))?; Ok(Self { dst, settings }) } @@ -832,9 +872,11 @@ impl<'a> Output<'a> { fn discard_cache(&self, offset: libc::off_t, len: libc::off_t) { #[cfg(target_os = "linux")] { - show_if_err!(self.dst.discard_cache(offset, len).map_err_context(|| { - "failed to discard cache for: 'standard output'".to_string() - })); + show_if_err!( + self.dst + .discard_cache(offset, len) + .map_err_context(|| { get_message("dd-error-failed-discard-cache-output") }) + ); } #[cfg(not(target_os = "linux"))] { @@ -1083,7 +1125,7 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> { #[cfg(target_os = "linux")] if let Err(e) = &signal_handler { if Some(StatusLevel::None) != i.settings.status { - eprintln!("Internal dd Warning: Unable to register signal handler \n\t{e}"); + eprintln!("{}\n\t{e}", get_message("dd-warning-signal-handler")); } } @@ -1419,7 +1461,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { None if is_stdout_redirected_to_seekable_file() => Output::new_file_from_stdout(&settings)?, None => Output::new_stdout(&settings)?, }; - dd_copy(i, o).map_err_context(|| "IO error".to_string()) + dd_copy(i, o).map_err_context(|| get_message("dd-error-io-error")) } pub fn uu_app() -> Command { diff --git a/src/uu/dd/src/parseargs.rs b/src/uu/dd/src/parseargs.rs index dd9a53fd8..758654de9 100644 --- a/src/uu/dd/src/parseargs.rs +++ b/src/uu/dd/src/parseargs.rs @@ -9,42 +9,53 @@ mod unit_tests; use super::{ConversionMode, IConvFlags, IFlags, Num, OConvFlags, OFlags, Settings, StatusLevel}; use crate::conversion_tables::ConversionTable; +use std::collections::HashMap; use thiserror::Error; use uucore::display::Quotable; use uucore::error::UError; +use uucore::locale::{get_message, get_message_with_args}; use uucore::parser::parse_size::{ParseSizeError, Parser as SizeParser}; use uucore::show_warning; /// Parser Errors describe errors with parser input #[derive(Debug, PartialEq, Eq, Error)] pub enum ParseError { - #[error("Unrecognized operand '{0}'")] + #[error("{}", get_message_with_args("dd-error-unrecognized-operand", + HashMap::from([("operand".to_string(), .0.clone())])))] UnrecognizedOperand(String), - #[error("Only one of conv=ascii conv=ebcdic or conv=ibm may be specified")] + #[error("{}", get_message("dd-error-multiple-format-table"))] MultipleFmtTable, - #[error("Only one of conv=lcase or conv=ucase may be specified")] + #[error("{}", get_message("dd-error-multiple-case"))] MultipleUCaseLCase, - #[error("Only one of conv=block or conv=unblock may be specified")] + #[error("{}", get_message("dd-error-multiple-block"))] MultipleBlockUnblock, - #[error("Only one ov conv=excl or conv=nocreat may be specified")] + #[error("{}", get_message("dd-error-multiple-excl"))] MultipleExclNoCreate, - #[error("invalid input flag: ‘{}’\nTry '{} --help' for more information.", .0, uucore::execution_phrase())] + #[error("{}", get_message_with_args("dd-error-invalid-flag", + HashMap::from([("flag".to_string(), .0.clone()),("cmd".to_string(), uucore::execution_phrase().to_string())])))] FlagNoMatch(String), - #[error("Unrecognized conv=CONV -> {0}")] + #[error("{}", get_message_with_args("dd-error-conv-flag-no-match", + HashMap::from([("flag".to_string(), .0.clone())])))] ConvFlagNoMatch(String), - #[error("invalid number: ‘{0}’")] + #[error("{}", get_message_with_args("dd-error-multiplier-parse-failure", + HashMap::from([("input".to_string(), .0.clone())])))] MultiplierStringParseFailure(String), - #[error("Multiplier string would overflow on current system -> {0}")] + #[error("{}", get_message_with_args("dd-error-multiplier-overflow", + HashMap::from([("input".to_string(), .0.clone())])))] MultiplierStringOverflow(String), - #[error("conv=block or conv=unblock specified without cbs=N")] + #[error("{}", get_message("dd-error-block-without-cbs"))] BlockUnblockWithoutCBS, - #[error("status=LEVEL not recognized -> {0}")] + #[error("{}", get_message_with_args("dd-error-status-not-recognized", + HashMap::from([("level".to_string(), .0.clone())])))] StatusLevelNotRecognized(String), - #[error("feature not implemented on this system -> {0}")] + #[error("{}", get_message_with_args("dd-error-unimplemented", + HashMap::from([("feature".to_string(), .0.clone())])))] Unimplemented(String), - #[error("{0}=N cannot fit into memory")] + #[error("{}", get_message_with_args("dd-error-bs-out-of-range", + HashMap::from([("param".to_string(), .0.clone())])))] BsOutOfRange(String), - #[error("invalid number: ‘{0}’")] + #[error("{}", get_message_with_args("dd-error-invalid-number", + HashMap::from([("input".to_string(), .0.clone())])))] InvalidNumber(String), } @@ -424,9 +435,14 @@ impl UError for ParseError { fn show_zero_multiplier_warning() { show_warning!( - "{} is a zero multiplier; use {} if that is intended", - "0x".quote(), - "00x".quote() + "{}", + get_message_with_args( + "dd-warning-zero-multiplier", + HashMap::from([ + ("zero".to_string(), "0x".quote().to_string()), + ("alternative".to_string(), "00x".quote().to_string()) + ]) + ) ); } diff --git a/src/uu/dd/src/progress.rs b/src/uu/dd/src/progress.rs index 85f5fa85a..00dbaa432 100644 --- a/src/uu/dd/src/progress.rs +++ b/src/uu/dd/src/progress.rs @@ -9,6 +9,7 @@ //! read and write progress of a running `dd` process. The //! [`gen_prog_updater`] function can be used to implement a progress //! updater that runs in its own thread. +use std::collections::HashMap; use std::io::Write; use std::sync::mpsc; #[cfg(target_os = "linux")] @@ -20,6 +21,8 @@ use signal_hook::iterator::Handle; use uucore::{ error::UResult, format::num_format::{FloatVariant, Formatter}, + locale::get_message_with_args, + locale::setup_localization, }; use crate::numbers::{SuffixType, to_magnitude_and_suffix}; @@ -102,8 +105,13 @@ impl ProgUpdate { self.write_stat.report(w)?; match self.read_stat.records_truncated { 0 => {} - 1 => writeln!(w, "1 truncated record")?, - n => writeln!(w, "{n} truncated records")?, + count => { + let message = get_message_with_args( + "dd-progress-truncated-record", + HashMap::from([("count".to_string(), count.to_string())]), + ); + writeln!(w, "{}", message)?; + } } Ok(()) } @@ -164,24 +172,45 @@ impl ProgUpdate { // If the number of bytes written is sufficiently large, then // print a more concise representation of the number, like // "1.2 kB" and "1.0 KiB". - match btotal { - 1 => write!( - w, - "{carriage_return}{btotal} byte copied, {duration_str} s, {transfer_rate}/s{newline}", - )?, - 0..=999 => write!( - w, - "{carriage_return}{btotal} bytes copied, {duration_str} s, {transfer_rate}/s{newline}", - )?, - 1000..=1023 => write!( - w, - "{carriage_return}{btotal} bytes ({btotal_metric}) copied, {duration_str} s, {transfer_rate}/s{newline}", - )?, - _ => write!( - w, - "{carriage_return}{btotal} bytes ({btotal_metric}, {btotal_bin}) copied, {duration_str} s, {transfer_rate}/s{newline}", - )?, + let message = match btotal { + 1 => get_message_with_args( + "dd-progress-byte-copied", + HashMap::from([ + ("bytes".to_string(), btotal.to_string()), + ("duration".to_string(), duration_str.to_string()), + ("rate".to_string(), transfer_rate.to_string()), + ]), + ), + 0..=999 => get_message_with_args( + "dd-progress-bytes-copied", + HashMap::from([ + ("bytes".to_string(), btotal.to_string()), + ("duration".to_string(), duration_str.to_string()), + ("rate".to_string(), transfer_rate.to_string()), + ]), + ), + 1000..=1023 => get_message_with_args( + "dd-progress-bytes-copied-si", + HashMap::from([ + ("bytes".to_string(), btotal.to_string()), + ("si".to_string(), btotal_metric.to_string()), + ("duration".to_string(), duration_str.to_string()), + ("rate".to_string(), transfer_rate.to_string()), + ]), + ), + _ => get_message_with_args( + "dd-progress-bytes-copied-si-iec", + HashMap::from([ + ("bytes".to_string(), btotal.to_string()), + ("si".to_string(), btotal_metric.to_string()), + ("iec".to_string(), btotal_bin.to_string()), + ("duration".to_string(), duration_str.to_string()), + ("rate".to_string(), transfer_rate.to_string()), + ]), + ), }; + + write!(w, "{carriage_return}{message}{newline}")?; Ok(()) } @@ -311,11 +340,14 @@ impl ReadStat { /// /// If there is a problem writing to `w`. fn report(&self, w: &mut impl Write) -> std::io::Result<()> { - writeln!( - w, - "{}+{} records in", - self.reads_complete, self.reads_partial - )?; + let message = get_message_with_args( + "dd-progress-records-in", + HashMap::from([ + ("complete".to_string(), self.reads_complete.to_string()), + ("partial".to_string(), self.reads_partial.to_string()), + ]), + ); + writeln!(w, "{}", message)?; Ok(()) } } @@ -368,11 +400,14 @@ impl WriteStat { /// /// If there is a problem writing to `w`. fn report(&self, w: &mut impl Write) -> std::io::Result<()> { - writeln!( - w, - "{}+{} records out", - self.writes_complete, self.writes_partial - ) + let message = get_message_with_args( + "dd-progress-records-out", + HashMap::from([ + ("complete".to_string(), self.writes_complete.to_string()), + ("partial".to_string(), self.writes_partial.to_string()), + ]), + ); + writeln!(w, "{}", message) } } @@ -428,6 +463,9 @@ pub(crate) fn gen_prog_updater( print_level: Option, ) -> impl Fn() { move || { + // As we are in a thread, we need to set up localization independently. + let _ = setup_localization("dd"); + let mut progress_printed = false; while let Ok(update) = rx.recv() { // Print the final read/write statistics. @@ -502,6 +540,9 @@ pub(crate) fn gen_prog_updater( ) -> impl Fn() { // -------------------------------------------------------------- move || { + // As we are in a thread, we need to set up localization independently. + let _ = setup_localization("dd"); + // Holds the state of whether we have printed the current progress. // This is needed so that we know whether or not to print a newline // character before outputting non-progress data. @@ -532,11 +573,18 @@ pub(crate) fn gen_prog_updater( #[cfg(test)] mod tests { - + use std::env; use std::io::Cursor; use std::time::Duration; + use uucore::locale::setup_localization; use super::{ProgUpdate, ReadStat, WriteStat}; + fn init() { + unsafe { + env::set_var("LANG", "C"); + } + let _ = setup_localization("dd"); + } fn prog_update_write(n: u128) -> ProgUpdate { ProgUpdate { @@ -561,22 +609,31 @@ mod tests { #[test] fn test_read_stat_report() { + init(); let read_stat = ReadStat::new(1, 2, 3, 4); let mut cursor = Cursor::new(vec![]); read_stat.report(&mut cursor).unwrap(); - assert_eq!(cursor.get_ref(), b"1+2 records in\n"); + assert_eq!( + std::str::from_utf8(cursor.get_ref()).unwrap(), + "1+2 records in\n" + ); } #[test] fn test_write_stat_report() { + init(); let write_stat = WriteStat::new(1, 2, 3); let mut cursor = Cursor::new(vec![]); write_stat.report(&mut cursor).unwrap(); - assert_eq!(cursor.get_ref(), b"1+2 records out\n"); + assert_eq!( + std::str::from_utf8(cursor.get_ref()).unwrap(), + "1+2 records out\n" + ); } #[test] fn test_prog_update_write_io_lines() { + init(); let read_stat = ReadStat::new(1, 2, 3, 4); let write_stat = WriteStat::new(4, 5, 6); let duration = Duration::new(789, 0); @@ -591,13 +648,14 @@ mod tests { let mut cursor = Cursor::new(vec![]); prog_update.write_io_lines(&mut cursor).unwrap(); assert_eq!( - cursor.get_ref(), - b"1+2 records in\n4+5 records out\n3 truncated records\n" + std::str::from_utf8(cursor.get_ref()).unwrap(), + "1+2 records in\n4+5 records out\n3 truncated records\n" ); } #[test] fn test_prog_update_write_prog_line() { + init(); let prog_update = ProgUpdate { read_stat: ReadStat::default(), write_stat: WriteStat::default(), @@ -615,45 +673,55 @@ mod tests { // 0 bytes copied, 7.9151e-05 s, 0.0 kB/s // // The throughput still does not match GNU dd. - assert_eq!(cursor.get_ref(), b"0 bytes copied, 1 s, 0.0 B/s\n"); + assert_eq!( + std::str::from_utf8(cursor.get_ref()).unwrap(), + "0 bytes copied, 1 s, 0.0 B/s\n" + ); let prog_update = prog_update_write(1); let mut cursor = Cursor::new(vec![]); prog_update.write_prog_line(&mut cursor, rewrite).unwrap(); - assert_eq!(cursor.get_ref(), b"1 byte copied, 1 s, 0.0 B/s\n"); + assert_eq!( + std::str::from_utf8(cursor.get_ref()).unwrap(), + "1 byte copied, 1 s, 0.0 B/s\n" + ); let prog_update = prog_update_write(999); let mut cursor = Cursor::new(vec![]); prog_update.write_prog_line(&mut cursor, rewrite).unwrap(); - assert_eq!(cursor.get_ref(), b"999 bytes copied, 1 s, 0.0 B/s\n"); + assert_eq!( + std::str::from_utf8(cursor.get_ref()).unwrap(), + "999 bytes copied, 1 s, 0.0 B/s\n" + ); let prog_update = prog_update_write(1000); let mut cursor = Cursor::new(vec![]); prog_update.write_prog_line(&mut cursor, rewrite).unwrap(); assert_eq!( - cursor.get_ref(), - b"1000 bytes (1.0 kB) copied, 1 s, 1.0 kB/s\n" + std::str::from_utf8(cursor.get_ref()).unwrap(), + "1000 bytes (1.0 kB) copied, 1 s, 1.0 kB/s\n" ); let prog_update = prog_update_write(1023); let mut cursor = Cursor::new(vec![]); prog_update.write_prog_line(&mut cursor, rewrite).unwrap(); assert_eq!( - cursor.get_ref(), - b"1023 bytes (1.0 kB) copied, 1 s, 1.0 kB/s\n" + std::str::from_utf8(cursor.get_ref()).unwrap(), + "1023 bytes (1.0 kB) copied, 1 s, 1.0 kB/s\n" ); let prog_update = prog_update_write(1024); let mut cursor = Cursor::new(vec![]); prog_update.write_prog_line(&mut cursor, rewrite).unwrap(); assert_eq!( - cursor.get_ref(), - b"1024 bytes (1.0 kB, 1.0 KiB) copied, 1 s, 1.0 kB/s\n" + std::str::from_utf8(cursor.get_ref()).unwrap(), + "1024 bytes (1.0 kB, 1.0 KiB) copied, 1 s, 1.0 kB/s\n" ); } #[test] fn write_transfer_stats() { + init(); let prog_update = ProgUpdate { read_stat: ReadStat::default(), write_stat: WriteStat::default(), @@ -664,16 +732,18 @@ mod tests { prog_update .write_transfer_stats(&mut cursor, false) .unwrap(); - let mut iter = cursor.get_ref().split(|v| *v == b'\n'); - assert_eq!(iter.next().unwrap(), b"0+0 records in"); - assert_eq!(iter.next().unwrap(), b"0+0 records out"); - assert_eq!(iter.next().unwrap(), b"0 bytes copied, 1 s, 0.0 B/s"); - assert_eq!(iter.next().unwrap(), b""); + let output_str = std::str::from_utf8(cursor.get_ref()).unwrap(); + let mut iter = output_str.split('\n'); + assert_eq!(iter.next().unwrap(), "0+0 records in"); + assert_eq!(iter.next().unwrap(), "0+0 records out"); + assert_eq!(iter.next().unwrap(), "0 bytes copied, 1 s, 0.0 B/s"); + assert_eq!(iter.next().unwrap(), ""); assert!(iter.next().is_none()); } #[test] fn write_final_transfer_stats() { + init(); // Tests the formatting of the final statistics written after a progress line. let prog_update = ProgUpdate { read_stat: ReadStat::default(), @@ -685,21 +755,26 @@ mod tests { let rewrite = true; prog_update.write_prog_line(&mut cursor, rewrite).unwrap(); prog_update.write_transfer_stats(&mut cursor, true).unwrap(); - let mut iter = cursor.get_ref().split(|v| *v == b'\n'); - assert_eq!(iter.next().unwrap(), b"\r0 bytes copied, 1 s, 0.0 B/s"); - assert_eq!(iter.next().unwrap(), b"0+0 records in"); - assert_eq!(iter.next().unwrap(), b"0+0 records out"); - assert_eq!(iter.next().unwrap(), b"0 bytes copied, 1 s, 0.0 B/s"); - assert_eq!(iter.next().unwrap(), b""); + let output_str = std::str::from_utf8(cursor.get_ref()).unwrap(); + let mut iter = output_str.split('\n'); + assert_eq!(iter.next().unwrap(), "\r0 bytes copied, 1 s, 0.0 B/s"); + assert_eq!(iter.next().unwrap(), "0+0 records in"); + assert_eq!(iter.next().unwrap(), "0+0 records out"); + assert_eq!(iter.next().unwrap(), "0 bytes copied, 1 s, 0.0 B/s"); + assert_eq!(iter.next().unwrap(), ""); assert!(iter.next().is_none()); } #[test] fn test_duration_precision() { + init(); let prog_update = prog_update_duration(Duration::from_nanos(123)); let mut cursor = Cursor::new(vec![]); let rewrite = false; prog_update.write_prog_line(&mut cursor, rewrite).unwrap(); - assert_eq!(cursor.get_ref(), b"0 bytes copied, 1.23e-07 s, 0.0 B/s\n"); + assert_eq!( + std::str::from_utf8(cursor.get_ref()).unwrap(), + "0 bytes copied, 0.000000123 s, 0.0 B/s\n" + ); } } diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index b9daf8de7..e7e23b9ca 100644 --- a/tests/by-util/test_dd.rs +++ b/tests/by-util/test_dd.rs @@ -1617,6 +1617,7 @@ fn test_reading_partial_blocks_from_fifo() { .args(["dd", "ibs=3", "obs=3", &format!("if={fifoname}")]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) + .env("LANG", "C") .spawn() .unwrap(); @@ -1661,6 +1662,7 @@ fn test_reading_partial_blocks_from_fifo_unbuffered() { .args(["dd", "bs=3", "ibs=1", "obs=1", &format!("if={fifoname}")]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) + .env("LANG", "C") .spawn() .unwrap(); @@ -1767,10 +1769,10 @@ fn test_wrong_number_err_msg() { new_ucmd!() .args(&["count=kBb"]) .fails() - .stderr_contains("dd: invalid number: ‘kBb’\n"); + .stderr_contains("dd: invalid number: 'kBb'\n"); new_ucmd!() .args(&["count=1kBb555"]) .fails() - .stderr_contains("dd: invalid number: ‘1kBb555’\n"); + .stderr_contains("dd: invalid number: '1kBb555'\n"); }