Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,098 changes: 1,098 additions & 0 deletions .github/workflows/l10n.yml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
32 changes: 18 additions & 14 deletions GNUmakefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 29 additions & 5 deletions docs/src/l10n.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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.

---

Expand Down Expand Up @@ -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.

---

Expand All @@ -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.
1 change: 1 addition & 0 deletions fuzz/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion src/uucore/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
204 changes: 204 additions & 0 deletions src/uucore/build.rs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for having this functionality in uucore?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because uucore is a dependency of every utility, so its build.rs always runs

cargo build -p uu_cat

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, at least from a conceptual point of view I'm struggling with this approach. For me uucore is a library used by different projects and thus it feels odd to have a dependency back to one of those projects. Wouldn't it be possible to do the same thing in build.rs of coreutils?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed
i am planning to work on it later
(esp for uptime)

Original file line number Diff line number Diff line change
@@ -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(())
}
2 changes: 1 addition & 1 deletion src/uucore/src/lib/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading