1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-27 19:17:43 +00:00

l10n: port fmt for translation + use thiserror + add french

This commit is contained in:
Sylvestre Ledru 2025-06-22 10:22:38 +02:00
parent 1675c3e981
commit ebc4fbcaf2
5 changed files with 143 additions and 94 deletions

1
Cargo.lock generated
View file

@ -3027,6 +3027,7 @@ name = "uu_fmt"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"clap", "clap",
"thiserror 2.0.12",
"unicode-width 0.2.1", "unicode-width 0.2.1",
"uucore", "uucore",
] ]

View file

@ -21,6 +21,7 @@ path = "src/fmt.rs"
clap = { workspace = true } clap = { workspace = true }
unicode-width = { workspace = true } unicode-width = { workspace = true }
uucore = { workspace = true } uucore = { workspace = true }
thiserror = { workspace = true }
[[bin]] [[bin]]
name = "fmt" name = "fmt"

View file

@ -1,2 +1,32 @@
fmt-about = Reformat paragraphs from input files (or stdin) to stdout. fmt-about = Reformat paragraphs from input (or standard input) to stdout.
fmt-usage = fmt [-WIDTH] [OPTION]... [FILE]... fmt-usage = [OPTION]... [FILE]...
# Help messages
fmt-crown-margin-help = First and second line of paragraph may have different indentations, in which case the first line's indentation is preserved, and each subsequent line's indentation matches the second line.
fmt-tagged-paragraph-help = Like -c, except that the first and second line of a paragraph *must* have different indentation or they are treated as separate paragraphs.
fmt-preserve-headers-help = Attempt to detect and preserve mail headers in the input. Be careful when combining this flag with -p.
fmt-split-only-help = Split lines only, do not reflow.
fmt-uniform-spacing-help = Insert exactly one space between words, and two between sentences. Sentence breaks in the input are detected as [?!.] followed by two spaces or a newline; other punctuation is not interpreted as a sentence break.
fmt-prefix-help = Reformat only lines beginning with PREFIX, reattaching PREFIX to reformatted lines. Unless -x is specified, leading whitespace will be ignored when matching PREFIX.
fmt-skip-prefix-help = Do not reformat lines beginning with PSKIP. Unless -X is specified, leading whitespace will be ignored when matching PSKIP
fmt-exact-prefix-help = PREFIX must match at the beginning of the line with no preceding whitespace.
fmt-exact-skip-prefix-help = PSKIP must match at the beginning of the line with no preceding whitespace.
fmt-width-help = Fill output lines up to a maximum of WIDTH columns, default 75. This can be specified as a negative number in the first argument.
fmt-goal-help = Goal width, default of 93% of WIDTH. Must be less than or equal to WIDTH.
fmt-quick-help = Break lines more quickly at the expense of a potentially more ragged appearance.
fmt-tab-width-help = Treat tabs as TABWIDTH spaces for determining line length, default 8. Note that this is used only for calculating line lengths; tabs are preserved in the output.
# Error messages
fmt-error-invalid-goal = invalid goal: {$goal}
fmt-error-goal-greater-than-width = GOAL cannot be greater than WIDTH.
fmt-error-invalid-width = invalid width: {$width}
fmt-error-width-out-of-range = invalid width: '{$width}': Numerical result out of range
fmt-error-invalid-tabwidth = Invalid TABWIDTH specification: {$tabwidth}
fmt-error-first-option-width = invalid option -- {$option}; -WIDTH is recognized only when it is the first
option; use -w N instead
Try 'fmt --help' for more information.
fmt-error-read = read error
fmt-error-invalid-width-malformed = invalid width: {$width}
fmt-error-cannot-open-for-reading = cannot open {$file} for reading
fmt-error-cannot-get-metadata = cannot get metadata for {$file}
fmt-error-failed-to-write-output = failed to write output

View file

@ -0,0 +1,32 @@
fmt-about = Reformate les paragraphes depuis l'entrée (ou l'entrée standard) vers la sortie standard.
fmt-usage = [OPTION]... [FICHIER]...
# Messages d'aide
fmt-crown-margin-help = La première et la deuxième ligne d'un paragraphe peuvent avoir des indentations différentes, auquel cas l'indentation de la première ligne est préservée, et chaque ligne suivante correspond à l'indentation de la deuxième ligne.
fmt-tagged-paragraph-help = Comme -c, sauf que la première et la deuxième ligne d'un paragraphe *doivent* avoir des indentations différentes ou elles sont traitées comme des paragraphes séparés.
fmt-preserve-headers-help = Tente de détecter et préserver les en-têtes de courrier dans l'entrée. Attention en combinant ce drapeau avec -p.
fmt-split-only-help = Divise les lignes seulement, ne les reformate pas.
fmt-uniform-spacing-help = Insère exactement un espace entre les mots, et deux entre les phrases. Les fins de phrase dans l'entrée sont détectées comme [?!.] suivies de deux espaces ou d'une nouvelle ligne ; les autres ponctuations ne sont pas interprétées comme des fins de phrase.
fmt-prefix-help = Reformate seulement les lignes commençant par PRÉFIXE, en rattachant PRÉFIXE aux lignes reformatées. À moins que -x soit spécifié, les espaces de début seront ignorés lors de la correspondance avec PRÉFIXE.
fmt-skip-prefix-help = Ne reformate pas les lignes commençant par PSKIP. À moins que -X soit spécifié, les espaces de début seront ignorés lors de la correspondance avec PSKIP
fmt-exact-prefix-help = PRÉFIXE doit correspondre au début de la ligne sans espace précédent.
fmt-exact-skip-prefix-help = PSKIP doit correspondre au début de la ligne sans espace précédent.
fmt-width-help = Remplit les lignes de sortie jusqu'à un maximum de WIDTH colonnes, par défaut 75. Cela peut être spécifié comme un nombre négatif dans le premier argument.
fmt-goal-help = Largeur objectif, par défaut 93% de WIDTH. Doit être inférieur ou égal à WIDTH.
fmt-quick-help = Divise les lignes plus rapidement au détriment d'un aspect potentiellement plus irrégulier.
fmt-tab-width-help = Traite les tabulations comme TABWIDTH espaces pour déterminer la longueur de ligne, par défaut 8. Notez que ceci n'est utilisé que pour calculer les longueurs de ligne ; les tabulations sont préservées dans la sortie.
# Messages d'erreur
fmt-error-invalid-goal = objectif invalide : {$goal}
fmt-error-goal-greater-than-width = GOAL ne peut pas être supérieur à WIDTH.
fmt-error-invalid-width = largeur invalide : {$width}
fmt-error-width-out-of-range = largeur invalide : '{$width}' : Résultat numérique hors limites
fmt-error-invalid-tabwidth = Spécification TABWIDTH invalide : {$tabwidth}
fmt-error-first-option-width = option invalide -- {$option} ; -WIDTH n'est reconnu que lorsqu'il est la première
option ; utilisez -w N à la place
Essayez 'fmt --help' pour plus d'informations.
fmt-error-read = erreur de lecture
fmt-error-invalid-width-malformed = largeur invalide : {$width}
fmt-error-cannot-open-for-reading = impossible d'ouvrir {$file} en lecture
fmt-error-cannot-get-metadata = impossible d'obtenir les métadonnées pour {$file}
fmt-error-failed-to-write-output = échec de l'écriture de sortie

View file

@ -9,16 +9,45 @@ use clap::{Arg, ArgAction, ArgMatches, Command};
use std::fs::File; use std::fs::File;
use std::io::{BufReader, BufWriter, Read, Stdout, Write, stdin, stdout}; use std::io::{BufReader, BufWriter, Read, Stdout, Write, stdin, stdout};
use uucore::display::Quotable; use uucore::display::Quotable;
use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; use uucore::error::{FromIo, UResult, USimpleError};
use uucore::format_usage; use uucore::format_usage;
use linebreak::break_lines; use linebreak::break_lines;
use parasplit::ParagraphStream; use parasplit::ParagraphStream;
use uucore::locale::get_message; use std::collections::HashMap;
use thiserror::Error;
use uucore::locale::{get_message, get_message_with_args};
mod linebreak; mod linebreak;
mod parasplit; mod parasplit;
#[derive(Debug, Error)]
enum FmtError {
#[error("{}", get_message_with_args("fmt-error-invalid-goal", HashMap::from([("goal".to_string(), .0.quote().to_string())])))]
InvalidGoal(String),
#[error("{}", get_message("fmt-error-goal-greater-than-width"))]
GoalGreaterThanWidth,
#[error("{}", get_message_with_args("fmt-error-invalid-width", HashMap::from([("width".to_string(), .0.quote().to_string())])))]
InvalidWidth(String),
#[error("{}", get_message_with_args("fmt-error-width-out-of-range", HashMap::from([("width".to_string(), .0.to_string())])))]
WidthOutOfRange(usize),
#[error("{}", get_message_with_args("fmt-error-invalid-tabwidth", HashMap::from([("tabwidth".to_string(), .0.quote().to_string())])))]
InvalidTabWidth(String),
#[error("{}", get_message_with_args("fmt-error-first-option-width", HashMap::from([("option".to_string(), .0.to_string())])))]
FirstOptionWidth(char),
#[error("{}", get_message("fmt-error-read"))]
ReadError,
#[error("{}", get_message_with_args("fmt-error-invalid-width-malformed", HashMap::from([("width".to_string(), .0.quote().to_string())])))]
InvalidWidthMalformed(String),
}
impl From<FmtError> for Box<dyn uucore::error::UError> {
fn from(err: FmtError) -> Self {
USimpleError::new(1, err.to_string())
}
}
const MAX_WIDTH: usize = 2500; const MAX_WIDTH: usize = 2500;
const DEFAULT_GOAL: usize = 70; const DEFAULT_GOAL: usize = 70;
const DEFAULT_WIDTH: usize = 75; const DEFAULT_WIDTH: usize = 75;
@ -92,10 +121,7 @@ impl FmtOptions {
match goal_str.parse::<usize>() { match goal_str.parse::<usize>() {
Ok(goal) => Some(goal), Ok(goal) => Some(goal),
Err(_) => { Err(_) => {
return Err(USimpleError::new( return Err(FmtError::InvalidGoal(goal_str.clone()).into());
1,
format!("invalid goal: {}", goal_str.quote()),
));
} }
} }
} else { } else {
@ -105,7 +131,7 @@ impl FmtOptions {
let (width, goal) = match (width_opt, goal_opt) { let (width, goal) = match (width_opt, goal_opt) {
(Some(w), Some(g)) => { (Some(w), Some(g)) => {
if g > w { if g > w {
return Err(USimpleError::new(1, "GOAL cannot be greater than WIDTH.")); return Err(FmtError::GoalGreaterThanWidth.into());
} }
(w, g) (w, g)
} }
@ -119,7 +145,7 @@ impl FmtOptions {
} }
(None, Some(g)) => { (None, Some(g)) => {
if g > DEFAULT_WIDTH { if g > DEFAULT_WIDTH {
return Err(USimpleError::new(1, "GOAL cannot be greater than WIDTH.")); return Err(FmtError::GoalGreaterThanWidth.into());
} }
let w = (g * 100 / DEFAULT_GOAL_TO_WIDTH_RATIO).max(g + 3); let w = (g * 100 / DEFAULT_GOAL_TO_WIDTH_RATIO).max(g + 3);
(w, g) (w, g)
@ -132,21 +158,15 @@ impl FmtOptions {
); );
if width > MAX_WIDTH { if width > MAX_WIDTH {
return Err(USimpleError::new( return Err(FmtError::WidthOutOfRange(width).into());
1,
format!("invalid width: '{width}': Numerical result out of range"),
));
} }
let mut tabwidth = 8; let mut tabwidth = 8;
if let Some(s) = matches.get_one::<String>(options::TAB_WIDTH) { if let Some(s) = matches.get_one::<String>(options::TAB_WIDTH) {
tabwidth = match s.parse::<usize>() { tabwidth = match s.parse::<usize>() {
Ok(t) => t, Ok(t) => t,
Err(e) => { Err(_) => {
return Err(USimpleError::new( return Err(FmtError::InvalidTabWidth(s.clone()).into());
1,
format!("Invalid TABWIDTH specification: {}: {e}", s.quote()),
));
} }
}; };
}; };
@ -192,13 +212,22 @@ fn process_file(
let mut fp = BufReader::new(match file_name { let mut fp = BufReader::new(match file_name {
"-" => Box::new(stdin()) as Box<dyn Read + 'static>, "-" => Box::new(stdin()) as Box<dyn Read + 'static>,
_ => { _ => {
let f = File::open(file_name) let f = File::open(file_name).map_err_context(|| {
.map_err_context(|| format!("cannot open {} for reading", file_name.quote()))?; get_message_with_args(
"fmt-error-cannot-open-for-reading",
HashMap::from([("file".to_string(), file_name.quote().to_string())]),
)
})?;
if f.metadata() if f.metadata()
.map_err_context(|| format!("cannot get metadata for {}", file_name.quote()))? .map_err_context(|| {
get_message_with_args(
"fmt-error-cannot-get-metadata",
HashMap::from([("file".to_string(), file_name.quote().to_string())]),
)
})?
.is_dir() .is_dir()
{ {
return Err(USimpleError::new(1, "read error".to_string())); return Err(FmtError::ReadError.into());
} }
Box::new(f) as Box<dyn Read + 'static> Box::new(f) as Box<dyn Read + 'static>
@ -211,20 +240,20 @@ fn process_file(
Err(s) => { Err(s) => {
ostream ostream
.write_all(s.as_bytes()) .write_all(s.as_bytes())
.map_err_context(|| "failed to write output".to_string())?; .map_err_context(|| get_message("fmt-error-failed-to-write-output"))?;
ostream ostream
.write_all(b"\n") .write_all(b"\n")
.map_err_context(|| "failed to write output".to_string())?; .map_err_context(|| get_message("fmt-error-failed-to-write-output"))?;
} }
Ok(para) => break_lines(&para, fmt_opts, ostream) Ok(para) => break_lines(&para, fmt_opts, ostream)
.map_err_context(|| "failed to write output".to_string())?, .map_err_context(|| get_message("fmt-error-failed-to-write-output"))?,
} }
} }
// flush the output after each file // flush the output after each file
ostream ostream
.flush() .flush()
.map_err_context(|| "failed to write output".to_string())?; .map_err_context(|| get_message("fmt-error-failed-to-write-output"))?;
Ok(()) Ok(())
} }
@ -251,10 +280,11 @@ fn extract_files(matches: &ArgMatches) -> UResult<Vec<String>> {
if in_first_pos && i == 0 { if in_first_pos && i == 0 {
None None
} else { } else {
let first_num = x.chars().nth(1).expect("a negative number should be at least two characters long"); let first_num = x
Some(Err( .chars()
UUsageError::new(1, format!("invalid option -- {first_num}; -WIDTH is recognized only when it is the first\noption; use -w N instead")) .nth(1)
)) .expect("a negative number should be at least two characters long");
Some(Err(FmtError::FirstOptionWidth(first_num).into()))
} }
} else { } else {
Some(Ok(x.clone())) Some(Ok(x.clone()))
@ -275,10 +305,7 @@ fn extract_width(matches: &ArgMatches) -> UResult<Option<usize>> {
return if let Ok(width) = width_str.parse::<usize>() { return if let Ok(width) = width_str.parse::<usize>() {
Ok(Some(width)) Ok(Some(width))
} else { } else {
Err(USimpleError::new( Err(FmtError::InvalidWidth(width_str.clone()).into())
1,
format!("invalid width: {}", width_str.quote()),
))
}; };
} }
@ -307,13 +334,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
&& first_arg.chars().nth(1).is_some_and(|c| c.is_ascii_digit()) && first_arg.chars().nth(1).is_some_and(|c| c.is_ascii_digit())
&& first_arg.chars().skip(2).any(|c| !c.is_ascii_digit()); && first_arg.chars().skip(2).any(|c| !c.is_ascii_digit());
if malformed_number { if malformed_number {
return Err(USimpleError::new( return Err(FmtError::InvalidWidthMalformed(
1, first_arg.strip_prefix('-').unwrap().to_string(),
format!( )
"invalid width: {}", .into());
first_arg.strip_prefix('-').unwrap().quote()
),
));
} }
} }
@ -343,102 +367,70 @@ pub fn uu_app() -> Command {
Arg::new(options::CROWN_MARGIN) Arg::new(options::CROWN_MARGIN)
.short('c') .short('c')
.long(options::CROWN_MARGIN) .long(options::CROWN_MARGIN)
.help( .help(get_message("fmt-crown-margin-help"))
"First and second line of paragraph \
may have different indentations, in which \
case the first line's indentation is preserved, \
and each subsequent line's indentation matches the second line.",
)
.action(ArgAction::SetTrue), .action(ArgAction::SetTrue),
) )
.arg( .arg(
Arg::new(options::TAGGED_PARAGRAPH) Arg::new(options::TAGGED_PARAGRAPH)
.short('t') .short('t')
.long("tagged-paragraph") .long("tagged-paragraph")
.help( .help(get_message("fmt-tagged-paragraph-help"))
"Like -c, except that the first and second line of a paragraph *must* \
have different indentation or they are treated as separate paragraphs.",
)
.action(ArgAction::SetTrue), .action(ArgAction::SetTrue),
) )
.arg( .arg(
Arg::new(options::PRESERVE_HEADERS) Arg::new(options::PRESERVE_HEADERS)
.short('m') .short('m')
.long("preserve-headers") .long("preserve-headers")
.help( .help(get_message("fmt-preserve-headers-help"))
"Attempt to detect and preserve mail headers in the input. \
Be careful when combining this flag with -p.",
)
.action(ArgAction::SetTrue), .action(ArgAction::SetTrue),
) )
.arg( .arg(
Arg::new(options::SPLIT_ONLY) Arg::new(options::SPLIT_ONLY)
.short('s') .short('s')
.long("split-only") .long("split-only")
.help("Split lines only, do not reflow.") .help(get_message("fmt-split-only-help"))
.action(ArgAction::SetTrue), .action(ArgAction::SetTrue),
) )
.arg( .arg(
Arg::new(options::UNIFORM_SPACING) Arg::new(options::UNIFORM_SPACING)
.short('u') .short('u')
.long("uniform-spacing") .long("uniform-spacing")
.help( .help(get_message("fmt-uniform-spacing-help"))
"Insert exactly one \
space between words, and two between sentences. \
Sentence breaks in the input are detected as [?!.] \
followed by two spaces or a newline; other punctuation \
is not interpreted as a sentence break.",
)
.action(ArgAction::SetTrue), .action(ArgAction::SetTrue),
) )
.arg( .arg(
Arg::new(options::PREFIX) Arg::new(options::PREFIX)
.short('p') .short('p')
.long("prefix") .long("prefix")
.help( .help(get_message("fmt-prefix-help"))
"Reformat only lines \
beginning with PREFIX, reattaching PREFIX to reformatted lines. \
Unless -x is specified, leading whitespace will be ignored \
when matching PREFIX.",
)
.value_name("PREFIX"), .value_name("PREFIX"),
) )
.arg( .arg(
Arg::new(options::SKIP_PREFIX) Arg::new(options::SKIP_PREFIX)
.short('P') .short('P')
.long("skip-prefix") .long("skip-prefix")
.help( .help(get_message("fmt-skip-prefix-help"))
"Do not reformat lines \
beginning with PSKIP. Unless -X is specified, leading whitespace \
will be ignored when matching PSKIP",
)
.value_name("PSKIP"), .value_name("PSKIP"),
) )
.arg( .arg(
Arg::new(options::EXACT_PREFIX) Arg::new(options::EXACT_PREFIX)
.short('x') .short('x')
.long("exact-prefix") .long("exact-prefix")
.help( .help(get_message("fmt-exact-prefix-help"))
"PREFIX must match at the \
beginning of the line with no preceding whitespace.",
)
.action(ArgAction::SetTrue), .action(ArgAction::SetTrue),
) )
.arg( .arg(
Arg::new(options::EXACT_SKIP_PREFIX) Arg::new(options::EXACT_SKIP_PREFIX)
.short('X') .short('X')
.long("exact-skip-prefix") .long("exact-skip-prefix")
.help( .help(get_message("fmt-exact-skip-prefix-help"))
"PSKIP must match at the \
beginning of the line with no preceding whitespace.",
)
.action(ArgAction::SetTrue), .action(ArgAction::SetTrue),
) )
.arg( .arg(
Arg::new(options::WIDTH) Arg::new(options::WIDTH)
.short('w') .short('w')
.long("width") .long("width")
.help("Fill output lines up to a maximum of WIDTH columns, default 75. This can be specified as a negative number in the first argument.") .help(get_message("fmt-width-help"))
// We must accept invalid values if they are overridden later. This is not supported by clap, so accept all strings instead. // We must accept invalid values if they are overridden later. This is not supported by clap, so accept all strings instead.
.value_name("WIDTH"), .value_name("WIDTH"),
) )
@ -446,7 +438,7 @@ pub fn uu_app() -> Command {
Arg::new(options::GOAL) Arg::new(options::GOAL)
.short('g') .short('g')
.long("goal") .long("goal")
.help("Goal width, default of 93% of WIDTH. Must be less than or equal to WIDTH.") .help(get_message("fmt-goal-help"))
// We must accept invalid values if they are overridden later. This is not supported by clap, so accept all strings instead. // We must accept invalid values if they are overridden later. This is not supported by clap, so accept all strings instead.
.value_name("GOAL"), .value_name("GOAL"),
) )
@ -454,21 +446,14 @@ pub fn uu_app() -> Command {
Arg::new(options::QUICK) Arg::new(options::QUICK)
.short('q') .short('q')
.long("quick") .long("quick")
.help( .help(get_message("fmt-quick-help"))
"Break lines more quickly at the \
expense of a potentially more ragged appearance.",
)
.action(ArgAction::SetTrue), .action(ArgAction::SetTrue),
) )
.arg( .arg(
Arg::new(options::TAB_WIDTH) Arg::new(options::TAB_WIDTH)
.short('T') .short('T')
.long("tab-width") .long("tab-width")
.help( .help(get_message("fmt-tab-width-help"))
"Treat tabs as TABWIDTH spaces for \
determining line length, default 8. Note that this is used only for \
calculating line lengths; tabs are preserved in the output.",
)
.value_name("TABWIDTH"), .value_name("TABWIDTH"),
) )
.arg( .arg(