Skip to content

jamesgober/lang-lib

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

lang-lib

Crates.io Crates.io Downloads Docs.rs CI Benchmarks

A lightweight, high-performance localization library for Rust.

Loads TOML language files, supports runtime locale switching, configurable paths per project, and automatic fallback chains. No proc macros, no codegen, no CLI tooling — just a macro and a map.

The crate is deliberately small. The goal is not to invent a translation platform. The goal is to make it easy to ship a Rust application with readable locale files, predictable fallback behavior, and almost no integration cost.

Installation

[dependencies]
lang-lib = "1.0.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

Apache-2.0 — Copyright © 2026 James Gober

About

Lightweight, high-performance localization library for Rust. TOML lang files, runtime locale switching, configurable paths, fallback chains.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages