l10n: use the embedded english strings when relevant

This commit is contained in:
Sylvestre Ledru
2025-08-03 10:59:38 +02:00
parent f1b377fa99
commit a432fe811f
7 changed files with 245 additions and 68 deletions
Generated
+1
View File
@@ -4083,6 +4083,7 @@ dependencies = [
"dns-lookup",
"dunce",
"fluent",
"fluent-bundle",
"fluent-syntax",
"glob",
"hex",
+1
View File
@@ -378,6 +378,7 @@ digest = "0.10.7"
# Fluent dependencies
fluent = "0.17.0"
fluent-bundle = "0.16.0"
unic-langid = "0.9.6"
fluent-syntax = "0.12.0"
+1
View File
@@ -1606,6 +1606,7 @@ dependencies = [
"digest",
"dunce",
"fluent",
"fluent-bundle",
"fluent-syntax",
"glob",
"hex",
+2 -1
View File
@@ -71,10 +71,11 @@ icu_decimal = { workspace = true, optional = true, features = [
icu_locale = { workspace = true, optional = true, features = ["compiled_data"] }
icu_provider = { workspace = true, optional = true }
# Fluent dependencies
# Fluent dependencies (always available for localization)
fluent = { workspace = true }
fluent-syntax = { workspace = true }
unic-langid = { workspace = true }
fluent-bundle = { workspace = true }
thiserror = { workspace = true }
[target.'cfg(unix)'.dependencies]
walkdir = { workspace = true, optional = true }
+93
View File
@@ -0,0 +1,93 @@
// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use std::env;
use std::path::Path;
fn main() {
println!("cargo:rerun-if-changed=build.rs");
let out_dir = env::var("OUT_DIR").unwrap();
// Always generate embedded English locale files for fallback
generate_embedded_english_locales(&out_dir).unwrap();
}
/// Generate embedded English locale files
///
/// # Errors
///
/// Returns an error if file operations fail or if there are I/O issues
fn generate_embedded_english_locales(out_dir: &str) -> Result<(), Box<dyn std::error::Error>> {
use std::fs::{self, File};
use std::io::Write;
// Since we're in uucore, we need to go up to the project root
let project_root = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent() // src/
.and_then(|p| p.parent()) // project root
.ok_or("Failed to find project root")?;
let mut embedded_file = File::create(Path::new(out_dir).join("embedded_locales.rs"))?;
writeln!(embedded_file, "// Generated at compile time - do not edit")?;
writeln!(
embedded_file,
"// This file contains embedded English locale files for all utilities"
)?;
writeln!(embedded_file)?;
writeln!(embedded_file, "use std::collections::HashMap;")?;
writeln!(embedded_file)?;
// Start the function that returns embedded locales
writeln!(
embedded_file,
"pub fn get_embedded_locales() -> HashMap<&'static str, &'static str> {{"
)?;
writeln!(embedded_file, " let mut locales = HashMap::new();")?;
writeln!(embedded_file)?;
// Scan for all utilities in src/uu/
let uu_dir = project_root.join("src/uu");
for entry in fs::read_dir(&uu_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
let util_name = entry.file_name().to_string_lossy().to_string();
let locale_path = path.join("locales/en-US.ftl");
if locale_path.exists() {
let content = fs::read_to_string(&locale_path)?;
writeln!(embedded_file, " // Locale for {util_name}")?;
writeln!(
embedded_file,
" locales.insert(\"{util_name}/en-US.ftl\", r###\"{content}\"###);"
)?;
writeln!(embedded_file)?;
// Tell Cargo to rerun if this file changes
println!("cargo:rerun-if-changed={}", locale_path.display());
}
}
}
// Also embed uucore locale file if it exists
let uucore_locale_path = project_root.join("src/uucore/locales/en-US.ftl");
if uucore_locale_path.exists() {
let content = fs::read_to_string(&uucore_locale_path)?;
writeln!(embedded_file, " // Common uucore locale")?;
writeln!(
embedded_file,
" locales.insert(\"uucore/en-US.ftl\", r###\"{content}\"###);"
)?;
println!("cargo:rerun-if-changed={}", uucore_locale_path.display());
}
writeln!(embedded_file)?;
writeln!(embedded_file, " locales")?;
writeln!(embedded_file, "}}")?;
embedded_file.flush()?;
Ok(())
}
+13 -11
View File
@@ -179,17 +179,19 @@ macro_rules! bin {
use uucore::locale;
// suppress extraneous error output for SIGPIPE failures/panics
uucore::panic::mute_sigpipe_panic();
locale::setup_localization(uucore::get_canonical_util_name(stringify!($util)))
.unwrap_or_else(|err| {
match err {
uucore::locale::LocalizationError::ParseResource {
error: err_msg,
snippet,
} => eprintln!("Localization parse error at {snippet}: {err_msg}"),
other => eprintln!("Could not init the localization system: {other}"),
}
std::process::exit(99)
});
locale::setup_localization_with_common(uucore::get_canonical_util_name(stringify!(
$util
)))
.unwrap_or_else(|err| {
match err {
uucore::locale::LocalizationError::ParseResource {
error: err_msg,
snippet,
} => eprintln!("Localization parse error at {snippet}: {err_msg:?}"),
other => eprintln!("Could not init the localization system: {other}"),
}
std::process::exit(99)
});
// execute utility code
let code = $util::uumain(uucore::args_os());
+134 -56
View File
@@ -55,6 +55,9 @@ impl UError for LocalizationError {
pub const DEFAULT_LOCALE: &str = "en-US";
// Include embedded locale files as fallback
include!(concat!(env!("OUT_DIR"), "/embedded_locales.rs"));
// A struct to handle localization with optional English fallback
struct Localizer {
primary_bundle: FluentBundle<FluentResource>,
@@ -108,12 +111,18 @@ thread_local! {
fn init_localization(
locale: &LanguageIdentifier,
locales_dir: &Path,
util_name: &str,
) -> Result<(), LocalizationError> {
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 {
// Try to load English from filesystem, fall back to embedded if that fails
let english_bundle = create_bundle(&default_locale, locales_dir).or_else(|_| {
// Try embedded English as fallback
create_bundle_from_embedded(&default_locale, util_name)
})?;
let loc = if locale == &default_locale {
// If requesting English, just use English as primary (no fallback needed)
Localizer::new(english_bundle)
} else {
@@ -180,6 +189,52 @@ fn create_bundle(
Ok(bundle)
}
/// Create a bundle from embedded locale files
fn create_bundle_from_embedded(
locale: &LanguageIdentifier,
util_name: &str,
) -> Result<FluentBundle<FluentResource>, LocalizationError> {
// Only support English from embedded files
if *locale != "en-US" {
return Err(LocalizationError::LocalesDirNotFound(
"Embedded locales only support en-US".to_string(),
));
}
let embedded_locales = get_embedded_locales();
let locale_key = format!("{util_name}/en-US.ftl");
let ftl_content = embedded_locales.get(locale_key.as_str()).ok_or_else(|| {
LocalizationError::LocalesDirNotFound(format!("No embedded locale found for {util_name}"))
})?;
let resource = FluentResource::try_new(ftl_content.to_string()).map_err(
|(_partial_resource, mut errs): (FluentResource, Vec<ParserError>)| {
let first_err = errs.remove(0);
let snippet = if let Some(range) = first_err.slice.clone() {
ftl_content.get(range).unwrap_or("").to_string()
} else {
String::new()
};
LocalizationError::ParseResource {
error: first_err,
snippet,
}
},
)?;
let mut bundle = FluentBundle::new(vec![locale.clone()]);
bundle.set_use_isolating(false);
bundle.add_resource(resource).map_err(|errs| {
LocalizationError::Bundle(format!(
"Failed to add embedded resource to bundle for {locale}: {errs:?}",
))
})?;
Ok(bundle)
}
fn get_message_internal(id: &str, args: Option<FluentArgs>) -> String {
LOCALIZER.with(|lock| {
lock.get()
@@ -305,8 +360,27 @@ pub fn setup_localization(p: &str) -> Result<(), LocalizationError> {
LanguageIdentifier::from_str(DEFAULT_LOCALE).expect("Default locale should always be valid")
});
let locales_dir = get_locales_dir(p)?;
init_localization(&locale, &locales_dir)
// Try to get locales from filesystem first
match get_locales_dir(p) {
Ok(locales_dir) => init_localization(&locale, &locales_dir, p),
Err(_) => {
// Fallback to embedded English locales
let default_locale = LanguageIdentifier::from_str(DEFAULT_LOCALE)
.expect("Default locale should always be valid");
let english_bundle = create_bundle_from_embedded(&default_locale, p)?;
let localizer = Localizer::new(english_bundle);
LOCALIZER.with(|lock| {
lock.set(localizer)
.map_err(|_| LocalizationError::Bundle("Localizer already initialized".into()))
})?;
Ok(())
}
}
}
pub fn setup_localization_with_common(util_name: &str) -> Result<(), LocalizationError> {
setup_localization(util_name)
}
#[cfg(not(debug_assertions))]
@@ -603,6 +677,7 @@ invalid-syntax = This is { $missing
#[test]
fn test_localizer_format_with_args() {
use fluent::FluentArgs;
let temp_dir = create_test_locales_dir();
let en_bundle = create_bundle(
&LanguageIdentifier::from_str("en-US").unwrap(),
@@ -664,7 +739,10 @@ invalid-syntax = This is { $missing
let temp_dir = create_test_locales_dir();
let locale = LanguageIdentifier::from_str("en-US").unwrap();
let result = init_localization(&locale, temp_dir.path());
let result = init_localization(&locale, temp_dir.path(), "nonexistent_test_util");
if let Err(e) = &result {
eprintln!("Init localization failed: {}", e);
}
assert!(result.is_ok());
// Test that we can get messages
@@ -681,7 +759,7 @@ invalid-syntax = This is { $missing
let temp_dir = create_test_locales_dir();
let locale = LanguageIdentifier::from_str("fr-FR").unwrap();
let result = init_localization(&locale, temp_dir.path());
let result = init_localization(&locale, temp_dir.path(), "nonexistent_test_util");
assert!(result.is_ok());
// Test French message
@@ -702,7 +780,7 @@ invalid-syntax = This is { $missing
let temp_dir = create_test_locales_dir();
let locale = LanguageIdentifier::from_str("de-DE").unwrap(); // No German file
let result = init_localization(&locale, temp_dir.path());
let result = init_localization(&locale, temp_dir.path(), "nonexistent_test_util");
assert!(result.is_ok());
// Should use English as primary since German failed to load
@@ -720,11 +798,11 @@ invalid-syntax = This is { $missing
let locale = LanguageIdentifier::from_str("en-US").unwrap();
// Initialize once
let result1 = init_localization(&locale, temp_dir.path());
let result1 = init_localization(&locale, temp_dir.path(), "test");
assert!(result1.is_ok());
// Try to initialize again - should fail
let result2 = init_localization(&locale, temp_dir.path());
let result2 = init_localization(&locale, temp_dir.path(), "test");
assert!(result2.is_err());
match result2 {
@@ -744,7 +822,7 @@ invalid-syntax = This is { $missing
let temp_dir = create_test_locales_dir();
let locale = LanguageIdentifier::from_str("fr-FR").unwrap();
init_localization(&locale, temp_dir.path()).unwrap();
init_localization(&locale, temp_dir.path(), "nonexistent_test_util").unwrap();
let message = get_message("greeting");
assert_eq!(message, "Bonjour, le monde!");
@@ -765,11 +843,12 @@ invalid-syntax = This is { $missing
#[test]
fn test_get_message_with_args() {
use fluent::FluentArgs;
std::thread::spawn(|| {
let temp_dir = create_test_locales_dir();
let locale = LanguageIdentifier::from_str("en-US").unwrap();
init_localization(&locale, temp_dir.path()).unwrap();
init_localization(&locale, temp_dir.path(), "nonexistent_test_util").unwrap();
let mut args = FluentArgs::new();
args.set("name".to_string(), "Bob".to_string());
@@ -783,11 +862,12 @@ invalid-syntax = This is { $missing
#[test]
fn test_get_message_with_args_pluralization() {
use fluent::FluentArgs;
std::thread::spawn(|| {
let temp_dir = create_test_locales_dir();
let locale = LanguageIdentifier::from_str("en-US").unwrap();
init_localization(&locale, temp_dir.path()).unwrap();
init_localization(&locale, temp_dir.path(), "nonexistent_test_util").unwrap();
// Test singular
let mut args1 = FluentArgs::new();
@@ -804,37 +884,26 @@ invalid-syntax = This is { $missing
.join()
.unwrap();
}
#[test]
fn test_detect_system_locale_from_lang_env() {
// Save current LANG value
let original_lang = env::var("LANG").ok();
// Test locale parsing logic directly instead of relying on environment variables
// which can have race conditions in multi-threaded test environments
// Test with a valid locale
unsafe {
env::set_var("LANG", "fr-FR.UTF-8");
}
let result = detect_system_locale();
assert!(result.is_ok());
assert_eq!(result.unwrap(), "fr-FR");
// Test parsing logic with UTF-8 encoding
let locale_with_encoding = "fr-FR.UTF-8";
let parsed = locale_with_encoding.split('.').next().unwrap();
let lang_id = LanguageIdentifier::from_str(parsed).unwrap();
assert_eq!(lang_id.to_string(), "fr-FR");
// Test with locale without encoding
unsafe {
env::set_var("LANG", "es-ES");
}
let result = detect_system_locale();
assert!(result.is_ok());
assert_eq!(result.unwrap(), "es-ES");
// Test parsing logic without encoding
let locale_without_encoding = "es-ES";
let lang_id = LanguageIdentifier::from_str(locale_without_encoding).unwrap();
assert_eq!(lang_id.to_string(), "es-ES");
// Restore original LANG value
if let Some(val) = original_lang {
unsafe {
env::set_var("LANG", val);
}
} else {
unsafe {
env::remove_var("LANG");
}
}
// Test that DEFAULT_LOCALE is valid
let default_lang_id = LanguageIdentifier::from_str(DEFAULT_LOCALE).unwrap();
assert_eq!(default_lang_id.to_string(), "en-US");
}
#[test]
@@ -928,19 +997,24 @@ invalid-syntax = This is { $missing
}
#[test]
fn test_setup_localization_missing_english_file() {
fn test_setup_localization_fallback_to_embedded() {
std::thread::spawn(|| {
let temp_dir = TempDir::new().unwrap(); // Empty directory
let result = setup_localization(temp_dir.path().to_str().unwrap());
assert!(result.is_err());
match result {
Err(LocalizationError::Io { source: _, path }) => {
assert!(path.to_string_lossy().contains("en-US.ftl"));
}
_ => panic!("Expected IO error for missing English file"),
// Force English locale for this test
unsafe {
std::env::set_var("LANG", "en-US");
}
// Test with a utility name that has embedded locales
// This should fall back to embedded English when filesystem files aren't found
let result = setup_localization("test");
if let Err(e) = &result {
eprintln!("Setup localization failed: {e}");
}
assert!(result.is_ok());
// Verify we can get messages (using embedded English)
let message = get_message("test-about");
assert_eq!(message, "Check file types and compare values."); // Should use embedded English
})
.join()
.unwrap();
@@ -956,7 +1030,7 @@ invalid-syntax = This is { $missing
let temp_path_main = temp_dir.path().to_path_buf();
let main_handle = thread::spawn(move || {
let locale = LanguageIdentifier::from_str("fr-FR").unwrap();
init_localization(&locale, &temp_path_main).unwrap();
init_localization(&locale, &temp_path_main, "nonexistent_test_util").unwrap();
let main_message = get_message("greeting");
assert_eq!(main_message, "Bonjour, le monde!");
});
@@ -971,7 +1045,7 @@ invalid-syntax = This is { $missing
// Initialize in this thread with English
let en_locale = LanguageIdentifier::from_str("en-US").unwrap();
init_localization(&en_locale, &temp_path).unwrap();
init_localization(&en_locale, &temp_path, "nonexistent_test_util").unwrap();
let thread_message_after_init = get_message("greeting");
assert_eq!(thread_message_after_init, "Hello, world!");
});
@@ -989,11 +1063,12 @@ invalid-syntax = This is { $missing
#[test]
fn test_japanese_localization() {
use fluent::FluentArgs;
std::thread::spawn(|| {
let temp_dir = create_test_locales_dir();
let locale = LanguageIdentifier::from_str("ja-JP").unwrap();
let result = init_localization(&locale, temp_dir.path());
let result = init_localization(&locale, temp_dir.path(), "nonexistent_test_util");
assert!(result.is_ok());
// Test Japanese greeting
@@ -1018,11 +1093,12 @@ invalid-syntax = This is { $missing
#[test]
fn test_arabic_localization() {
use fluent::FluentArgs;
std::thread::spawn(|| {
let temp_dir = create_test_locales_dir();
let locale = LanguageIdentifier::from_str("ar-SA").unwrap();
let result = init_localization(&locale, temp_dir.path());
let result = init_localization(&locale, temp_dir.path(), "nonexistent_test_util");
assert!(result.is_ok());
// Test Arabic greeting (RTL text)
@@ -1077,7 +1153,7 @@ invalid-syntax = This is { $missing
let temp_dir = create_test_locales_dir();
let locale = LanguageIdentifier::from_str("ar-SA").unwrap();
let result = init_localization(&locale, temp_dir.path());
let result = init_localization(&locale, temp_dir.path(), "nonexistent_test_util");
assert!(result.is_ok());
// Test Arabic greeting (RTL text)
@@ -1118,7 +1194,7 @@ invalid-syntax = This is { $missing
let temp_dir = create_test_locales_dir();
let locale = LanguageIdentifier::from_str("ar-SA").unwrap();
let result = init_localization(&locale, temp_dir.path());
let result = init_localization(&locale, temp_dir.path(), "nonexistent_test_util");
assert!(result.is_ok());
// Test Arabic message exists
@@ -1132,13 +1208,15 @@ invalid-syntax = This is { $missing
.join()
.unwrap();
}
#[test]
fn test_unicode_directional_isolation_disabled() {
use fluent::FluentArgs;
std::thread::spawn(|| {
let temp_dir = create_test_locales_dir();
let locale = LanguageIdentifier::from_str("ar-SA").unwrap();
init_localization(&locale, temp_dir.path()).unwrap();
init_localization(&locale, temp_dir.path(), "nonexistent_test_util").unwrap();
// Test that Latin script names are NOT isolated in RTL context
// since we disabled Unicode directional isolation