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

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

This commit is contained in:
Sylvestre Ledru 2025-06-22 11:21:51 +02:00
parent 411874f34b
commit 3f0b87e319
5 changed files with 333 additions and 70 deletions

1
Cargo.lock generated
View file

@ -3671,6 +3671,7 @@ version = "0.1.0"
dependencies = [
"chrono",
"clap",
"thiserror 2.0.12",
"uucore",
]

View file

@ -21,6 +21,7 @@ path = "src/stat.rs"
clap = { workspace = true }
uucore = { workspace = true, features = ["entries", "libc", "fs", "fsext"] }
chrono = { workspace = true }
thiserror = { workspace = true }
[features]
selinux = ["uucore/selinux"]

View file

@ -52,3 +52,59 @@ stat-after-help = Valid format sequences for files (without `--file-system`):
NOTE: your shell may have its own version of stat, which usually supersedes
the version described here. Please refer to your shell's documentation
for details about the options it supports.
## Error messages
stat-error-invalid-quoting-style = Invalid quoting style: {$style}
stat-error-missing-operand = missing operand
Try 'stat --help' for more information.
stat-error-invalid-directive = {$directive}: invalid directive
stat-error-cannot-read-filesystem = cannot read table of mounted file systems: {$error}
stat-error-stdin-filesystem-mode = using '-' to denote standard input does not work in file system mode
stat-error-cannot-read-filesystem-info = cannot read file system information for {$file}: {$error}
stat-error-cannot-stat = cannot stat {$file}: {$error}
## Warning messages
stat-warning-backslash-end-format = backslash at end of format
stat-warning-unrecognized-escape-x = unrecognized escape '\x'
stat-warning-incomplete-hex-escape = incomplete hex escape '\x'
stat-warning-unrecognized-escape = unrecognized escape '\{$escape}'
## Help messages
stat-help-dereference = follow links
stat-help-file-system = display file system status instead of file status
stat-help-terse = print the information in terse form
stat-help-format = use the specified FORMAT instead of the default;
output a newline after each use of FORMAT
stat-help-printf = like --format, but interpret backslash escapes,
and do not output a mandatory trailing newline;
if you want a newline, include \n in FORMAT
## Word translations
stat-word-file = File
stat-word-id = ID
stat-word-namelen = Namelen
stat-word-type = Type
stat-word-block = Block
stat-word-size = size
stat-word-fundamental = Fundamental
stat-word-block-size = block size
stat-word-blocks = Blocks
stat-word-total = Total
stat-word-free = Free
stat-word-available = Available
stat-word-inodes = Inodes
stat-word-device = Device
stat-word-inode = Inode
stat-word-links = Links
stat-word-io = IO
stat-word-access = Access
stat-word-uid = Uid
stat-word-gid = Gid
stat-word-modify = Modify
stat-word-change = Change
stat-word-birth = Birth
## SELinux context messages
stat-selinux-failed-get-context = failed to get security context
stat-selinux-unsupported-system = unsupported on this system
stat-selinux-unsupported-os = unsupported for this operating system

View file

@ -0,0 +1,109 @@
stat-about = afficher le statut du fichier ou du système de fichiers.
stat-usage = stat [OPTION]... FICHIER...
stat-after-help = Séquences de format valides pour les fichiers (sans `--file-system`) :
-`%a` : droits d'accès en octal (note : drapeaux printf '#' et '0')
-`%A` : droits d'accès en format lisible
-`%b` : nombre de blocs alloués (voir %B)
-`%B` : la taille en octets de chaque bloc rapporté par %b
-`%C` : chaîne de contexte de sécurité SELinux
-`%d` : numéro de périphérique en décimal
-`%D` : numéro de périphérique en hexadécimal
-`%f` : mode brut en hexadécimal
-`%F` : type de fichier
-`%g` : ID de groupe du propriétaire
-`%G` : nom de groupe du propriétaire
-`%h` : nombre de liens physiques
-`%i` : numéro d'inode
-`%m` : point de montage
-`%n` : nom de fichier
-`%N` : nom de fichier avec guillemets et déréférencement (suivi) si lien symbolique
-`%o` : suggestion de taille optimale de transfert E/S
-`%s` : taille totale, en octets
-`%t` : type de périphérique majeur en hex, pour les fichiers spéciaux caractère/bloc
-`%T` : type de périphérique mineur en hex, pour les fichiers spéciaux caractère/bloc
-`%u` : ID utilisateur du propriétaire
-`%U` : nom d'utilisateur du propriétaire
-`%w` : heure de création du fichier, lisible ; - si inconnue
-`%W` : heure de création du fichier, secondes depuis l'Époque ; 0 si inconnue
-`%x` : heure du dernier accès, lisible
-`%X` : heure du dernier accès, secondes depuis l'Époque
-`%y` : heure de la dernière modification de données, lisible
-`%Y` : heure de la dernière modification de données, secondes depuis l'Époque
-`%z` : heure du dernier changement de statut, lisible
-`%Z` : heure du dernier changement de statut, secondes depuis l'Époque
Séquences de format valides pour les systèmes de fichiers :
-`%a` : blocs libres disponibles pour les non-superutilisateurs
-`%b` : blocs de données totaux dans le système de fichiers
-`%c` : nœuds de fichiers totaux dans le système de fichiers
-`%d` : nœuds de fichiers libres dans le système de fichiers
-`%f` : blocs libres dans le système de fichiers
-`%i` : ID du système de fichiers en hexadécimal
-`%l` : longueur maximale des noms de fichiers
-`%n` : nom de fichier
-`%s` : taille de bloc (pour des transferts plus rapides)
-`%S` : taille de bloc fondamentale (pour les comptes de blocs)
-`%t` : type de système de fichiers en hexadécimal
-`%T` : type de système de fichiers en format lisible
NOTE : votre shell peut avoir sa propre version de stat, qui remplace généralement
la version décrite ici. Veuillez vous référer à la documentation de votre shell
pour les détails sur les options qu'il prend en charge.
# Messages d'aide
stat-help-dereference = suivre les liens
stat-help-file-system = afficher le statut du système de fichiers au lieu du statut du fichier
stat-help-terse = afficher les informations en forme concise
stat-help-format = utiliser le FORMAT spécifié au lieu du défaut ;
afficher une nouvelle ligne après chaque utilisation de FORMAT
stat-help-printf = comme --format, mais interpréter les séquences d'échappement avec barre oblique inverse,
et ne pas afficher une nouvelle ligne finale obligatoire ;
si vous voulez une nouvelle ligne, incluez \n dans FORMAT
## Traductions de mots
stat-word-file = Fichier
stat-word-id = ID
stat-word-namelen = Longnom
stat-word-type = Type
stat-word-block = Bloc
stat-word-size = taille
stat-word-fundamental = Fondamentale
stat-word-block-size = taille bloc
stat-word-blocks = Blocs
stat-word-total = Total
stat-word-free = Libres
stat-word-available = Disponibles
stat-word-inodes = Inodes
stat-word-device = Périphérique
stat-word-inode = Inode
stat-word-links = Liens
stat-word-io = E/S
stat-word-access = Accès
stat-word-uid = Uid
stat-word-gid = Gid
stat-word-modify = Modif
stat-word-change = Changt
stat-word-birth = Créé
## Messages d'erreur
stat-error-invalid-quoting-style = Style de guillemets invalide : {$style}
stat-error-missing-operand = opérande manquant
Essayez 'stat --help' pour plus d'informations.
stat-error-invalid-directive = {$directive} : directive invalide
stat-error-cannot-read-filesystem = impossible de lire la table des systèmes de fichiers montés : {$error}
stat-error-stdin-filesystem-mode = utiliser '-' pour désigner l'entrée standard ne fonctionne pas en mode système de fichiers
stat-error-cannot-read-filesystem-info = impossible de lire les informations du système de fichiers pour {$file} : {$error}
stat-error-cannot-stat = impossible d'obtenir le statut de {$file} : {$error}
## Messages d'avertissement
stat-warning-backslash-end-format = barre oblique inverse à la fin du format
stat-warning-unrecognized-escape-x = séquence d'échappement non reconnue '\x'
stat-warning-incomplete-hex-escape = séquence d'échappement hexadécimale incomplète '\x'
stat-warning-unrecognized-escape = séquence d'échappement non reconnue '\{$escape}'
## Messages de contexte SELinux
stat-selinux-failed-get-context = impossible d'obtenir le contexte de sécurité
stat-selinux-unsupported-system = non pris en charge sur ce système
stat-selinux-unsupported-os = non pris en charge pour ce système d'exploitation

View file

@ -4,7 +4,7 @@
// file that was distributed with this source code.
// spell-checker:ignore datetime
use uucore::error::{UResult, USimpleError};
use uucore::error::{UError, UResult, USimpleError};
use clap::builder::ValueParser;
use uucore::display::Quotable;
@ -26,7 +26,33 @@ use std::os::unix::prelude::OsStrExt;
use std::path::Path;
use std::{env, fs};
use uucore::locale::get_message;
use std::collections::HashMap;
use thiserror::Error;
use uucore::locale::{get_message, get_message_with_args};
#[derive(Debug, Error)]
enum StatError {
#[error("{}", get_message_with_args("stat-error-invalid-quoting-style", HashMap::from([("style".to_string(), style.clone())])))]
InvalidQuotingStyle { style: String },
#[error("{}", get_message("stat-error-missing-operand"))]
MissingOperand,
#[error("{}", get_message_with_args("stat-error-invalid-directive", HashMap::from([("directive".to_string(), directive.clone())])))]
InvalidDirective { directive: String },
#[error("{}", get_message_with_args("stat-error-cannot-read-filesystem", HashMap::from([("error".to_string(), error.clone())])))]
CannotReadFilesystem { error: String },
#[error("{}", get_message("stat-error-stdin-filesystem-mode"))]
StdinFilesystemMode,
#[error("{}", get_message_with_args("stat-error-cannot-read-filesystem-info", HashMap::from([("file".to_string(), file.clone()), ("error".to_string(), error.clone())])))]
CannotReadFilesystemInfo { file: String, error: String },
#[error("{}", get_message_with_args("stat-error-cannot-stat", HashMap::from([("file".to_string(), file.clone()), ("error".to_string(), error.clone())])))]
CannotStat { file: String, error: String },
}
impl UError for StatError {
fn code(&self) -> i32 {
1
}
}
mod options {
pub const DEREFERENCE: &str = "dereference";
@ -54,7 +80,10 @@ fn check_bound(slice: &str, bound: usize, beg: usize, end: usize) -> UResult<()>
if end >= bound {
return Err(USimpleError::new(
1,
format!("{}: invalid directive", slice[beg..end].quote()),
StatError::InvalidDirective {
directive: slice[beg..end].quote().to_string(),
}
.to_string(),
));
}
Ok(())
@ -104,7 +133,7 @@ enum QuotingStyle {
}
impl std::str::FromStr for QuotingStyle {
type Err = String;
type Err = StatError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
@ -112,7 +141,9 @@ impl std::str::FromStr for QuotingStyle {
"shell" => Ok(QuotingStyle::Shell),
"shell-escape-always" => Ok(QuotingStyle::ShellEscapeAlways),
// The others aren't exposed to the user
_ => Err(format!("Invalid quoting style: {s}")),
_ => Err(StatError::InvalidQuotingStyle {
style: s.to_string(),
}),
}
}
}
@ -144,24 +175,22 @@ trait ScanUtil {
}
impl ScanUtil for str {
/// Scans for a number at the beginning of the string
/// Returns the parsed number and the character count
/// Since we only deal with ASCII characters (+, -, 0-9), character count equals byte count
fn scan_num<F>(&self) -> Option<(F, usize)>
where
F: std::str::FromStr,
{
let mut chars = self.chars();
let mut i = 0;
match chars.next() {
Some('-' | '+' | '0'..='9') => i += 1,
_ => return None,
}
for c in chars {
match c {
'0'..='9' => i += 1,
_ => break,
}
}
if i > 0 {
F::from_str(&self[..i]).ok().map(|x| (x, i))
let count = chars
.next()
.filter(|&c| c.is_ascii_digit() || c == '-' || c == '+')
.map(|_| 1 + chars.take_while(char::is_ascii_digit).count())
.unwrap_or(0);
if count > 0 {
F::from_str(&self[..count]).ok().map(|x| (x, count))
} else {
None
}
@ -607,6 +636,17 @@ impl Stater {
}
}
/// Converts a character index to a byte index in a UTF-8 string
/// This is necessary because Rust strings are UTF-8 encoded, so character positions
/// don't always align with byte positions for multi-byte characters
fn char_index_to_byte_index(format_str: &str, char_index: usize) -> usize {
format_str
.char_indices()
.nth(char_index)
.map(|(byte_idx, _)| byte_idx)
.unwrap_or(format_str.len())
}
fn handle_percent_case(
chars: &[char],
i: &mut usize,
@ -632,7 +672,8 @@ impl Stater {
let mut precision = Precision::NotSpecified;
let mut j = *i;
if let Some((field_width, offset)) = format_str[j..].scan_num::<usize>() {
let j_byte = Self::char_index_to_byte_index(format_str, j);
if let Some((field_width, offset)) = format_str[j_byte..].scan_num::<usize>() {
width = field_width;
j += offset;
@ -641,7 +682,10 @@ impl Stater {
let invalid_directive: String = chars[old..=j.min(bound - 1)].iter().collect();
return Err(USimpleError::new(
1,
format!("{}: invalid directive", invalid_directive.quote()),
StatError::InvalidDirective {
directive: invalid_directive.quote().to_string(),
}
.to_string(),
));
}
}
@ -651,7 +695,8 @@ impl Stater {
j += 1;
check_bound(format_str, bound, old, j)?;
match format_str[j..].scan_num::<i32>() {
let j_byte = Self::char_index_to_byte_index(format_str, j);
match format_str[j_byte..].scan_num::<i32>() {
Some((value, offset)) => {
if value >= 0 {
precision = Precision::Number(value as usize);
@ -698,7 +743,7 @@ impl Stater {
) -> Token {
*i += 1;
if *i >= bound {
show_warning!("backslash at end of format");
show_warning!("{}", get_message("stat-warning-backslash-end-format"));
return Token::Char('\\');
}
match chars[*i] {
@ -728,22 +773,30 @@ impl Stater {
Token::Byte(value)
}
'x' => {
// Parse hexadecimal escape sequence
// Parse hexadecimal escape sequence (\xNN format)
// Uses UTF-8 safe byte indexing to handle multi-byte characters properly
if *i + 1 < bound {
if let Some((c, offset)) = format_str[*i + 1..].scan_char(16) {
let byte_index = Self::char_index_to_byte_index(format_str, *i + 1);
if let Some((c, offset)) = format_str[byte_index..].scan_char(16) {
*i += offset;
Token::Byte(c as u8)
} else {
show_warning!("unrecognized escape '\\x'");
show_warning!("{}", get_message("stat-warning-unrecognized-escape-x"));
Token::Byte(b'x')
}
} else {
show_warning!("incomplete hex escape '\\x'");
show_warning!("{}", get_message("stat-warning-incomplete-hex-escape"));
Token::Byte(b'x')
}
}
other => {
show_warning!("unrecognized escape '\\{other}'");
show_warning!(
"{}",
get_message_with_args(
"stat-warning-unrecognized-escape",
HashMap::from([("escape".to_string(), other.to_string())])
)
);
Token::Byte(other as u8)
}
}
@ -751,8 +804,8 @@ impl Stater {
fn generate_tokens(format_str: &str, use_printf: bool) -> UResult<Vec<Token>> {
let mut tokens = Vec::new();
let bound = format_str.len();
let chars = format_str.chars().collect::<Vec<char>>();
let bound = chars.len();
let mut i = 0;
while i < bound {
match chars.get(i) {
@ -785,10 +838,7 @@ impl Stater {
.map(|v| v.map(OsString::from).collect())
.unwrap_or_default();
if files.is_empty() {
return Err(Box::new(USimpleError {
code: 1,
message: "missing operand\nTry 'stat --help' for more information.".to_string(),
}));
return Err(Box::new(StatError::MissingOperand) as Box<dyn UError>);
}
let format_str = if matches.contains_id(options::PRINTF) {
matches
@ -819,8 +869,13 @@ impl Stater {
} else {
let mut mount_list = read_fs_list()
.map_err(|e| {
let context = "cannot read table of mounted file systems";
USimpleError::new(e.code(), format!("{context}: {e}"))
USimpleError::new(
e.code(),
StatError::CannotReadFilesystem {
error: e.to_string(),
}
.to_string(),
)
})?
.iter()
.map(|mi| mi.mount_dir.clone())
@ -907,17 +962,17 @@ impl Stater {
match uucore::selinux::get_selinux_security_context(Path::new(file))
{
Ok(ctx) => OutputType::Str(ctx),
Err(_) => OutputType::Str(
"failed to get security context".to_string(),
),
Err(_) => OutputType::Str(get_message(
"stat-selinux-failed-get-context",
)),
}
} else {
OutputType::Str("unsupported on this system".to_string())
OutputType::Str(get_message("stat-selinux-unsupported-system"))
}
}
#[cfg(not(feature = "selinux"))]
{
OutputType::Str("unsupported for this operating system".to_string())
OutputType::Str(get_message("stat-selinux-unsupported-os"))
}
}
// device number in decimal
@ -1028,7 +1083,7 @@ impl Stater {
let display_name = file.to_string_lossy();
let file = if cfg!(unix) && display_name == "-" {
if self.show_fs {
show_error!("using '-' to denote standard input does not work in file system mode");
show_error!("{}", StatError::StdinFilesystemMode);
return 1;
}
if let Ok(p) = Path::new("/dev/stdin").canonicalize() {
@ -1055,8 +1110,11 @@ impl Stater {
}
Err(e) => {
show_error!(
"cannot read file system information for {}: {e}",
display_name.quote(),
"{}",
StatError::CannotReadFilesystemInfo {
file: display_name.quote().to_string(),
error: e.to_string()
}
);
return 1;
}
@ -1092,7 +1150,13 @@ impl Stater {
}
}
Err(e) => {
show_error!("cannot stat {}: {e}", display_name.quote());
show_error!(
"{}",
StatError::CannotStat {
file: display_name.quote().to_string(),
error: e.to_string()
}
);
return 1;
}
}
@ -1107,25 +1171,64 @@ impl Stater {
if terse {
"%n %i %l %t %s %S %b %f %a %c %d\n".into()
} else {
" File: \"%n\"\n ID: %-8i Namelen: %-7l Type: %T\nBlock \
size: %-10s Fundamental block size: %S\nBlocks: Total: %-10b \
Free: %-10f Available: %a\nInodes: Total: %-10c Free: %d\n"
.into()
format!(
" {}: \"%n\"\n {}: %-8i {}: %-7l {}: %T\n{} \
{}: %-10s {} {}: %S\n{}: {}: %-10b \
{}: %-10f {}: %a\n{}: {}: %-10c {}: %d\n",
get_message("stat-word-file"),
get_message("stat-word-id"),
get_message("stat-word-namelen"),
get_message("stat-word-type"),
get_message("stat-word-block"),
get_message("stat-word-size"),
get_message("stat-word-fundamental"),
get_message("stat-word-block-size"),
get_message("stat-word-blocks"),
get_message("stat-word-total"),
get_message("stat-word-free"),
get_message("stat-word-available"),
get_message("stat-word-inodes"),
get_message("stat-word-total"),
get_message("stat-word-free")
)
}
} else if terse {
"%n %s %b %f %u %g %D %i %h %t %T %X %Y %Z %W %o\n".into()
} else {
[
" File: %N\n Size: %-10s\tBlocks: %-10b IO Block: %-6o %F\n",
if show_dev_type {
"Device: %Dh/%dd\tInode: %-10i Links: %-5h Device type: %t,%T\n"
} else {
"Device: %Dh/%dd\tInode: %-10i Links: %h\n"
},
"Access: (%04a/%10.10A) Uid: (%5u/%8U) Gid: (%5g/%8G)\n",
"Access: %x\nModify: %y\nChange: %z\n Birth: %w\n",
]
.join("")
let device_line = if show_dev_type {
format!(
"{}: %Dh/%dd\t{}: %-10i {}: %-5h {} {}: %t,%T\n",
get_message("stat-word-device"),
get_message("stat-word-inode"),
get_message("stat-word-links"),
get_message("stat-word-device"),
get_message("stat-word-type")
)
} else {
format!(
"{}: %Dh/%dd\t{}: %-10i {}: %h\n",
get_message("stat-word-device"),
get_message("stat-word-inode"),
get_message("stat-word-links")
)
};
format!(
" {}: %N\n {}: %-10s\t{}: %-10b {} {}: %-6o %F\n{}{}: (%04a/%10.10A) {}: (%5u/%8U) {}: (%5g/%8G)\n{}: %x\n{}: %y\n{}: %z\n {}: %w\n",
get_message("stat-word-file"),
get_message("stat-word-size"),
get_message("stat-word-blocks"),
get_message("stat-word-io"),
get_message("stat-word-block"),
device_line,
get_message("stat-word-access"),
get_message("stat-word-uid"),
get_message("stat-word-gid"),
get_message("stat-word-access"),
get_message("stat-word-modify"),
get_message("stat-word-change"),
get_message("stat-word-birth")
)
}
}
}
@ -1155,42 +1258,35 @@ pub fn uu_app() -> Command {
Arg::new(options::DEREFERENCE)
.short('L')
.long(options::DEREFERENCE)
.help("follow links")
.help(get_message("stat-help-dereference"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::FILE_SYSTEM)
.short('f')
.long(options::FILE_SYSTEM)
.help("display file system status instead of file status")
.help(get_message("stat-help-file-system"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::TERSE)
.short('t')
.long(options::TERSE)
.help("print the information in terse form")
.help(get_message("stat-help-terse"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::FORMAT)
.short('c')
.long(options::FORMAT)
.help(
"use the specified FORMAT instead of the default;
output a newline after each use of FORMAT",
)
.help(get_message("stat-help-format"))
.value_name("FORMAT"),
)
.arg(
Arg::new(options::PRINTF)
.long(options::PRINTF)
.value_name("FORMAT")
.help(
"like --format, but interpret backslash escapes,
and do not output a mandatory trailing newline;
if you want a newline, include \n in FORMAT",
),
.help(get_message("stat-help-printf")),
)
.arg(
Arg::new(options::FILES)