mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-28 11:37:44 +00:00
locale: refactor the locale system:
* remove the default value. Avoid duplication of the english string + facilitate translation * have english as a default. Load english when the translated string isn't available
This commit is contained in:
parent
fa17dc7809
commit
72597bcf7b
1 changed files with 120 additions and 117 deletions
|
@ -45,27 +45,48 @@ impl UError for LocalizationError {
|
||||||
|
|
||||||
pub const DEFAULT_LOCALE: &str = "en-US";
|
pub const DEFAULT_LOCALE: &str = "en-US";
|
||||||
|
|
||||||
// A struct to handle localization
|
// A struct to handle localization with optional English fallback
|
||||||
struct Localizer {
|
struct Localizer {
|
||||||
bundle: FluentBundle<FluentResource>,
|
primary_bundle: FluentBundle<FluentResource>,
|
||||||
|
fallback_bundle: Option<FluentBundle<FluentResource>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Localizer {
|
impl Localizer {
|
||||||
fn new(bundle: FluentBundle<FluentResource>) -> Self {
|
fn new(primary_bundle: FluentBundle<FluentResource>) -> Self {
|
||||||
Self { bundle }
|
Self {
|
||||||
|
primary_bundle,
|
||||||
|
fallback_bundle: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format(&self, id: &str, args: Option<&FluentArgs>, default: &str) -> String {
|
fn with_fallback(mut self, fallback_bundle: FluentBundle<FluentResource>) -> Self {
|
||||||
match self.bundle.get_message(id).and_then(|m| m.value()) {
|
self.fallback_bundle = Some(fallback_bundle);
|
||||||
Some(value) => {
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format(&self, id: &str, args: Option<&FluentArgs>) -> String {
|
||||||
|
// Try primary bundle first
|
||||||
|
if let Some(message) = self.primary_bundle.get_message(id).and_then(|m| m.value()) {
|
||||||
let mut errs = Vec::new();
|
let mut errs = Vec::new();
|
||||||
self.bundle
|
return self
|
||||||
.format_pattern(value, args, &mut errs)
|
.primary_bundle
|
||||||
.to_string()
|
.format_pattern(message, args, &mut errs)
|
||||||
|
.to_string();
|
||||||
}
|
}
|
||||||
None => default.to_string(),
|
|
||||||
|
// Fall back to English bundle if available
|
||||||
|
if let Some(ref fallback) = self.fallback_bundle {
|
||||||
|
if let Some(message) = fallback.get_message(id).and_then(|m| m.value()) {
|
||||||
|
let mut errs = Vec::new();
|
||||||
|
return fallback
|
||||||
|
.format_pattern(message, args, &mut errs)
|
||||||
|
.to_string();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return the key ID if not found anywhere
|
||||||
|
id.to_string()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global localizer stored in thread-local OnceLock
|
// Global localizer stored in thread-local OnceLock
|
||||||
|
@ -76,117 +97,117 @@ thread_local! {
|
||||||
// Initialize localization with a specific locale and config
|
// Initialize localization with a specific locale and config
|
||||||
fn init_localization(
|
fn init_localization(
|
||||||
locale: &LanguageIdentifier,
|
locale: &LanguageIdentifier,
|
||||||
config: &LocalizationConfig,
|
locales_dir: &Path,
|
||||||
) -> Result<(), LocalizationError> {
|
) -> Result<(), LocalizationError> {
|
||||||
let bundle = create_bundle(locale, config)?;
|
let en_locale = LanguageIdentifier::from_str(DEFAULT_LOCALE)
|
||||||
|
.expect("Default locale should always be valid");
|
||||||
|
|
||||||
|
let english_bundle = create_bundle(&en_locale, locales_dir)?;
|
||||||
|
let loc = if locale == &en_locale {
|
||||||
|
// If requesting English, just use English as primary (no fallback needed)
|
||||||
|
Localizer::new(english_bundle)
|
||||||
|
} else {
|
||||||
|
// Try to load the requested locale
|
||||||
|
if let Ok(primary_bundle) = create_bundle(locale, locales_dir) {
|
||||||
|
// Successfully loaded requested locale, load English as fallback
|
||||||
|
Localizer::new(primary_bundle).with_fallback(english_bundle)
|
||||||
|
} else {
|
||||||
|
// Failed to load requested locale, just use English as primary
|
||||||
|
Localizer::new(english_bundle)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
LOCALIZER.with(|lock| {
|
LOCALIZER.with(|lock| {
|
||||||
let loc = Localizer::new(bundle);
|
|
||||||
lock.set(loc)
|
lock.set(loc)
|
||||||
.map_err(|_| LocalizationError::Bundle("Localizer already initialized".into()))
|
.map_err(|_| LocalizationError::Bundle("Localizer already initialized".into()))
|
||||||
})?;
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a bundle for a locale with fallback chain
|
// Create a bundle for a specific locale
|
||||||
fn create_bundle(
|
fn create_bundle(
|
||||||
locale: &LanguageIdentifier,
|
locale: &LanguageIdentifier,
|
||||||
config: &LocalizationConfig,
|
locales_dir: &Path,
|
||||||
) -> Result<FluentBundle<FluentResource>, LocalizationError> {
|
) -> Result<FluentBundle<FluentResource>, LocalizationError> {
|
||||||
// Create a new bundle with requested locale
|
let locale_path = locales_dir.join(format!("{locale}.ftl"));
|
||||||
let mut bundle = FluentBundle::new(vec![locale.clone()]);
|
|
||||||
|
|
||||||
// Try to load the requested locale
|
let ftl_file = fs::read_to_string(&locale_path).map_err(|e| LocalizationError::Io {
|
||||||
let mut locales_to_try = vec![locale.clone()];
|
source: e,
|
||||||
locales_to_try.extend_from_slice(&config.fallback_locales);
|
path: locale_path.clone(),
|
||||||
|
})?;
|
||||||
|
|
||||||
// Try each locale in the chain
|
|
||||||
let mut tried_paths = Vec::new();
|
|
||||||
|
|
||||||
for try_locale in locales_to_try {
|
|
||||||
let locale_path = config.get_locale_path(&try_locale);
|
|
||||||
tried_paths.push(locale_path.clone());
|
|
||||||
|
|
||||||
if let Ok(ftl_file) = fs::read_to_string(&locale_path) {
|
|
||||||
let resource = FluentResource::try_new(ftl_file).map_err(|_| {
|
let resource = FluentResource::try_new(ftl_file).map_err(|_| {
|
||||||
LocalizationError::Parse(format!(
|
LocalizationError::Parse(format!(
|
||||||
"Failed to parse localization resource for {}",
|
"Failed to parse localization resource for {}: {}",
|
||||||
try_locale
|
locale,
|
||||||
|
locale_path.display()
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
bundle.add_resource(resource).map_err(|_| {
|
let mut bundle = FluentBundle::new(vec![locale.clone()]);
|
||||||
|
|
||||||
|
bundle.add_resource(resource).map_err(|errs| {
|
||||||
LocalizationError::Bundle(format!(
|
LocalizationError::Bundle(format!(
|
||||||
"Failed to add resource to bundle for {}",
|
"Failed to add resource to bundle for {}: {:?}",
|
||||||
try_locale
|
locale, errs
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
return Ok(bundle);
|
Ok(bundle)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let paths_str = tried_paths
|
|
||||||
.iter()
|
|
||||||
.map(|p| p.to_string_lossy().to_string())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ");
|
|
||||||
|
|
||||||
Err(LocalizationError::Io {
|
|
||||||
source: std::io::Error::new(std::io::ErrorKind::NotFound, "No localization files found"),
|
|
||||||
path: PathBuf::from(paths_str),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_message_internal(id: &str, args: Option<FluentArgs>, default: &str) -> String {
|
fn get_message_internal(id: &str, args: Option<FluentArgs>) -> String {
|
||||||
LOCALIZER.with(|lock| {
|
LOCALIZER.with(|lock| {
|
||||||
lock.get()
|
lock.get()
|
||||||
.map(|loc| loc.format(id, args.as_ref(), default))
|
.map(|loc| loc.format(id, args.as_ref()))
|
||||||
.unwrap_or_else(|| default.to_string())
|
.unwrap_or_else(|| id.to_string()) // Return the key ID if localizer not initialized
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieves a localized message by its identifier.
|
/// Retrieves a localized message by its identifier.
|
||||||
///
|
///
|
||||||
/// Looks up a message with the given ID in the current locale bundle and returns
|
/// Looks up a message with the given ID in the current locale bundle and returns
|
||||||
/// the localized text. If the message ID is not found, returns the provided default text.
|
/// the localized text. If the message ID is not found in the current locale,
|
||||||
|
/// it will fall back to English. If the message is not found in English either,
|
||||||
|
/// returns the message ID itself.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `id` - The message identifier in the Fluent resources
|
/// * `id` - The message identifier in the Fluent resources
|
||||||
/// * `default` - Default text to use if the message ID isn't found
|
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
///
|
///
|
||||||
/// A `String` containing either the localized message or the default text
|
/// A `String` containing the localized message, or the message ID if not found
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use uucore::locale::get_message;
|
/// use uucore::locale::get_message;
|
||||||
///
|
///
|
||||||
/// // Get a localized greeting or fall back to English
|
/// // Get a localized greeting (from .ftl files)
|
||||||
/// let greeting = get_message("greeting", "Hello, World!");
|
/// let greeting = get_message("greeting");
|
||||||
/// println!("{}", greeting);
|
/// println!("{}", greeting);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn get_message(id: &str, default: &str) -> String {
|
pub fn get_message(id: &str) -> String {
|
||||||
get_message_internal(id, None, default)
|
get_message_internal(id, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieves a localized message with variable substitution.
|
/// Retrieves a localized message with variable substitution.
|
||||||
///
|
///
|
||||||
/// Looks up a message with the given ID in the current locale bundle,
|
/// Looks up a message with the given ID in the current locale bundle,
|
||||||
/// substitutes variables from the provided arguments map, and returns the
|
/// substitutes variables from the provided arguments map, and returns the
|
||||||
/// localized text. If the message ID is not found, returns the provided default text.
|
/// localized text. If the message ID is not found in the current locale,
|
||||||
|
/// it will fall back to English. If the message is not found in English either,
|
||||||
|
/// returns the message ID itself.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `id` - The message identifier in the Fluent resources
|
/// * `id` - The message identifier in the Fluent resources
|
||||||
/// * `ftl_args` - Key-value pairs for variable substitution in the message
|
/// * `ftl_args` - Key-value pairs for variable substitution in the message
|
||||||
/// * `default` - Default text to use if the message ID isn't found
|
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
///
|
///
|
||||||
/// A `String` containing either the localized message with variable substitution or the default text
|
/// A `String` containing the localized message with variable substitution, or the message ID if not found
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
|
@ -199,44 +220,25 @@ pub fn get_message(id: &str, default: &str) -> String {
|
||||||
/// args.insert("name".to_string(), "Alice".to_string());
|
/// args.insert("name".to_string(), "Alice".to_string());
|
||||||
/// args.insert("count".to_string(), "3".to_string());
|
/// args.insert("count".to_string(), "3".to_string());
|
||||||
///
|
///
|
||||||
/// let message = get_message_with_args(
|
/// let message = get_message_with_args("notification", args);
|
||||||
/// "notification",
|
|
||||||
/// args,
|
|
||||||
/// "Hello! You have notifications."
|
|
||||||
/// );
|
|
||||||
/// println!("{}", message);
|
/// println!("{}", message);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn get_message_with_args(id: &str, ftl_args: HashMap<String, String>, default: &str) -> String {
|
pub fn get_message_with_args(id: &str, ftl_args: HashMap<String, String>) -> String {
|
||||||
let args = ftl_args.into_iter().collect();
|
let mut args = FluentArgs::new();
|
||||||
get_message_internal(id, Some(args), default)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configuration for localization
|
for (key, value) in ftl_args {
|
||||||
#[derive(Clone)]
|
// Try to parse as number first for proper pluralization support
|
||||||
struct LocalizationConfig {
|
if let Ok(num_val) = value.parse::<i64>() {
|
||||||
locales_dir: PathBuf,
|
args.set(key, num_val);
|
||||||
fallback_locales: Vec<LanguageIdentifier>,
|
} else if let Ok(float_val) = value.parse::<f64>() {
|
||||||
}
|
args.set(key, float_val);
|
||||||
|
} else {
|
||||||
impl LocalizationConfig {
|
// Keep as string if not a number
|
||||||
// Create a new config with a specific locales directory
|
args.set(key, value);
|
||||||
fn new<P: AsRef<Path>>(locales_dir: P) -> Self {
|
|
||||||
Self {
|
|
||||||
locales_dir: locales_dir.as_ref().to_path_buf(),
|
|
||||||
fallback_locales: vec![],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set fallback locales
|
get_message_internal(id, Some(args))
|
||||||
fn with_fallbacks(mut self, fallbacks: Vec<LanguageIdentifier>) -> Self {
|
|
||||||
self.fallback_locales = fallbacks;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get path for a specific locale
|
|
||||||
fn get_locale_path(&self, locale: &LanguageIdentifier) -> PathBuf {
|
|
||||||
self.locales_dir.join(format!("{}.ftl", locale))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to detect system locale from environment variables
|
// Function to detect system locale from environment variables
|
||||||
|
@ -252,11 +254,12 @@ fn detect_system_locale() -> Result<LanguageIdentifier, LocalizationError> {
|
||||||
.map_err(|_| LocalizationError::Parse(format!("Failed to parse locale: {}", locale_str)))
|
.map_err(|_| LocalizationError::Parse(format!("Failed to parse locale: {}", locale_str)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets up localization using the system locale (or default) and project paths.
|
/// Sets up localization using the system locale with English fallback.
|
||||||
///
|
///
|
||||||
/// This function initializes the localization system based on the system's locale
|
/// This function initializes the localization system based on the system's locale
|
||||||
/// preferences (via the LANG environment variable) or falls back to the default locale
|
/// preferences (via the LANG environment variable) or falls back to English
|
||||||
/// if the system locale cannot be determined or is invalid.
|
/// if the system locale cannot be determined or the locale file doesn't exist.
|
||||||
|
/// English is always loaded as a fallback.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
|
@ -270,8 +273,8 @@ fn detect_system_locale() -> Result<LanguageIdentifier, LocalizationError> {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns a `LocalizationError` if:
|
/// Returns a `LocalizationError` if:
|
||||||
/// * The localization files cannot be read
|
/// * The en-US.ftl file cannot be read (English is required)
|
||||||
/// * The files contain invalid syntax
|
/// * The files contain invalid Fluent syntax
|
||||||
/// * The bundle cannot be initialized properly
|
/// * The bundle cannot be initialized properly
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
|
@ -280,6 +283,8 @@ fn detect_system_locale() -> Result<LanguageIdentifier, LocalizationError> {
|
||||||
/// use uucore::locale::setup_localization;
|
/// use uucore::locale::setup_localization;
|
||||||
///
|
///
|
||||||
/// // Initialize localization using files in the "locales" directory
|
/// // Initialize localization using files in the "locales" directory
|
||||||
|
/// // Make sure you have at least an "en-US.ftl" file in this directory
|
||||||
|
/// // Other locale files like "fr-FR.ftl" are optional
|
||||||
/// match setup_localization("./locales") {
|
/// match setup_localization("./locales") {
|
||||||
/// Ok(_) => println!("Localization initialized successfully"),
|
/// Ok(_) => println!("Localization initialized successfully"),
|
||||||
/// Err(e) => eprintln!("Failed to initialize localization: {}", e),
|
/// Err(e) => eprintln!("Failed to initialize localization: {}", e),
|
||||||
|
@ -290,14 +295,12 @@ pub fn setup_localization(p: &str) -> Result<(), LocalizationError> {
|
||||||
LanguageIdentifier::from_str(DEFAULT_LOCALE).expect("Default locale should always be valid")
|
LanguageIdentifier::from_str(DEFAULT_LOCALE).expect("Default locale should always be valid")
|
||||||
});
|
});
|
||||||
|
|
||||||
let locales_dir = PathBuf::from(p);
|
let coreutils_path = PathBuf::from(format!("src/uu/{p}/locales/"));
|
||||||
let fallback_locales = vec![
|
let locales_dir = if coreutils_path.exists() {
|
||||||
LanguageIdentifier::from_str(DEFAULT_LOCALE)
|
coreutils_path
|
||||||
.expect("Default locale should always be valid"),
|
} else {
|
||||||
];
|
PathBuf::from(p)
|
||||||
|
};
|
||||||
|
|
||||||
let config = LocalizationConfig::new(locales_dir).with_fallbacks(fallback_locales);
|
init_localization(&locale, &locales_dir)
|
||||||
|
|
||||||
init_localization(&locale, &config)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue