lang-lib 1.1.0

A lightweight, high-performance localization library for Rust. Loads TOML language files, supports runtime locale switching, configurable paths, and automatic fallback chains.
Documentation

Language Library ( lang-lib ) is a file-based multi-language translation library for Rust. It loads TOML translation files at startup and serves lookups by key, with runtime locale switching and configurable fallback chains. Designed to be simple, fast, lightweight, concurrent, and lock-free, it stays focused on doing one thing well — translation — without the weight of a full internationalization framework.

Setup is deliberately frictionless. There's no code generation, no build script, no CLI tooling, and no compile-time macros to wrestle with, just map your language files, call the t! macro, and you're translating. Drop your TOML files in a directory, point lang-lib at it, and every key is available across your application. Add a new language by adding a file; no rebuild step, no schema regeneration, nothing to wire up.

Every part of the API is shaped to reduce the amount of code you write. The t! macro handles the common case in a single line, accepts an optional locale override, and takes an inline fallback for missing keys, covering three distinct lookup patterns with one consistent call. For web handlers that need a fixed locale per request, a lightweight Translator wraps the active locale so you call .translate("key") without repeating it. No setup objects, no lifecycle management, no plumbing, just the call you actually wanted to make.

A simple, lightweight library with a high-performance, enterprise-ready core; engineered for maximum stability and the resilience to thrive under load. Don't let the simplicity fool you: underneath is a heavily-tested, performance-tuned, and fully error-hardened engine. Simplicity, stability, and high-performance — together, with zero compromise.

FEATURES

  • Multi-Language Support — Load any number of locales from plain TOML files and switch the active language at runtime. No fixed locale set, no recompile to add one.

  • Zero-Allocation Lookups — Translation values are interned at load time into a process-wide pool, and every successful lookup returns a Cow::Borrowed(&'static str) that points directly into that pool. The hot path costs zero heap allocations. Fallback and key-return paths also avoid allocation by borrowing from the caller's inputs.

  • Lock-Free Reads — Translation state is swapped atomically via ArcSwap. Concurrent lookups never take a lock and never contend, scaling cleanly across cores.

  • Thread-Safe — Every public type is Send + Sync. Share one translation store across your entire application; call it from any thread without external synchronization.

  • Configurable Fallback Chains — When a key is missing in the active locale, lang-lib walks an ordered fallback list before giving up, so partial translations degrade gracefully instead of breaking.

  • Runtime Locale Switching — Change the active language on the fly without reloading files or rebuilding state. Ideal for per-request locale selection in web servers.

  • One-Line API — A single t! macro covers the common case, with optional locale and inline-fallback arguments. No translator object to construct, no context to thread through your call stack.

  • No Build Step — No code generation, no build script, no CLI tooling. Map your language files, call the macro, ship. Adding a language means adding a file.

  • Minimal Dependencies — A lean dependency graph and a small surface area keep compile times fast and audits simple.

  • Cross-Platform — Runs identically on Linux, macOS, and Windows.

Installation

[dependencies]
lang-lib = "1.1.0"

Quick Start

use lang_lib::{t, Lang};

// Point to your project's lang folder
Lang::set_path("assets/lang");

// Load the locales you need
Lang::load("en").unwrap();
Lang::load("es").unwrap();

// Set the active locale
Lang::set_locale("en");

// Set a fallback chain (checked when a key is missing)
Lang::set_fallbacks(vec!["en".to_string()]);

// Translate
let msg = t!("bad_password");                          // active locale
let msg = t!("bad_password", "es");                    // specific locale
let msg = t!("unknown_key", fallback: "Oops");         // inline fallback
let msg = t!("unknown_key", "es", fallback: "Oops");   // locale + fallback

Tutorial

If you are wiring this into a real application, the usual setup looks like this.

1. Create a locale directory

your-app/
|- Cargo.toml
|- src/
|  \- main.rs
\- locales/
   |- en.toml
   \- es.toml

2. Create language files

Keep each file flat. One translation key maps to one string value.

# locales/en.toml
app_title = "Acme Control Panel"
login_title = "Sign in"
login_button = "Continue"
bad_password = "Your password is incorrect."
network_error = "We could not reach the server."
# locales/es.toml
app_title = "Panel de Control Acme"
login_title = "Iniciar sesion"
login_button = "Continuar"
bad_password = "La contrasena es incorrecta."
network_error = "No pudimos conectarnos al servidor."

Rules that matter:

  • File names become locale identifiers, so en.toml loads as en.
  • Locale identifiers must be simple file stems like en, en-US, or pt_BR.
  • Nested TOML tables and non-string values are ignored.
  • Keep keys stable and descriptive. Treat them like public API for your UI.

3. Load locales during startup

use lang_lib::Lang;

fn configure_i18n() -> Result<(), lang_lib::LangError> {
	Lang::set_path("locales");
	Lang::load("en")?;
	Lang::load("es")?;
	Lang::set_fallbacks(vec!["en".to_string()]);
	Lang::set_locale("en");
	Ok(())
}

4. Translate where the text is rendered

use lang_lib::{t, Lang};

fn render_login() {
	println!("{}", t!("login_title"));
	println!("{}", t!("login_button"));

	Lang::set_locale("es");
	println!("{}", t!("login_title"));
	println!("{}", t!("missing_key", fallback: "Default copy"));
}

5. Run the included example

The repository includes a runnable example wired to sample locale files:

cargo run --example basic

Server-Side Locale Policy

For request-driven services, the safest pattern is simple: load locales once at startup, resolve the locale for each request, and pass that locale explicitly when translating.

That means you should usually avoid calling Lang::set_locale inside request handlers. Lang stores its active locale as process-global state, so changing it per request creates unnecessary cross-request coupling.

Preferred pattern:

use lang_lib::{resolve_accept_language, Lang};

fn render_for_request(header: &str) -> String {
	let locale = resolve_accept_language(header, &["en", "es"], "en");
	Lang::translate("login_title", Some(locale), Some("Sign in"))
}

Runnable server-oriented example:

cargo run --example server

If you want less repetition inside handlers, create a request-scoped helper:

use lang_lib::{resolve_accept_language, Lang};


fn render_for_request(header: &str) -> String {
	let locale = resolve_accept_language(header, &["en", "es"], "en");
	let translator = Lang::translator(locale);
	translator.translate_with_fallback("login_title", "Sign in")
}

Accept-Language Helper

If your application already receives an Accept-Language header, the crate now includes a small helper for turning that header into one of your supported locale identifiers.

use lang_lib::resolve_accept_language;

let locale = resolve_accept_language(
	"es-ES,es;q=0.9,en;q=0.8",
	&["en", "es"],
	"en",
);

assert_eq!(locale, "es");

The helper prefers higher q values, then exact locale matches, then primary-language matches like es-ES -> es.

If your supported locales are built at runtime, use the owned variant instead:

use lang_lib::resolve_accept_language_owned;

let supported = vec!["en".to_string(), "es".to_string()];
let locale = resolve_accept_language_owned(
	"es-MX,es;q=0.9,en;q=0.7",
	&supported,
	"en",
);

assert_eq!(locale, "es");

In plain terms: this version is for cases where your locale list is not a hard-coded &["en", "es"], but comes from config or some other runtime data.

Translator Helper

Translator is a tiny convenience wrapper around a locale string. It keeps request-local code readable while still using the safe server-side policy.

use lang_lib::{Lang, Translator};

fn render_page(locale: &str) -> String {
	let translator = Translator::new(locale);
	translator.translate_with_fallback("dashboard_title", "Dashboard")
}

fn render_page_via_lang(locale: &str) -> String {
	let translator = Lang::translator(locale);
	translator.translate("dashboard_title")
}

This helper does not change Lang::locale(). It only bundles a locale with repeated translation calls.

Axum Example

The repository also includes a real axum example that plugs locale resolution into an HTTP handler.

Run it with:

cargo run --example axum_server --features web-example-axum

Then request it with different Accept-Language headers:

curl http://127.0.0.1:3000/
curl -H "Accept-Language: es-ES,es;q=0.9" http://127.0.0.1:3000/

If you prefer actix-web, the repository includes a matching example:

cargo run --example actix_server --features web-example-actix

The three server-oriented examples share the same locale bootstrap and Accept-Language parsing helper, so their behavior stays aligned as the examples evolve.

File Format

Plain TOML, one key per string:

# locales/en.toml
bad_password = "Your password is incorrect."
not_found    = "The page you requested does not exist."

Files are resolved as {path}/{locale}.toml. Locale identifiers must be simple file stems like en, en-US, or pt_BR. Path separators and relative path components are rejected before file access.

API Notes

  • Lang::set_path changes the base directory used by Lang::load.
  • Lang::load_from lets you load a locale from a one-off directory.
  • Lang::set_locale changes the process-wide active locale.
  • Lang::translator creates a request-scoped helper for repeated lookups.
  • resolve_accept_language maps an Accept-Language header to one of your supported locales.
  • resolve_accept_language_owned does the same job when your supported locales live in Vec<String> or similar runtime data.
  • Lang::set_fallbacks controls the order used when a key is missing.
  • Lang::loaded returns a sorted list, which is useful for diagnostics.

Fallback Behavior

When a key is not found, lookup proceeds as follows:

  1. Requested locale (or active locale)
  2. Each locale in the fallback chain, in order
  3. Inline fallback: value if provided in t!
  4. The key string itself — never returns empty

Error Handling

lang-lib keeps failure modes narrow and explicit.

use lang_lib::{Lang, LangError};

match Lang::load("en") {
	Ok(()) => {}
	Err(LangError::Io { locale, cause }) => {
		eprintln!("could not read {locale}: {cause}");
	}
	Err(LangError::Parse { locale, cause }) => {
		eprintln!("invalid TOML in {locale}: {cause}");
	}
	Err(LangError::InvalidLocale { locale }) => {
		eprintln!("rejected invalid locale identifier: {locale}");
	}
	Err(LangError::NotLoaded { locale }) => {
		eprintln!("locale was expected but not loaded: {locale}");
	}
}

Tips For Production Use

  • Load all required locales during startup instead of lazily during request handling.
  • Keep one fallback locale with complete coverage, usually en.
  • In servers, resolve a locale per request and pass it explicitly instead of mutating the global active locale.
  • Treat translation keys as stable identifiers and review changes to them carefully.

Production Notes

  • File lookup is cross-platform and uses the platform's native path handling.
  • Locale loading rejects path traversal inputs such as ../en or nested paths.
  • Internal state recovers from poisoned locks instead of panicking on future reads.
  • Lang::loaded() returns a sorted list for deterministic diagnostics and tests.

Benchmarks

The repository includes a small Criterion benchmark that measures two hot paths:

  • resolve_accept_language
  • translation lookup through the loaded in-memory store
  • fallback-chain lookup when a key is missing in the requested locale
  • complete miss with inline fallback string
  • complete miss that returns the key itself

Run it with:

cargo bench --bench performance

This is not a full benchmarking suite, but it gives you repeatable numbers for the operations most likely to matter in a request-driven application.

For interpretation guidance and CI benchmark policy, see BENCHMARKS.md.

Health Signals

The badges at the top of this README point to the CI and benchmark workflows. If they are blank right after adding workflows, trigger the workflows once and GitHub will start showing status immediately.

Repository Examples

  • examples/basic.rs: end-to-end startup and translation flow.
  • examples/server.rs: request-scoped locale resolution for server-side code.
  • examples/axum_server.rs: real axum handler using request-scoped translation.
  • examples/actix_server.rs: real actix-web handler using the same request-scoped policy.
  • examples/common/mod.rs: shared example helper for locale loading and request locale resolution.
  • examples/locales/en.toml: sample English locale file.
  • examples/locales/es.toml: sample Spanish locale file.
  • BENCHMARKS.md: benchmark usage notes, regression guidance, and CI benchmark policy.
  • benches/performance.rs: Criterion benchmark for request locale resolution and translation lookup.

License

Licensed under either of:

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.