mirror of
https://github.com/uutils/coreutils.git
synced 2026-06-10 15:48:22 -07:00
Merge pull request #8423 from sylvestre/en-embedded
l10n: embedded english strings + test the whole thing in github
This commit is contained in:
File diff suppressed because it is too large
Load Diff
Generated
+1
@@ -4083,6 +4083,7 @@ dependencies = [
|
||||
"dns-lookup",
|
||||
"dunce",
|
||||
"fluent",
|
||||
"fluent-bundle",
|
||||
"fluent-syntax",
|
||||
"glob",
|
||||
"hex",
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
+18
-14
@@ -418,25 +418,29 @@ endif
|
||||
|
||||
ifeq ($(LOCALES),y)
|
||||
locales:
|
||||
$(foreach prog, $(INSTALLEES), \
|
||||
if [ -d "$(BASEDIR)/src/uu/$(prog)/locales" ]; then \
|
||||
mkdir -p "$(BUILDDIR)/locales/$(prog)"; \
|
||||
for locale_file in "$(BASEDIR)"/src/uu/$(prog)/locales/*.ftl; do \
|
||||
$(INSTALL) -v "$$locale_file" "$(BUILDDIR)/locales/$(prog)/"; \
|
||||
@for prog in $(INSTALLEES); do \
|
||||
if [ -d "$(BASEDIR)/src/uu/$$prog/locales" ]; then \
|
||||
mkdir -p "$(BUILDDIR)/locales/$$prog"; \
|
||||
for locale_file in "$(BASEDIR)"/src/uu/$$prog/locales/*.ftl; do \
|
||||
if [ "$$(basename "$$locale_file")" != "en-US.ftl" ]; then \
|
||||
$(INSTALL) -v "$$locale_file" "$(BUILDDIR)/locales/$$prog/"; \
|
||||
fi; \
|
||||
done; \
|
||||
fi $(newline) \
|
||||
)
|
||||
fi; \
|
||||
done
|
||||
|
||||
|
||||
install-locales:
|
||||
$(foreach prog, $(INSTALLEES), \
|
||||
if [ -d "$(BASEDIR)/src/uu/$(prog)/locales" ]; then \
|
||||
mkdir -p "$(DESTDIR)$(DATAROOTDIR)/locales/$(prog)"; \
|
||||
for locale_file in "$(BASEDIR)"/src/uu/$(prog)/locales/*.ftl; do \
|
||||
$(INSTALL) -v "$$locale_file" "$(DESTDIR)$(DATAROOTDIR)/locales/$(prog)/"; \
|
||||
@for prog in $(INSTALLEES); do \
|
||||
if [ -d "$(BASEDIR)/src/uu/$$prog/locales" ]; then \
|
||||
mkdir -p "$(DESTDIR)$(DATAROOTDIR)/locales/$$prog"; \
|
||||
for locale_file in "$(BASEDIR)"/src/uu/$$prog/locales/*.ftl; do \
|
||||
if [ "$$(basename "$$locale_file")" != "en-US.ftl" ]; then \
|
||||
$(INSTALL) -v "$$locale_file" "$(DESTDIR)$(DATAROOTDIR)/locales/$$prog/"; \
|
||||
fi; \
|
||||
done; \
|
||||
fi $(newline) \
|
||||
)
|
||||
fi; \
|
||||
done
|
||||
else
|
||||
install-locales:
|
||||
endif
|
||||
|
||||
+29
-5
@@ -2,6 +2,15 @@
|
||||
|
||||
This guide explains how localization (L10n) is implemented in the **Rust-based coreutils project**, detailing the use of [Fluent](https://projectfluent.org/) files, runtime behavior, and developer integration.
|
||||
|
||||
## 🏗️ Architecture Overview
|
||||
|
||||
**English (US) locale files (`en-US.ftl`) are embedded directly in the binary**, ensuring that English always works regardless of how the software is installed. Other language locale files are loaded from the filesystem at runtime.
|
||||
|
||||
### Source Repository Structure
|
||||
|
||||
- **Main repository**: Contains English (`en-US.ftl`) locale files embedded in binaries
|
||||
- **Translation repository**: [uutils/coreutils-l10n](https://github.com/uutils/coreutils-l10n) contains all other language translations
|
||||
|
||||
---
|
||||
|
||||
## 📁 Fluent File Layout
|
||||
@@ -15,8 +24,8 @@ Each utility has its own set of translation files under:
|
||||
Examples:
|
||||
|
||||
```
|
||||
src/uu/ls/locales/en-US.ftl
|
||||
src/uu/ls/locales/fr-FR.ftl
|
||||
src/uu/ls/locales/en-US.ftl # Embedded in binary
|
||||
src/uu/ls/locales/fr-FR.ftl # Loaded from filesystem
|
||||
```
|
||||
|
||||
These files follow Fluent syntax and contain localized message patterns.
|
||||
@@ -31,12 +40,11 @@ Localization must be explicitly initialized at runtime using:
|
||||
setup_localization(path)
|
||||
```
|
||||
|
||||
|
||||
This is typically done:
|
||||
- In `src/bin/coreutils.rs` for **multi-call binaries**
|
||||
- In `src/uucore/src/lib.rs` for **single-call utilities**
|
||||
|
||||
The string parameter determines the lookup path for Fluent files.
|
||||
The string parameter determines the lookup path for Fluent files. **English always works** because it's embedded, but other languages need their `.ftl` files to be available at runtime.
|
||||
|
||||
---
|
||||
|
||||
@@ -155,9 +163,13 @@ In release mode, **paths are resolved relative to the executable**:
|
||||
|
||||
```
|
||||
<executable_dir>/locales/<utility>/
|
||||
<prefix>/share/locales/<utility>/
|
||||
~/.local/share/coreutils/locales/<utility>/
|
||||
~/.cargo/share/coreutils/locales/<utility>/
|
||||
/usr/share/coreutils/locales/<utility>/
|
||||
```
|
||||
|
||||
If both fallback paths fail, an error is returned during `setup_localization()`.
|
||||
If external locale files aren't found, the system falls back to embedded English locales.
|
||||
|
||||
---
|
||||
|
||||
@@ -184,3 +196,15 @@ Fluent default (disabled here):
|
||||
```
|
||||
"\u{2068}Alice\u{2069}"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Embedded English Locales
|
||||
|
||||
English locale files are always embedded directly in the binary during the build process. This ensures that:
|
||||
|
||||
- **English always works** regardless of installation method (e.g., `cargo install`)
|
||||
- **No runtime dependency** on external `.ftl` files for English
|
||||
- **Fallback behavior** when other language files are missing
|
||||
|
||||
The embedded English locales are generated at build time and included in the binary, providing a reliable fallback while still supporting full localization for other languages when their `.ftl` files are available.
|
||||
|
||||
Generated
+1
@@ -1606,6 +1606,7 @@ dependencies = [
|
||||
"digest",
|
||||
"dunce",
|
||||
"fluent",
|
||||
"fluent-bundle",
|
||||
"fluent-syntax",
|
||||
"glob",
|
||||
"hex",
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
// 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::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let out_dir = env::var("OUT_DIR")?;
|
||||
|
||||
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"
|
||||
)?;
|
||||
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)?;
|
||||
|
||||
// Try to detect if we're building for a specific utility by checking build configuration
|
||||
// This attempts to identify individual utility builds vs multicall binary builds
|
||||
let target_utility = detect_target_utility();
|
||||
|
||||
match target_utility {
|
||||
Some(util_name) => {
|
||||
// Embed only the specific utility's locale (cat.ftl for cat for example)
|
||||
embed_single_utility_locale(&mut embedded_file, &project_root()?, &util_name)?;
|
||||
}
|
||||
None => {
|
||||
// Embed all utilities locales (multicall binary or fallback)
|
||||
embed_all_utilities_locales(&mut embedded_file, &project_root()?)?;
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(embedded_file)?;
|
||||
writeln!(embedded_file, " locales")?;
|
||||
writeln!(embedded_file, "}}")?;
|
||||
|
||||
embedded_file.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the project root directory
|
||||
fn project_root() -> Result<std::path::PathBuf, Box<dyn std::error::Error>> {
|
||||
let manifest_dir = env::var("CARGO_MANIFEST_DIR")?;
|
||||
let uucore_path = std::path::Path::new(&manifest_dir);
|
||||
|
||||
// Navigate from src/uucore to project root
|
||||
let project_root = uucore_path
|
||||
.parent() // src/
|
||||
.and_then(|p| p.parent()) // project root
|
||||
.ok_or("Could not determine project root")?;
|
||||
|
||||
Ok(project_root.to_path_buf())
|
||||
}
|
||||
|
||||
/// Attempt to detect which specific utility is being built
|
||||
fn detect_target_utility() -> Option<String> {
|
||||
use std::fs;
|
||||
|
||||
// First check if an explicit environment variable was set
|
||||
if let Ok(target_util) = env::var("UUCORE_TARGET_UTIL") {
|
||||
if !target_util.is_empty() {
|
||||
return Some(target_util);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for a build configuration file in the target directory
|
||||
if let Ok(target_dir) = env::var("CARGO_TARGET_DIR") {
|
||||
let config_path = std::path::Path::new(&target_dir).join("uucore_target_util.txt");
|
||||
if let Ok(content) = fs::read_to_string(&config_path) {
|
||||
let util_name = content.trim();
|
||||
if !util_name.is_empty() && util_name != "multicall" {
|
||||
return Some(util_name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Check the default target directory
|
||||
if let Ok(project_root) = project_root() {
|
||||
let config_path = project_root.join("target/uucore_target_util.txt");
|
||||
if let Ok(content) = fs::read_to_string(&config_path) {
|
||||
let util_name = content.trim();
|
||||
if !util_name.is_empty() && util_name != "multicall" {
|
||||
return Some(util_name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no configuration found, assume multicall build
|
||||
None
|
||||
}
|
||||
|
||||
/// Embed locale for a single specific utility
|
||||
fn embed_single_utility_locale(
|
||||
embedded_file: &mut std::fs::File,
|
||||
project_root: &Path,
|
||||
util_name: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use std::fs;
|
||||
|
||||
// Embed the specific utility's locale
|
||||
let locale_path = project_root
|
||||
.join("src/uu")
|
||||
.join(util_name)
|
||||
.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());
|
||||
}
|
||||
|
||||
// Always 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());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Embed locale files for all utilities (multicall binary)
|
||||
fn embed_all_utilities_locales(
|
||||
embedded_file: &mut std::fs::File,
|
||||
project_root: &Path,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use std::fs;
|
||||
|
||||
// Discover all uu_* directories
|
||||
let src_uu_dir = project_root.join("src/uu");
|
||||
if !src_uu_dir.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut util_dirs = Vec::new();
|
||||
for entry in fs::read_dir(&src_uu_dir)? {
|
||||
let entry = entry?;
|
||||
if entry.file_type()?.is_dir() {
|
||||
if let Some(dir_name) = entry.file_name().to_str() {
|
||||
util_dirs.push(dir_name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
util_dirs.sort();
|
||||
|
||||
// Embed locale files for each utility
|
||||
for util_name in &util_dirs {
|
||||
let locale_path = src_uu_dir.join(util_name).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());
|
||||
}
|
||||
|
||||
embedded_file.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -185,7 +185,7 @@ macro_rules! bin {
|
||||
uucore::locale::LocalizationError::ParseResource {
|
||||
error: err_msg,
|
||||
snippet,
|
||||
} => eprintln!("Localization parse error at {snippet}: {err_msg}"),
|
||||
} => eprintln!("Localization parse error at {snippet}: {err_msg:?}"),
|
||||
other => eprintln!("Could not init the localization system: {other}"),
|
||||
}
|
||||
std::process::exit(99)
|
||||
|
||||
@@ -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,22 @@ thread_local! {
|
||||
fn init_localization(
|
||||
locale: &LanguageIdentifier,
|
||||
locales_dir: &Path,
|
||||
util_name: &str,
|
||||
) -> Result<(), LocalizationError> {
|
||||
let en_locale = LanguageIdentifier::from_str(DEFAULT_LOCALE)
|
||||
let default_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 embedded resources first, then fall back to filesystem.
|
||||
// This ensures consistent behavior and faster loading since embedded resources
|
||||
// are immediately available. The filesystem fallback allows for development
|
||||
// and testing scenarios where locale files might be present in the filesystem.
|
||||
let english_bundle =
|
||||
create_english_bundle_from_embedded(&default_locale, util_name).or_else(|_| {
|
||||
// Try filesystem as fallback (useful for development/testing)
|
||||
create_bundle(&default_locale, locales_dir)
|
||||
})?;
|
||||
|
||||
let loc = if locale == &default_locale {
|
||||
// If requesting English, just use English as primary (no fallback needed)
|
||||
Localizer::new(english_bundle)
|
||||
} else {
|
||||
@@ -180,6 +193,56 @@ fn create_bundle(
|
||||
Ok(bundle)
|
||||
}
|
||||
|
||||
/// Create a bundle from embedded English locale files
|
||||
fn create_english_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, errs): (FluentResource, Vec<ParserError>)| {
|
||||
if let Some(first_err) = errs.into_iter().next() {
|
||||
let snippet = first_err
|
||||
.slice
|
||||
.clone()
|
||||
.and_then(|range| ftl_content.get(range))
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
LocalizationError::ParseResource {
|
||||
error: first_err,
|
||||
snippet,
|
||||
}
|
||||
} else {
|
||||
LocalizationError::LocalesDirNotFound("Parse error without details".to_string())
|
||||
}
|
||||
},
|
||||
)?;
|
||||
|
||||
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 +368,25 @@ 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 find the locales directory. If found, use init_localization which
|
||||
// will prioritize embedded resources but can also load from filesystem.
|
||||
// If no locales directory exists, directly use embedded English resources.
|
||||
match get_locales_dir(p) {
|
||||
Ok(locales_dir) => init_localization(&locale, &locales_dir, p),
|
||||
Err(_) => {
|
||||
// No locales directory found, use embedded English directly
|
||||
let default_locale = LanguageIdentifier::from_str(DEFAULT_LOCALE)
|
||||
.expect("Default locale should always be valid");
|
||||
let english_bundle = create_english_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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
@@ -603,6 +683,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 +745,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 +765,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 +786,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 +804,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 +828,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 +849,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 +868,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 +890,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 +1003,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 +1036,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 +1051,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 +1069,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 +1099,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 +1159,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 +1200,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 +1214,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
|
||||
|
||||
Reference in New Issue
Block a user